Add T3 Connect onboarding for mobile and web#3765
Conversation
- Present an onboarding sheet after in-session sign-in - Let users publish local environments and connect cloud environments - Persist onboarding completion per account across mobile sessions
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 5 potential issues.
Bugbot Autofix prepared fixes for all 5 issues found in the latest run.
- ✅ Fixed: Account switch skips onboarding
- CloudAuthBridge now requests onboarding on any in-session transition to a different signed-in account (including A→B switches), not only null→account, while still excluding cold start.
- ✅ Fixed: Stale request overwrites atom
- A new account atom synced from CloudAuthBridge lets requestConnectOnboardingIfNeeded re-validate the account after its async preference load and drops any pending request when the signed-in account changes, so stale requests can no longer overwrite the atom or navigate for the wrong user.
- ✅ Fixed: Sheet marks switched account complete
- The mobile sheet now captures the account it opened for and persists completion only when that account still matches the current user, matching web's openForAccount behavior.
- ✅ Fixed: Publish targets load once
- The load effect now tracks loaded environment ids and reloads when a bearer connection appears that was never checked, so tokens surfacing after catalog readiness are picked up without reloading on removals or identity churn.
- ✅ Fixed: Web opens on cold start
- The web dialog now tracks the observed account and only opens after a sign-in that completed during the current session, matching the mobile trigger instead of opening on every cold start.
Or push these changes by commenting:
@cursor push 0f795434f1
Preview (0f795434f1)
diff --git a/apps/mobile/src/features/cloud/CloudAuthProvider.tsx b/apps/mobile/src/features/cloud/CloudAuthProvider.tsx
--- a/apps/mobile/src/features/cloud/CloudAuthProvider.tsx
+++ b/apps/mobile/src/features/cloud/CloudAuthProvider.tsx
@@ -18,7 +18,10 @@
setAgentAwarenessRelayTokenProvider,
unregisterAgentAwarenessDeviceForCurrentUser,
} from "../agent-awareness/remoteRegistration";
-import { requestConnectOnboardingIfNeeded } from "./connectOnboarding";
+import {
+ requestConnectOnboardingIfNeeded,
+ syncConnectOnboardingAccount,
+} from "./connectOnboarding";
import { resolveCloudPublicConfig, resolveRelayClerkTokenOptions } from "./publicConfig";
function resetManagedRelayTokenCache() {
@@ -67,11 +70,17 @@
const previousObservedAccount = observedAccountRef.current;
const nextAccount = isSignedIn && userId ? userId : null;
observedAccountRef.current = nextAccount;
+ syncConnectOnboardingAccount(nextAccount);
- // A sign-in that completed during this session (a cold start observes
- // undefined → account and must not re-prompt) requests the T3 Connect
- // onboarding sheet once per account.
- if (previousObservedAccount === null && nextAccount !== null) {
+ // A sign-in that completed during this session — from signed-out or by
+ // switching accounts (a cold start observes undefined → account and must
+ // not re-prompt) — requests the T3 Connect onboarding sheet once per
+ // account.
+ if (
+ previousObservedAccount !== undefined &&
+ previousObservedAccount !== nextAccount &&
+ nextAccount !== null
+ ) {
void (async () => {
const result = await settlePromise(() => requestConnectOnboardingIfNeeded(nextAccount));
reportAtomCommandResult(result, { label: "connect onboarding request" });
diff --git a/apps/mobile/src/features/cloud/ConnectOnboardingRouteScreen.tsx b/apps/mobile/src/features/cloud/ConnectOnboardingRouteScreen.tsx
--- a/apps/mobile/src/features/cloud/ConnectOnboardingRouteScreen.tsx
+++ b/apps/mobile/src/features/cloud/ConnectOnboardingRouteScreen.tsx
@@ -78,15 +78,23 @@
cloudEnvironments: null,
});
- // Dismissing the sheet (swipe, Skip, or Done) counts as completion.
+ // Dismissing the sheet (swipe, Skip, or Done) counts as completion — but
+ // only for the account the sheet opened for. Signing out or switching
+ // accounts mid-flow dismisses without persisting, so the new account still
+ // gets its own onboarding.
+ const openedForAccountRef = useRef<string | null>(null);
+ if (openedForAccountRef.current === null && userId) {
+ openedForAccountRef.current = userId;
+ }
const completionPersistedRef = useRef(false);
const persistCompletion = useCallback(() => {
- if (completionPersistedRef.current || !userId) {
+ const accountId = openedForAccountRef.current;
+ if (completionPersistedRef.current || !accountId || accountId !== userId) {
return;
}
completionPersistedRef.current = true;
void (async () => {
- const result = await settlePromise(() => markConnectOnboardingCompleted(userId));
+ const result = await settlePromise(() => markConnectOnboardingCompleted(accountId));
reportAtomCommandResult(result, { label: "connect onboarding completion" });
})();
}, [userId]);
@@ -172,14 +180,26 @@
);
// Check authorization + link state once the locally saved connections are
- // ready. Loaded once per mount: reconnect churn in the registry must not
- // flip the section back to "checking" mid-interaction.
- const loadedRef = useRef(false);
+ // ready. Bearer tokens can surface after the catalog is ready (prepared
+ // connections load asynchronously), so reload when a connection appears
+ // that was never checked — but never reload on removals or identity churn,
+ // which must not flip the section back to "checking" mid-interaction.
+ const loadedEnvironmentIdsRef = useRef<Set<string> | null>(null);
useEffect(() => {
- if (isLoadingSavedConnection || loadedRef.current) {
+ if (isLoadingSavedConnection) {
return;
}
- loadedRef.current = true;
+ const loadedIds = loadedEnvironmentIdsRef.current;
+ if (
+ loadedIds !== null &&
+ bearerConnections.every((connection) => loadedIds.has(connection.environmentId))
+ ) {
+ return;
+ }
+ loadedEnvironmentIdsRef.current = new Set([
+ ...(loadedIds ?? []),
+ ...bearerConnections.map((connection) => connection.environmentId),
+ ]);
void loadPublishTargets(bearerConnections);
}, [bearerConnections, isLoadingSavedConnection, loadPublishTargets]);
diff --git a/apps/mobile/src/features/cloud/connectOnboarding.ts b/apps/mobile/src/features/cloud/connectOnboarding.ts
--- a/apps/mobile/src/features/cloud/connectOnboarding.ts
+++ b/apps/mobile/src/features/cloud/connectOnboarding.ts
@@ -15,12 +15,38 @@
Atom.withLabel("mobile:connect-onboarding-request"),
);
+// The account CloudAuthBridge currently observes. Requests are only valid
+// while their account stays signed in.
+const connectOnboardingAccountAtom = Atom.make<string | null>(null).pipe(
+ Atom.keepAlive,
+ Atom.withLabel("mobile:connect-onboarding-account"),
+);
+
/**
+ * Records the signed-in account observed by CloudAuthBridge. A sign-out or
+ * account switch drops any pending onboarding request for another account —
+ * including a request whose preference load is still in flight — so the sheet
+ * can never present for an account that is no longer signed in.
+ */
+export function syncConnectOnboardingAccount(accountId: string | null): void {
+ appAtomRegistry.set(connectOnboardingAccountAtom, accountId);
+ const requested = appAtomRegistry.get(connectOnboardingRequestAtom);
+ if (requested !== null && requested !== accountId) {
+ clearConnectOnboardingRequest();
+ }
+}
+
+/**
* Requests the onboarding sheet for the given account unless it already
* completed (or skipped) onboarding on this device.
*/
export async function requestConnectOnboardingIfNeeded(accountId: string): Promise<void> {
const preferences = await loadPreferences();
+ // The account may have signed out (or been switched) while the preference
+ // load was in flight; a stale request must not overwrite the current state.
+ if (appAtomRegistry.get(connectOnboardingAccountAtom) !== accountId) {
+ return;
+ }
if (preferences.connectOnboardingCompletedAccounts?.includes(accountId)) {
return;
}
diff --git a/apps/web/src/components/cloud/ConnectOnboardingDialog.tsx b/apps/web/src/components/cloud/ConnectOnboardingDialog.tsx
--- a/apps/web/src/components/cloud/ConnectOnboardingDialog.tsx
+++ b/apps/web/src/components/cloud/ConnectOnboardingDialog.tsx
@@ -30,11 +30,12 @@
/**
* Post-sign-in onboarding wizard for T3 Connect. Opens once per account (per
- * browser) after the user is signed in: first prompts to publish this
- * environment (managed tunnel + agent activity, both defaulting on) when the
- * current session is authorized to manage the relay link, then lists the
- * account's T3 Connect environments so every device can be connected right
- * away. Dismissing the dialog counts as completion — it never nags twice.
+ * browser) after a sign-in completes during this session: first prompts to
+ * publish this environment (managed tunnel + agent activity, both defaulting
+ * on) when the current session is authorized to manage the relay link, then
+ * lists the account's T3 Connect environments so every device can be
+ * connected right away. Dismissing the dialog counts as completion — it never
+ * nags twice.
*/
export function ConnectOnboardingDialog() {
if (!hasCloudPublicConfig()) return null;
@@ -84,8 +85,28 @@
const completedAccounts = onboardingState.completedAccounts;
+ // Only a sign-in that completed during this session prompts onboarding: a
+ // cold start (a page load observing an already signed-in user) must not
+ // re-prompt, matching the mobile trigger.
+ const observedAccountRef = useRef<string | null | undefined>(undefined);
+ const [signedInAccount, setSignedInAccount] = useState<string | null>(null);
useEffect(() => {
+ if (!isLoaded) return;
+ const previousObservedAccount = observedAccountRef.current;
+ const nextAccount = isSignedIn && userId ? userId : null;
+ observedAccountRef.current = nextAccount;
+ if (
+ previousObservedAccount !== undefined &&
+ previousObservedAccount !== nextAccount &&
+ nextAccount !== null
+ ) {
+ setSignedInAccount(nextAccount);
+ }
+ }, [isLoaded, isSignedIn, userId]);
+
+ useEffect(() => {
if (!isLoaded || !isSignedIn || !userId) return;
+ if (signedInAccount !== userId) return;
if (openForAccount !== null) return;
if (completedAccounts.includes(userId)) return;
if (!sessionScopesKnown) return;
@@ -102,6 +123,7 @@
isSignedIn,
openForAccount,
sessionScopesKnown,
+ signedInAccount,
userId,
]);You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 53ab016. Configure here.
| const result = await settlePromise(() => requestConnectOnboardingIfNeeded(nextAccount)); | ||
| reportAtomCommandResult(result, { label: "connect onboarding request" }); | ||
| })(); | ||
| } |
There was a problem hiding this comment.
Account switch skips onboarding
Medium Severity
The T3 Connect onboarding is currently requested only when a user transitions from a signed-out state to a signed-in state. This means switching directly between different signed-in accounts in the same session won't trigger the onboarding, so users may miss the first-run sheet.
Reviewed by Cursor Bugbot for commit 53ab016. Configure here.
| const timer = setTimeout(() => { | ||
| clearConnectOnboardingRequest(); | ||
| navigation.navigate("ConnectOnboarding"); | ||
| }, PRESENT_ONBOARDING_DELAY_MS); |
There was a problem hiding this comment.
Stale request overwrites atom
High Severity
The T3 Connect onboarding flow has race conditions with user authentication. A stale account ID can be stored in connectOnboardingRequestAtom, causing the onboarding sheet to appear for the wrong or no user after a delay. Dismissal also marks completion using the current user ID, potentially marking the wrong account as onboarded if the user signs out or switches accounts while the sheet is open.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 53ab016. Configure here.
| useEffect( | ||
| () => navigation.addListener("beforeRemove", persistCompletion), | ||
| [navigation, persistCompletion], | ||
| ); |
There was a problem hiding this comment.
Sheet marks switched account complete
Medium Severity
The onboarding sheet always persists completion for the current userId on any beforeRemove, and never tracks which account opened it. If the user switches accounts while the sheet is open, dismissing can mark the new account complete even though onboarding was triggered for someone else—unlike web, which closes without persisting when userId !== openForAccount.
Reviewed by Cursor Bugbot for commit 53ab016. Configure here.
| } | ||
| loadedRef.current = true; | ||
| void loadPublishTargets(bearerConnections); | ||
| }, [bearerConnections, isLoadingSavedConnection, loadPublishTargets]); |
There was a problem hiding this comment.
Publish targets load once
High Severity
The publish section runs loadPublishTargets only once per sheet mount because loadedRef stays true after the first run. isLoadingSavedConnection tracks catalog readiness, not bearer tokens on saved connections, so the first pass can see zero bearer connections while preparedConnection is still loading. Later tokens never trigger a reload, so onboarding can show “no locally paired environments” despite paired envs on the device.
Reviewed by Cursor Bugbot for commit 53ab016. Configure here.
| setExposeEnvironment(true); | ||
| setPublishAgentActivity(true); | ||
| setStep(canManageRelay && controller.linkState.target !== null ? "publish" : "devices"); | ||
| setOpenForAccount(userId); |
There was a problem hiding this comment.
Web opens on cold start
Medium Severity
The web dialog opens whenever a loaded, signed-in user is not in completedAccounts and session scopes are known, including on a normal app reload. Mobile only requests onboarding after an in-session sign-in (null → account), so returning users who already signed in can get the wizard on every visit until they dismiss it, not only right after signing in during that session.
Reviewed by Cursor Bugbot for commit 53ab016. Configure here.
| const navigation = useNavigation(); | ||
| const requestedAccountId = useAtomValue(connectOnboardingRequestAtom); | ||
|
|
||
| useEffect(() => { |
There was a problem hiding this comment.
🟡 Medium cloud/connectOnboarding.ts:56
The 600 ms timer in useConnectOnboardingNavigation fires navigation.navigate("ConnectOnboarding") without re-checking that the account that requested onboarding is still the signed-in account. When a user signs out before the delay elapses, the sheet still opens; if a different account then signs in while the sheet is open, the screen calls markConnectOnboardingCompleted for that new account, recording onboarding completion for the wrong user.
The effect only guards on requestedAccountId changing and never validates the current auth state inside the setTimeout callback. Consider capturing the account id at queue time and verifying it still matches the active session before navigating, or cancelling the timer on sign-out.
Also found in 1 other location(s)
apps/mobile/src/features/cloud/CloudAuthProvider.tsx:76
The new onboarding trigger at
CloudAuthProvider.tsxline 76 runsrequestConnectOnboardingIfNeeded(nextAccount)in a detached async task without any cancellation or account re-check before it sets the request atom. If the user signs in and then signs out or switches accounts beforeloadPreferences()resolves, the old task still callsappAtomRegistry.set(connectOnboardingRequestAtom, accountId), anduseConnectOnboardingNavigationwill navigate toConnectOnboardingfor a stale/signed-out session. There is no later code that clears this request on sign-out, so a fast sign-out can still surface the onboarding sheet incorrectly.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/mobile/src/features/cloud/connectOnboarding.ts around line 56:
The 600 ms timer in `useConnectOnboardingNavigation` fires `navigation.navigate("ConnectOnboarding")` without re-checking that the account that requested onboarding is still the signed-in account. When a user signs out before the delay elapses, the sheet still opens; if a different account then signs in while the sheet is open, the screen calls `markConnectOnboardingCompleted` for that new account, recording onboarding completion for the wrong user.
The effect only guards on `requestedAccountId` changing and never validates the current auth state inside the `setTimeout` callback. Consider capturing the account id at queue time and verifying it still matches the active session before navigating, or cancelling the timer on sign-out.
Also found in 1 other location(s):
- apps/mobile/src/features/cloud/CloudAuthProvider.tsx:76 -- The new onboarding trigger at `CloudAuthProvider.tsx` line 76 runs `requestConnectOnboardingIfNeeded(nextAccount)` in a detached async task without any cancellation or account re-check before it sets the request atom. If the user signs in and then signs out or switches accounts before `loadPreferences()` resolves, the old task still calls `appAtomRegistry.set(connectOnboardingRequestAtom, accountId)`, and `useConnectOnboardingNavigation` will navigate to `ConnectOnboarding` for a stale/signed-out session. There is no later code that clears this request on sign-out, so a fast sign-out can still surface the onboarding sheet incorrectly.
| <Dialog | ||
| open={openForAccount !== null} | ||
| onOpenChange={(open) => { | ||
| if (!open) complete(); | ||
| }} | ||
| > |
There was a problem hiding this comment.
🟡 Medium cloud/ConnectOnboardingDialog.tsx:166
When the publish step's async reconcileCloudState() request is in flight, the user can still click the dialog's built-in close button (or otherwise dismiss it), which calls complete() and permanently adds the account to completedAccounts. If the in-flight request subsequently fails, the onboarding never appears again for that account, even though T3 Connect was never enabled. Consider guarding complete() to prevent dismissal while isApplying is true (e.g., skip the complete() call in onOpenChange when isApplying is set, and/or hide the close button during the request).
<Dialog
open={openForAccount !== null}
onOpenChange={(open) => {
- if (!open) complete();
+ if (!open && !isApplying) complete();
}}
>🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/cloud/ConnectOnboardingDialog.tsx around lines 166-171:
When the publish step's async `reconcileCloudState()` request is in flight, the user can still click the dialog's built-in close button (or otherwise dismiss it), which calls `complete()` and permanently adds the account to `completedAccounts`. If the in-flight request subsequently fails, the onboarding never appears again for that account, even though T3 Connect was never enabled. Consider guarding `complete()` to prevent dismissal while `isApplying` is true (e.g., skip the `complete()` call in `onOpenChange` when `isApplying` is set, and/or hide the close button during the request).
| const publishableTargets = (targets ?? []).filter((target) => target.phase === "ready"); | ||
| const publishedTargets = (targets ?? []).filter((target) => target.phase === "published"); | ||
| const hasRetryableErrors = (targets ?? []).some((target) => target.phase === "error"); | ||
| const canPublish = |
There was a problem hiding this comment.
🟡 Medium cloud/ConnectOnboardingRouteScreen.tsx:358
The publish button stays enabled even when the selected toggles would change nothing. When a ready target is ready only because managedTunnelActive is false and publishAgentActivity is already true, turning off publishEnvironment while leaving publishAgentActivity on means handlePublish performs no linkEnvironmentToCloud or setEnvironmentPublishAgentActivity call for that target — the user taps "Publish" and nothing happens, with no feedback. canPublish only checks that publishableTargets.length > 0 and at least one toggle is on; it should also verify that the toggles would actually produce a change (e.g. publishEnvironment needs a target that isn't fully linked/managed, or publishAgentActivity needs a target where it's not already on).
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/mobile/src/features/cloud/ConnectOnboardingRouteScreen.tsx around line 358:
The publish button stays enabled even when the selected toggles would change nothing. When a `ready` target is ready only because `managedTunnelActive` is false and `publishAgentActivity` is already `true`, turning off `publishEnvironment` while leaving `publishAgentActivity` on means `handlePublish` performs no `linkEnvironmentToCloud` or `setEnvironmentPublishAgentActivity` call for that target — the user taps "Publish" and nothing happens, with no feedback. `canPublish` only checks that `publishableTargets.length > 0` and at least one toggle is on; it should also verify that the toggles would actually produce a change (e.g. `publishEnvironment` needs a target that isn't fully linked/managed, or `publishAgentActivity` needs a target where it's not already on).
| ) : publishedTargets.length > 0 ? ( | ||
| <View className="p-4"> | ||
| <Text className="text-sm leading-normal text-foreground-muted"> | ||
| {"This device's environments already publish over T3 Connect."} | ||
| </Text> | ||
| </View> | ||
| ) : ( |
There was a problem hiding this comment.
🟡 Medium cloud/ConnectOnboardingRouteScreen.tsx:416
When one environment is published but another is unauthorized or error, the PublishSection renders "This device's environments already publish over T3 Connect." even though some environments on the device are not publishing. The publishedTargets.length > 0 branch is reached whenever publishableTargets.length === 0 and at least one target is published, so any mix of published and failed/unauthorized targets produces a false success message. Consider only showing this message when all targets are published, or render the per-target status rows so the failures are visible.
- <View className="p-4">
- <Text className="text-sm leading-normal text-foreground-muted">
- {"This device's environments already publish over T3 Connect."}
- </Text>
- </View>
+ <View className="p-4">
+ <Text className="text-sm leading-normal text-foreground-muted">
+ {publishedTargets.length === targets.length
+ ? "This device's environments already publish over T3 Connect."
+ : "Some of this device's environments can't be published. Check the status below."}
+ </Text>
+ </View>🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/mobile/src/features/cloud/ConnectOnboardingRouteScreen.tsx around lines 416-422:
When one environment is `published` but another is `unauthorized` or `error`, the `PublishSection` renders `"This device's environments already publish over T3 Connect."` even though some environments on the device are not publishing. The `publishedTargets.length > 0` branch is reached whenever `publishableTargets.length === 0` and at least one target is `published`, so any mix of published and failed/unauthorized targets produces a false success message. Consider only showing this message when all targets are `published`, or render the per-target status rows so the failures are visible.
| props.onDisconnect(); | ||
| }} | ||
| onToggleError={props.onToggleError} | ||
| value={props.environment.connectionState !== "available"} |
There was a problem hiding this comment.
🟡 Medium connection/CloudEnvironmentRows.tsx:157
The Switch in ConnectedCloudEnvironmentRow is bound to props.environment.connectionState !== "available", so it stays on for environments in "offline", "error", or "reconnecting" states even though they are not actually connected. In those states the user cannot flip the switch on to retry — it is already on — so the only available action is turning it off, which calls onDisconnect and removes the environment instead of reconnecting. The switch value should be driven by a condition that is true only for genuinely connected states (e.g. "connected"), so that failed or offline environments show an off switch that the user can toggle to retry.
| value={props.environment.connectionState !== "available"} | |
| value={props.environment.connectionState === "connected"} |
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/mobile/src/features/connection/CloudEnvironmentRows.tsx around line 157:
The `Switch` in `ConnectedCloudEnvironmentRow` is bound to `props.environment.connectionState !== "available"`, so it stays on for environments in `"offline"`, `"error"`, or `"reconnecting"` states even though they are not actually connected. In those states the user cannot flip the switch on to retry — it is already on — so the only available action is turning it off, which calls `onDisconnect` and removes the environment instead of reconnecting. The switch value should be driven by a condition that is true only for genuinely connected states (e.g. `"connected"`), so that failed or offline environments show an off switch that the user can toggle to retry.
ApprovabilityVerdict: Needs human review 5 blocking correctness issues found. This PR adds a significant new T3 Connect onboarding feature for mobile and web with ~1500+ lines of new code and new user-facing flows. Multiple unresolved high/medium severity bugs were identified involving auth race conditions that could mark the wrong account as onboarded. You can customize Macroscope's approvability policy. Learn more. |



Summary
Testing
vp check.vp run typecheck.vp testor targeted backend tests where applicable.Note
Medium Risk
Changes relay environment linking, session-scope checks, and post-sign-in UX across mobile and web; onboarding is mostly enable-only but still mutates cloud link state and preferences.
Overview
Adds a one-time-per-account T3 Connect onboarding flow on mobile (form sheet) and web (dialog), triggered after an in-session sign-in—not on cold start when the user was already signed in.
Mobile wires
CloudAuthProviderto request onboarding, persists completion in preferences (connectOnboardingCompletedAccounts), and presentsConnectOnboardingvia a delayed navigation hook. The sheet lets users publish locally paired bearer environments (managed tunnel and/or agent activity, defaults on, enable-only) and connect relay environments via sharedCloudEnvironmentRows(extracted from Settings).Cloud linking gains optional
CloudLinkMode(managedvspublish_only) on mobilelinkEnvironmentToCloud, plus helpers to read publish authorization/link state and set agent-activity preferences. Web extractsuseCloudLinkControllerfor reconciling managed tunnel + publish toggles (used by onboarding and Connections settings) and sharedCloudEnvironmentConnectRows/ConnectionStatusDot.Reviewed by Cursor Bugbot for commit 53ab016. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add T3 Connect onboarding flow for mobile and web
localStorageon web and in device preferences on mobile — to prevent repeat prompts.useCloudLinkControlleron web andlinkEnvironmentToCloudmode support on mobile to handle link/unlink, publish preferences, and relay refresh from a shared controller.📊 Macroscope summarized 53ab016. 16 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted
🗂️ Filtered Issues
No issues evaluated.