Skip to content

Conversation

@devin-ai-integration
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot commented Feb 10, 2026

fix: trial not starting during onboarding

Summary

Fixes trial not starting for new users during the onboarding flow. Two bugs in final.tsx:

  1. Stale closure bug: The useEffect had [auth, store] as dependencies. When auth.refreshSession() updated the session, auth object identity changed, causing React to re-fire the effect — but hasHandledRef blocked re-execution. The async handle() continued running with a stale auth from the original closure. Fixed by using refs (authRef, storeRef) and changing deps to [].

  2. Fixed 3s sleep was unreliable: The Stripe subscription creation → webhook → Supabase auth hook → JWT claims pipeline takes variable time. Replaced sleep(3000) with exponential backoff polling (1s → 5s cap, 10 attempts) that checks JWT claims for the hyprnote_pro entitlement before proceeding.

Also adds proper AbortController cleanup so the async polling is cancelled if the component unmounts.

Review & Testing Checklist for Human

  • Test the full onboarding flow end-to-end in a production-like environment — this fix was based on code analysis only; the desktop app was not built/tested locally. Verify a new user can sign up → reach Final step → trial starts → "You're all set!" appears with pro entitlements in the JWT.
  • Verify polling timeout UX: If the Stripe webhook is slow and polling times out after ~30s, the user sees "You're all set!" without pro claims in the token. Is this acceptable, or should we show a warning/retry?
  • Verify authCommands.decodeClaims works correctly with the access token format — the polling utility depends on this to detect hyprnote_pro in entitlements.
  • Check that the skipLogin path still works — when auth.session is null, the effect should set isLoading=false immediately without attempting trial.

Notes


Open with Devin

Co-Authored-By: yujonglee <yujonglee.dev@gmail.com>
@netlify
Copy link

netlify bot commented Feb 10, 2026

Deploy Preview for hyprnote-storybook canceled.

Name Link
🔨 Latest commit 6859463
🔍 Latest deploy log https://app.netlify.com/projects/hyprnote-storybook/deploys/698a8d213b1df100080c14d4

@netlify
Copy link

netlify bot commented Feb 10, 2026

Deploy Preview for hyprnote canceled.

Name Link
🔨 Latest commit 6859463
🔍 Latest deploy log https://app.netlify.com/projects/hyprnote/deploys/698a8d2169de7c00089dc83c

@devin-ai-integration
Copy link
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

Copy link
Contributor Author

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment on lines 76 to +83
};

void handle();
}, [auth, store]);

return () => {
abortController.abort();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
Copy link
Contributor Author

Choose a reason for hiding this comment

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

🟡 AbortController cleanup + hasHandledRef causes infinite loading in React StrictMode

In development mode with React StrictMode (enabled at apps/desktop/src/main.tsx:133), the component gets stuck on the loading screen forever.

Root Cause: StrictMode double-mount interacts badly with hasHandledRef + AbortController

React StrictMode unmounts and re-mounts components to surface side-effect bugs. The sequence is:

  1. First mount: Effect runs → hasHandledRef.current set to truehandle() starts async → cleanup function returned
  2. StrictMode unmount: Cleanup runs → abortController.abort() → the in-flight handle() will see the abort
  3. Second mount: Effect runs again → hasHandledRef.current is still true (refs persist across StrictMode remounts) → returns early without starting any work or returning a cleanup function

The handle() from step 1 reaches pollForTrialActivation which detects the aborted signal and returns { status: "aborted" }. Back in handle() at line 66, if (result.status === "aborted") return; causes it to return without calling setIsLoading(false). The second mount never starts a new handle() because hasHandledRef blocks it.

Result: isLoading remains true forever, and the user sees the spinner indefinitely.

Impact: Developers working on the onboarding flow in dev mode will be unable to proceed past the final step. This doesn't affect production builds since StrictMode effects only double-fire in development.

The fix should either: (a) remove hasHandledRef and rely solely on the AbortController cleanup (letting StrictMode re-run the effect properly on remount), or (b) reset hasHandledRef.current = false in the cleanup function so the second mount can re-execute.

(Refers to lines 38-83)

Prompt for agents
The hasHandledRef guard and AbortController cleanup are incompatible under React StrictMode. In StrictMode, the cleanup aborts the first execution, but hasHandledRef prevents the second execution from starting, leaving the user stuck on loading.

Two possible fixes:

Option A (recommended): Remove hasHandledRef entirely and rely on AbortController for cleanup. The effect will re-run on StrictMode remount, starting a fresh handle() with a new AbortController. The previous one is properly cleaned up via abort.

In apps/desktop/src/components/onboarding/final.tsx, remove lines 29 (hasHandledRef declaration), 38-40 (the guard and set), and keep the rest of the useEffect as-is. The AbortController cleanup already handles the double-mount case correctly.

Option B: Reset hasHandledRef in the cleanup function by adding hasHandledRef.current = false before abortController.abort() in the cleanup at line 80-82.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant