Skip to content

Add T3 Connect onboarding for mobile and web#3765

Open
juliusmarminge wants to merge 1 commit into
mainfrom
t3code/connect-onboarding-wizard
Open

Add T3 Connect onboarding for mobile and web#3765
juliusmarminge wants to merge 1 commit into
mainfrom
t3code/connect-onboarding-wizard

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Jul 7, 2026

Copy link
Copy Markdown
Member

Summary

  • Adds a first-run T3 Connect onboarding flow that appears after in-session sign-in and only once per account per device.
  • Lets users publish local paired environments from onboarding, including environment linking and agent activity publishing state.
  • Adds a connect onboarding sheet/dialog on mobile and web, plus supporting navigation, storage, and cloud-linking updates.
  • Adjusts mobile widget asset wiring and splash asset handling as part of the onboarding-related app config cleanup.

Testing

  • Not run (not provided in the diff summary).
  • Expected checks: vp check.
  • Expected checks: vp run typecheck.
  • Expected checks for backend-related changes: vp test or 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 CloudAuthProvider to request onboarding, persists completion in preferences (connectOnboardingCompletedAccounts), and presents ConnectOnboarding via 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 shared CloudEnvironmentRows (extracted from Settings).

Cloud linking gains optional CloudLinkMode (managed vs publish_only) on mobile linkEnvironmentToCloud, plus helpers to read publish authorization/link state and set agent-activity preferences. Web extracts useCloudLinkController for reconciling managed tunnel + publish toggles (used by onboarding and Connections settings) and shared CloudEnvironmentConnectRows / 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

  • Adds a multi-step onboarding dialog/screen for T3 Connect on both web (ConnectOnboardingDialog) and mobile (ConnectOnboardingRouteScreen), shown once per account after sign-in.
  • Users can configure managed tunnel and agent activity publishing, and connect additional environments from within the onboarding flow.
  • Onboarding completion is persisted per account — in localStorage on web and in device preferences on mobile — to prevent repeat prompts.
  • Introduces useCloudLinkController on web and linkEnvironmentToCloud mode support on mobile to handle link/unlink, publish preferences, and relay refresh from a shared controller.
  • Refactors shared UI (connection status dot, environment connect rows, item row constants) into reusable components consumed by both onboarding and settings.
📊 Macroscope summarized 53ab016. 16 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

- 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
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2792a347-f3ca-42b5-9a12-46a86a90bee9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/connect-onboarding-wizard

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Jul 7, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 5 potential issues.

Fix All in Cursor

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.

Create PR

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" });
})();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 53ab016. Configure here.

const timer = setTimeout(() => {
clearConnectOnboardingRequest();
navigation.navigate("ConnectOnboarding");
}, PRESENT_ONBOARDING_DELAY_MS);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 53ab016. Configure here.

useEffect(
() => navigation.addListener("beforeRemove", persistCompletion),
[navigation, persistCompletion],
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 53ab016. Configure here.

}
loadedRef.current = true;
void loadPublishTargets(bearerConnections);
}, [bearerConnections, isLoadingSavedConnection, loadPublishTargets]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 53ab016. Configure here.

setExposeEnvironment(true);
setPublishAgentActivity(true);
setStep(canManageRelay && controller.linkState.target !== null ? "publish" : "devices");
setOpenForAccount(userId);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 53ab016. Configure here.

const navigation = useNavigation();
const requestedAccountId = useAtomValue(connectOnboardingRequestAtom);

useEffect(() => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.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.

🚀 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.

Comment on lines +166 to +171
<Dialog
open={openForAccount !== null}
onOpenChange={(open) => {
if (!open) complete();
}}
>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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).

Comment on lines +416 to +422
) : 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>
) : (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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"}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Suggested change
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.

@macroscopeapp

macroscopeapp Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant