fix(onboarding): persist completion so users aren't trapped in the Get Started loop - #284

Merged
AndresL230 merged 1 commit into
mainfrom
fix/onboarding-completion-persistence
Jun 30, 2026
Merged

fix(onboarding): persist completion so users aren't trapped in the Get Started loop#284
AndresL230 merged 1 commit into
mainfrom
fix/onboarding-completion-persistence

Conversation

@AndresL230

@AndresL230AndresL230 commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Problem

On staging, signing in repeatedly re-shows the "Get Started" / onboarding flow every time, even for an already-onboarded, approved user. Investigation (against the staging DB) showed the user row persists fine — the only thing stuck is the onboarding_completed boolean, which had been False since the account was created despite many sign-ins.

Root cause

page.tsx:handleOnboardingComplete posted to /api/onboarding/profile using the component-local API_URL = process.env.NEXT_PUBLIC_API_URL — a cross-origin subdomain (https://api.staging.saplinglearn.com) — without credentials: 'include'. The browser therefore dropped the sapling_session cookie, so the backend's require_self returned 401 and the onboarding_completed = True write (backend/routes/onboarding.py:42) never ran.

The failure was invisible because the call used await fetch(...) with no res.ok check inside a try/catch that only catches network errors — so the 401 was swallowed and the user was routed to /dashboard as if it succeeded, then bounced back to onboarding on the next load.

Reproduced live: POST https://api.staging.saplinglearn.com/api/onboarding/profile with no cookie → 401 {"detail":"Not authenticated"}.

Fix

Use the existing submitOnboardingProfile() helper from lib/api.ts, which goes through fetchJSON:

  • same-origin relative path (API_URL = '', proxied to the backend via the /api/* rewrite) → the sapling_session cookie is sent
  • credentials: 'include'
  • checks res.ok and throws on failure, so errors surface instead of being hidden

Blast radius

Audited all other direct NEXT_PUBLIC_API_URL usages — this was the only authenticated cross-origin fetch. The rest are non-authed (/api/onboarding/courses search) or top-level OAuth popup navigations (/api/auth/google), and middleware.ts sends the Cookie header explicitly server-side.

Verification

  • npm run typecheck (tsc --noEmit) passes with 0 errors.
  • Affected staging user's onboarding_completed was manually set to True to unblock immediately; this PR fixes the persistence path so it no longer recurs.

Note: main's Frontend CI check is independently red repo-wide (lockfile + eslint baseline), unrelated to this change.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved onboarding completion so profile details are saved more reliably.
    • Reduced issues caused by browser cookie and login-session handling during onboarding.
    • Kept the rest of the onboarding flow unchanged.

…g sticks
handleOnboardingComplete POSTed /api/onboarding/profile to the cross-origin
NEXT_PUBLIC_API_URL without credentials:'include', so the browser dropped the
sapling_session cookie and the backend's require_self returned 401. The
onboarding_completed flag never flipped to True — and because the code never
checked res.ok, the 401 was swallowed and users were routed to /dashboard as if
it worked, then bounced back to "Get Started" on every subsequent sign-in.
Switch to the existing submitOnboardingProfile() helper, which goes through the
same-origin lib/api.ts fetchJSON path (API_URL='' proxied via the /api/* rewrite,
credentials:'include', and an res.ok check that surfaces failures instead of
hiding them).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

In frontend/src/app/page.tsx, the onboarding profile persistence in handleOnboardingComplete is switched from a raw fetch POST to ${API_URL}/api/onboarding/profile to a call to the shared submitOnboardingProfile helper imported from @/lib/api, along with its OnboardingProfilePayload type.

Changes

Onboarding persistence refactor

Layer / File(s)Summary
Replace raw fetch with submitOnboardingProfile
frontend/src/app/page.tsx
Imports submitOnboardingProfile and OnboardingProfilePayload from @/lib/api; replaces the raw fetch POST in handleOnboardingComplete with the helper call, mapping the same form fields and casting learning_style.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~3 minutes

Poem

🐇 No more raw fetch, no cookie woes,
A helper now carries where onboarding goes.
submitOnboardingProfile hops into place,
Clean imports and casts with elegant grace.
The rabbit approves this tidy embrace! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title is specific and accurately summarizes the main change: persisting onboarding completion to stop the Get Started loop.
Description check✅ PassedThe description is detailed and covers the issue, fix, and verification, though it doesn't follow the template's exact section headings.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/onboarding-completion-persistence

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging830555eCommit Preview URL

Branch Preview URL
Jun 30 2026, 01:30 AM

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/app/page.tsx (1)

624-637: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Stop the success flow when onboarding persistence fails.

submitOnboardingProfile now throws on non-OK responses, but this catch logs the error and then falls through to the completion animation and dashboard redirect. If the profile save returns 401/500, onboarding_completed can remain false and recreate the loop this PR is fixing.

🐛 Proposed fix
 } catch (e) {
console.error('Failed to save onboarding profile:', e);
+ return;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/app/page.tsx` around lines 624 - 637, The onboarding save error
is being swallowed in the submit path, allowing the success/redirect flow to
continue even when submitOnboardingProfile fails. Update the handler around
submitOnboardingProfile in page.tsx so that the catch path stops the completion
animation and dashboard redirect, and only advances the success flow when the
profile persistence call succeeds. Keep the existing error logging, but ensure
the branch after the try/catch does not run as if onboarding completed when the
request returns a non-OK response.
🧹 Nitpick comments (1)
frontend/src/app/page.tsx (1)

633-633: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Avoid casting unvalidated learning styles into the API contract.

formData.style is typed as string, so the assertion can send invalid values despite OnboardingProfilePayload restricting learning_style to a fixed union.

♻️ Proposed refactor
+ const learningStyle = formData.style;+ if (+ learningStyle !== 'visual' &&+ learningStyle !== 'reading' &&+ learningStyle !== 'auditory' &&+ learningStyle !== 'hands-on' &&+ learningStyle !== 'mixed'+ ) {+ console.error('Invalid onboarding learning style:', learningStyle);+ return;+ }+
await submitOnboardingProfile({
user_id: userId,
first_name: formData.firstName,
last_name: formData.lastName,
year: formData.year,
majors: formData.majors,
minors: formData.minors,
course_ids: formData.course_ids,
- learning_style: formData.style as OnboardingProfilePayload['learning_style'],+ learning_style: learningStyle,
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/app/page.tsx` at line 633, The onboarding payload is casting
formData.style directly into OnboardingProfilePayload['learning_style'], which
can bypass the union restriction and send invalid values. Update the code in the
page.tsx form submission flow to validate formData.style against the allowed
learning_style options before building the payload, and only assign a value that
is already narrowed to the contract in the onboarding request object.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@frontend/src/app/page.tsx`:
- Around line 624-637: The onboarding save error is being swallowed in the
submit path, allowing the success/redirect flow to continue even when
submitOnboardingProfile fails. Update the handler around submitOnboardingProfile
in page.tsx so that the catch path stops the completion animation and dashboard
redirect, and only advances the success flow when the profile persistence call
succeeds. Keep the existing error logging, but ensure the branch after the
try/catch does not run as if onboarding completed when the request returns a
non-OK response.
---
Nitpick comments:
In `@frontend/src/app/page.tsx`:
- Line 633: The onboarding payload is casting formData.style directly into
OnboardingProfilePayload['learning_style'], which can bypass the union
restriction and send invalid values. Update the code in the page.tsx form
submission flow to validate formData.style against the allowed learning_style
options before building the payload, and only assign a value that is already
narrowed to the contract in the onboarding request object.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 38c0e9e7-6934-4cc9-a544-0dae0aca21f3

📥 Commits

Reviewing files that changed from the base of the PR and between 76fb251 and 830555e.

📒 Files selected for processing (1)
  • frontend/src/app/page.tsx

@AndresL230
AndresL230 merged commit d86edde into mainJun 30, 2026
6 checks passed
@AndresL230
AndresL230 deleted the fix/onboarding-completion-persistence branch August 2, 2026 18:30
Sign up for freeto 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

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(onboarding): persist completion so users aren't trapped in the Get Started loop - #284

Merged
AndresL230 merged 1 commit into
mainfrom
fix/onboarding-completion-persistence
Jun 30, 2026
Merged

fix(onboarding): persist completion so users aren't trapped in the Get Started loop#284
AndresL230 merged 1 commit into
mainfrom
fix/onboarding-completion-persistence

Conversation

@AndresL230

@AndresL230AndresL230 commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Problem

On staging, signing in repeatedly re-shows the "Get Started" / onboarding flow every time, even for an already-onboarded, approved user. Investigation (against the staging DB) showed the user row persists fine — the only thing stuck is the onboarding_completed boolean, which had been False since the account was created despite many sign-ins.

Root cause

page.tsx:handleOnboardingComplete posted to /api/onboarding/profile using the component-local API_URL = process.env.NEXT_PUBLIC_API_URL — a cross-origin subdomain (https://api.staging.saplinglearn.com) — without credentials: 'include'. The browser therefore dropped the sapling_session cookie, so the backend's require_self returned 401 and the onboarding_completed = True write (backend/routes/onboarding.py:42) never ran.

The failure was invisible because the call used await fetch(...) with no res.ok check inside a try/catch that only catches network errors — so the 401 was swallowed and the user was routed to /dashboard as if it succeeded, then bounced back to onboarding on the next load.

Reproduced live: POST https://api.staging.saplinglearn.com/api/onboarding/profile with no cookie → 401 {"detail":"Not authenticated"}.

Fix

Use the existing submitOnboardingProfile() helper from lib/api.ts, which goes through fetchJSON:

  • same-origin relative path (API_URL = '', proxied to the backend via the /api/* rewrite) → the sapling_session cookie is sent
  • credentials: 'include'
  • checks res.ok and throws on failure, so errors surface instead of being hidden

Blast radius

Audited all other direct NEXT_PUBLIC_API_URL usages — this was the only authenticated cross-origin fetch. The rest are non-authed (/api/onboarding/courses search) or top-level OAuth popup navigations (/api/auth/google), and middleware.ts sends the Cookie header explicitly server-side.

Verification

  • npm run typecheck (tsc --noEmit) passes with 0 errors.
  • Affected staging user's onboarding_completed was manually set to True to unblock immediately; this PR fixes the persistence path so it no longer recurs.

Note: main's Frontend CI check is independently red repo-wide (lockfile + eslint baseline), unrelated to this change.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved onboarding completion so profile details are saved more reliably.
    • Reduced issues caused by browser cookie and login-session handling during onboarding.
    • Kept the rest of the onboarding flow unchanged.

…g sticks
handleOnboardingComplete POSTed /api/onboarding/profile to the cross-origin
NEXT_PUBLIC_API_URL without credentials:'include', so the browser dropped the
sapling_session cookie and the backend's require_self returned 401. The
onboarding_completed flag never flipped to True — and because the code never
checked res.ok, the 401 was swallowed and users were routed to /dashboard as if
it worked, then bounced back to "Get Started" on every subsequent sign-in.
Switch to the existing submitOnboardingProfile() helper, which goes through the
same-origin lib/api.ts fetchJSON path (API_URL='' proxied via the /api/* rewrite,
credentials:'include', and an res.ok check that surfaces failures instead of
hiding them).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

In frontend/src/app/page.tsx, the onboarding profile persistence in handleOnboardingComplete is switched from a raw fetch POST to ${API_URL}/api/onboarding/profile to a call to the shared submitOnboardingProfile helper imported from @/lib/api, along with its OnboardingProfilePayload type.

Changes

Onboarding persistence refactor

Layer / File(s)Summary
Replace raw fetch with submitOnboardingProfile
frontend/src/app/page.tsx
Imports submitOnboardingProfile and OnboardingProfilePayload from @/lib/api; replaces the raw fetch POST in handleOnboardingComplete with the helper call, mapping the same form fields and casting learning_style.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~3 minutes

Poem

🐇 No more raw fetch, no cookie woes,
A helper now carries where onboarding goes.
submitOnboardingProfile hops into place,
Clean imports and casts with elegant grace.
The rabbit approves this tidy embrace! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title is specific and accurately summarizes the main change: persisting onboarding completion to stop the Get Started loop.
Description check✅ PassedThe description is detailed and covers the issue, fix, and verification, though it doesn't follow the template's exact section headings.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/onboarding-completion-persistence

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging830555eCommit Preview URL

Branch Preview URL
Jun 30 2026, 01:30 AM

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/app/page.tsx (1)

624-637: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Stop the success flow when onboarding persistence fails.

submitOnboardingProfile now throws on non-OK responses, but this catch logs the error and then falls through to the completion animation and dashboard redirect. If the profile save returns 401/500, onboarding_completed can remain false and recreate the loop this PR is fixing.

🐛 Proposed fix
 } catch (e) {
console.error('Failed to save onboarding profile:', e);
+ return;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/app/page.tsx` around lines 624 - 637, The onboarding save error
is being swallowed in the submit path, allowing the success/redirect flow to
continue even when submitOnboardingProfile fails. Update the handler around
submitOnboardingProfile in page.tsx so that the catch path stops the completion
animation and dashboard redirect, and only advances the success flow when the
profile persistence call succeeds. Keep the existing error logging, but ensure
the branch after the try/catch does not run as if onboarding completed when the
request returns a non-OK response.
🧹 Nitpick comments (1)
frontend/src/app/page.tsx (1)

633-633: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Avoid casting unvalidated learning styles into the API contract.

formData.style is typed as string, so the assertion can send invalid values despite OnboardingProfilePayload restricting learning_style to a fixed union.

♻️ Proposed refactor
+ const learningStyle = formData.style;+ if (+ learningStyle !== 'visual' &&+ learningStyle !== 'reading' &&+ learningStyle !== 'auditory' &&+ learningStyle !== 'hands-on' &&+ learningStyle !== 'mixed'+ ) {+ console.error('Invalid onboarding learning style:', learningStyle);+ return;+ }+
await submitOnboardingProfile({
user_id: userId,
first_name: formData.firstName,
last_name: formData.lastName,
year: formData.year,
majors: formData.majors,
minors: formData.minors,
course_ids: formData.course_ids,
- learning_style: formData.style as OnboardingProfilePayload['learning_style'],+ learning_style: learningStyle,
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/app/page.tsx` at line 633, The onboarding payload is casting
formData.style directly into OnboardingProfilePayload['learning_style'], which
can bypass the union restriction and send invalid values. Update the code in the
page.tsx form submission flow to validate formData.style against the allowed
learning_style options before building the payload, and only assign a value that
is already narrowed to the contract in the onboarding request object.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@frontend/src/app/page.tsx`:
- Around line 624-637: The onboarding save error is being swallowed in the
submit path, allowing the success/redirect flow to continue even when
submitOnboardingProfile fails. Update the handler around submitOnboardingProfile
in page.tsx so that the catch path stops the completion animation and dashboard
redirect, and only advances the success flow when the profile persistence call
succeeds. Keep the existing error logging, but ensure the branch after the
try/catch does not run as if onboarding completed when the request returns a
non-OK response.
---
Nitpick comments:
In `@frontend/src/app/page.tsx`:
- Line 633: The onboarding payload is casting formData.style directly into
OnboardingProfilePayload['learning_style'], which can bypass the union
restriction and send invalid values. Update the code in the page.tsx form
submission flow to validate formData.style against the allowed learning_style
options before building the payload, and only assign a value that is already
narrowed to the contract in the onboarding request object.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 38c0e9e7-6934-4cc9-a544-0dae0aca21f3

📥 Commits

Reviewing files that changed from the base of the PR and between 76fb251 and 830555e.

📒 Files selected for processing (1)
  • frontend/src/app/page.tsx

@AndresL230
AndresL230 merged commit d86edde into mainJun 30, 2026
6 checks passed
@AndresL230
AndresL230 deleted the fix/onboarding-completion-persistence branch August 2, 2026 18:30
Sign up for freeto 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

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(onboarding): persist completion so users aren't trapped in the Get Started loop - #284

Merged
AndresL230 merged 1 commit into
mainfrom
fix/onboarding-completion-persistence
Jun 30, 2026
Merged

fix(onboarding): persist completion so users aren't trapped in the Get Started loop#284
AndresL230 merged 1 commit into
mainfrom
fix/onboarding-completion-persistence

Conversation

@AndresL230

@AndresL230AndresL230 commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Problem

On staging, signing in repeatedly re-shows the "Get Started" / onboarding flow every time, even for an already-onboarded, approved user. Investigation (against the staging DB) showed the user row persists fine — the only thing stuck is the onboarding_completed boolean, which had been False since the account was created despite many sign-ins.

Root cause

page.tsx:handleOnboardingComplete posted to /api/onboarding/profile using the component-local API_URL = process.env.NEXT_PUBLIC_API_URL — a cross-origin subdomain (https://api.staging.saplinglearn.com) — without credentials: 'include'. The browser therefore dropped the sapling_session cookie, so the backend's require_self returned 401 and the onboarding_completed = True write (backend/routes/onboarding.py:42) never ran.

The failure was invisible because the call used await fetch(...) with no res.ok check inside a try/catch that only catches network errors — so the 401 was swallowed and the user was routed to /dashboard as if it succeeded, then bounced back to onboarding on the next load.

Reproduced live: POST https://api.staging.saplinglearn.com/api/onboarding/profile with no cookie → 401 {"detail":"Not authenticated"}.

Fix

Use the existing submitOnboardingProfile() helper from lib/api.ts, which goes through fetchJSON:

  • same-origin relative path (API_URL = '', proxied to the backend via the /api/* rewrite) → the sapling_session cookie is sent
  • credentials: 'include'
  • checks res.ok and throws on failure, so errors surface instead of being hidden

Blast radius

Audited all other direct NEXT_PUBLIC_API_URL usages — this was the only authenticated cross-origin fetch. The rest are non-authed (/api/onboarding/courses search) or top-level OAuth popup navigations (/api/auth/google), and middleware.ts sends the Cookie header explicitly server-side.

Verification

  • npm run typecheck (tsc --noEmit) passes with 0 errors.
  • Affected staging user's onboarding_completed was manually set to True to unblock immediately; this PR fixes the persistence path so it no longer recurs.

Note: main's Frontend CI check is independently red repo-wide (lockfile + eslint baseline), unrelated to this change.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved onboarding completion so profile details are saved more reliably.
    • Reduced issues caused by browser cookie and login-session handling during onboarding.
    • Kept the rest of the onboarding flow unchanged.

…g sticks
handleOnboardingComplete POSTed /api/onboarding/profile to the cross-origin
NEXT_PUBLIC_API_URL without credentials:'include', so the browser dropped the
sapling_session cookie and the backend's require_self returned 401. The
onboarding_completed flag never flipped to True — and because the code never
checked res.ok, the 401 was swallowed and users were routed to /dashboard as if
it worked, then bounced back to "Get Started" on every subsequent sign-in.
Switch to the existing submitOnboardingProfile() helper, which goes through the
same-origin lib/api.ts fetchJSON path (API_URL='' proxied via the /api/* rewrite,
credentials:'include', and an res.ok check that surfaces failures instead of
hiding them).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

In frontend/src/app/page.tsx, the onboarding profile persistence in handleOnboardingComplete is switched from a raw fetch POST to ${API_URL}/api/onboarding/profile to a call to the shared submitOnboardingProfile helper imported from @/lib/api, along with its OnboardingProfilePayload type.

Changes

Onboarding persistence refactor

Layer / File(s)Summary
Replace raw fetch with submitOnboardingProfile
frontend/src/app/page.tsx
Imports submitOnboardingProfile and OnboardingProfilePayload from @/lib/api; replaces the raw fetch POST in handleOnboardingComplete with the helper call, mapping the same form fields and casting learning_style.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~3 minutes

Poem

🐇 No more raw fetch, no cookie woes,
A helper now carries where onboarding goes.
submitOnboardingProfile hops into place,
Clean imports and casts with elegant grace.
The rabbit approves this tidy embrace! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title is specific and accurately summarizes the main change: persisting onboarding completion to stop the Get Started loop.
Description check✅ PassedThe description is detailed and covers the issue, fix, and verification, though it doesn't follow the template's exact section headings.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/onboarding-completion-persistence

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging830555eCommit Preview URL

Branch Preview URL
Jun 30 2026, 01:30 AM

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/app/page.tsx (1)

624-637: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Stop the success flow when onboarding persistence fails.

submitOnboardingProfile now throws on non-OK responses, but this catch logs the error and then falls through to the completion animation and dashboard redirect. If the profile save returns 401/500, onboarding_completed can remain false and recreate the loop this PR is fixing.

🐛 Proposed fix
 } catch (e) {
console.error('Failed to save onboarding profile:', e);
+ return;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/app/page.tsx` around lines 624 - 637, The onboarding save error
is being swallowed in the submit path, allowing the success/redirect flow to
continue even when submitOnboardingProfile fails. Update the handler around
submitOnboardingProfile in page.tsx so that the catch path stops the completion
animation and dashboard redirect, and only advances the success flow when the
profile persistence call succeeds. Keep the existing error logging, but ensure
the branch after the try/catch does not run as if onboarding completed when the
request returns a non-OK response.
🧹 Nitpick comments (1)
frontend/src/app/page.tsx (1)

633-633: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Avoid casting unvalidated learning styles into the API contract.

formData.style is typed as string, so the assertion can send invalid values despite OnboardingProfilePayload restricting learning_style to a fixed union.

♻️ Proposed refactor
+ const learningStyle = formData.style;+ if (+ learningStyle !== 'visual' &&+ learningStyle !== 'reading' &&+ learningStyle !== 'auditory' &&+ learningStyle !== 'hands-on' &&+ learningStyle !== 'mixed'+ ) {+ console.error('Invalid onboarding learning style:', learningStyle);+ return;+ }+
await submitOnboardingProfile({
user_id: userId,
first_name: formData.firstName,
last_name: formData.lastName,
year: formData.year,
majors: formData.majors,
minors: formData.minors,
course_ids: formData.course_ids,
- learning_style: formData.style as OnboardingProfilePayload['learning_style'],+ learning_style: learningStyle,
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/app/page.tsx` at line 633, The onboarding payload is casting
formData.style directly into OnboardingProfilePayload['learning_style'], which
can bypass the union restriction and send invalid values. Update the code in the
page.tsx form submission flow to validate formData.style against the allowed
learning_style options before building the payload, and only assign a value that
is already narrowed to the contract in the onboarding request object.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@frontend/src/app/page.tsx`:
- Around line 624-637: The onboarding save error is being swallowed in the
submit path, allowing the success/redirect flow to continue even when
submitOnboardingProfile fails. Update the handler around submitOnboardingProfile
in page.tsx so that the catch path stops the completion animation and dashboard
redirect, and only advances the success flow when the profile persistence call
succeeds. Keep the existing error logging, but ensure the branch after the
try/catch does not run as if onboarding completed when the request returns a
non-OK response.
---
Nitpick comments:
In `@frontend/src/app/page.tsx`:
- Line 633: The onboarding payload is casting formData.style directly into
OnboardingProfilePayload['learning_style'], which can bypass the union
restriction and send invalid values. Update the code in the page.tsx form
submission flow to validate formData.style against the allowed learning_style
options before building the payload, and only assign a value that is already
narrowed to the contract in the onboarding request object.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 38c0e9e7-6934-4cc9-a544-0dae0aca21f3

📥 Commits

Reviewing files that changed from the base of the PR and between 76fb251 and 830555e.

📒 Files selected for processing (1)
  • frontend/src/app/page.tsx

@AndresL230
AndresL230 merged commit d86edde into mainJun 30, 2026
6 checks passed
@AndresL230
AndresL230 deleted the fix/onboarding-completion-persistence branch August 2, 2026 18:30
Sign up for freeto 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

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(onboarding): persist completion so users aren't trapped in the Get Started loop - #284

Merged
AndresL230 merged 1 commit into
mainfrom
fix/onboarding-completion-persistence
Jun 30, 2026
Merged

fix(onboarding): persist completion so users aren't trapped in the Get Started loop#284
AndresL230 merged 1 commit into
mainfrom
fix/onboarding-completion-persistence

Conversation

@AndresL230

@AndresL230AndresL230 commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Problem

On staging, signing in repeatedly re-shows the "Get Started" / onboarding flow every time, even for an already-onboarded, approved user. Investigation (against the staging DB) showed the user row persists fine — the only thing stuck is the onboarding_completed boolean, which had been False since the account was created despite many sign-ins.

Root cause

page.tsx:handleOnboardingComplete posted to /api/onboarding/profile using the component-local API_URL = process.env.NEXT_PUBLIC_API_URL — a cross-origin subdomain (https://api.staging.saplinglearn.com) — without credentials: 'include'. The browser therefore dropped the sapling_session cookie, so the backend's require_self returned 401 and the onboarding_completed = True write (backend/routes/onboarding.py:42) never ran.

The failure was invisible because the call used await fetch(...) with no res.ok check inside a try/catch that only catches network errors — so the 401 was swallowed and the user was routed to /dashboard as if it succeeded, then bounced back to onboarding on the next load.

Reproduced live: POST https://api.staging.saplinglearn.com/api/onboarding/profile with no cookie → 401 {"detail":"Not authenticated"}.

Fix

Use the existing submitOnboardingProfile() helper from lib/api.ts, which goes through fetchJSON:

  • same-origin relative path (API_URL = '', proxied to the backend via the /api/* rewrite) → the sapling_session cookie is sent
  • credentials: 'include'
  • checks res.ok and throws on failure, so errors surface instead of being hidden

Blast radius

Audited all other direct NEXT_PUBLIC_API_URL usages — this was the only authenticated cross-origin fetch. The rest are non-authed (/api/onboarding/courses search) or top-level OAuth popup navigations (/api/auth/google), and middleware.ts sends the Cookie header explicitly server-side.

Verification

  • npm run typecheck (tsc --noEmit) passes with 0 errors.
  • Affected staging user's onboarding_completed was manually set to True to unblock immediately; this PR fixes the persistence path so it no longer recurs.

Note: main's Frontend CI check is independently red repo-wide (lockfile + eslint baseline), unrelated to this change.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved onboarding completion so profile details are saved more reliably.
    • Reduced issues caused by browser cookie and login-session handling during onboarding.
    • Kept the rest of the onboarding flow unchanged.

…g sticks
handleOnboardingComplete POSTed /api/onboarding/profile to the cross-origin
NEXT_PUBLIC_API_URL without credentials:'include', so the browser dropped the
sapling_session cookie and the backend's require_self returned 401. The
onboarding_completed flag never flipped to True — and because the code never
checked res.ok, the 401 was swallowed and users were routed to /dashboard as if
it worked, then bounced back to "Get Started" on every subsequent sign-in.
Switch to the existing submitOnboardingProfile() helper, which goes through the
same-origin lib/api.ts fetchJSON path (API_URL='' proxied via the /api/* rewrite,
credentials:'include', and an res.ok check that surfaces failures instead of
hiding them).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

In frontend/src/app/page.tsx, the onboarding profile persistence in handleOnboardingComplete is switched from a raw fetch POST to ${API_URL}/api/onboarding/profile to a call to the shared submitOnboardingProfile helper imported from @/lib/api, along with its OnboardingProfilePayload type.

Changes

Onboarding persistence refactor

Layer / File(s)Summary
Replace raw fetch with submitOnboardingProfile
frontend/src/app/page.tsx
Imports submitOnboardingProfile and OnboardingProfilePayload from @/lib/api; replaces the raw fetch POST in handleOnboardingComplete with the helper call, mapping the same form fields and casting learning_style.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~3 minutes

Poem

🐇 No more raw fetch, no cookie woes,
A helper now carries where onboarding goes.
submitOnboardingProfile hops into place,
Clean imports and casts with elegant grace.
The rabbit approves this tidy embrace! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title is specific and accurately summarizes the main change: persisting onboarding completion to stop the Get Started loop.
Description check✅ PassedThe description is detailed and covers the issue, fix, and verification, though it doesn't follow the template's exact section headings.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/onboarding-completion-persistence

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging830555eCommit Preview URL

Branch Preview URL
Jun 30 2026, 01:30 AM

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/app/page.tsx (1)

624-637: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Stop the success flow when onboarding persistence fails.

submitOnboardingProfile now throws on non-OK responses, but this catch logs the error and then falls through to the completion animation and dashboard redirect. If the profile save returns 401/500, onboarding_completed can remain false and recreate the loop this PR is fixing.

🐛 Proposed fix
 } catch (e) {
console.error('Failed to save onboarding profile:', e);
+ return;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/app/page.tsx` around lines 624 - 637, The onboarding save error
is being swallowed in the submit path, allowing the success/redirect flow to
continue even when submitOnboardingProfile fails. Update the handler around
submitOnboardingProfile in page.tsx so that the catch path stops the completion
animation and dashboard redirect, and only advances the success flow when the
profile persistence call succeeds. Keep the existing error logging, but ensure
the branch after the try/catch does not run as if onboarding completed when the
request returns a non-OK response.
🧹 Nitpick comments (1)
frontend/src/app/page.tsx (1)

633-633: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Avoid casting unvalidated learning styles into the API contract.

formData.style is typed as string, so the assertion can send invalid values despite OnboardingProfilePayload restricting learning_style to a fixed union.

♻️ Proposed refactor
+ const learningStyle = formData.style;+ if (+ learningStyle !== 'visual' &&+ learningStyle !== 'reading' &&+ learningStyle !== 'auditory' &&+ learningStyle !== 'hands-on' &&+ learningStyle !== 'mixed'+ ) {+ console.error('Invalid onboarding learning style:', learningStyle);+ return;+ }+
await submitOnboardingProfile({
user_id: userId,
first_name: formData.firstName,
last_name: formData.lastName,
year: formData.year,
majors: formData.majors,
minors: formData.minors,
course_ids: formData.course_ids,
- learning_style: formData.style as OnboardingProfilePayload['learning_style'],+ learning_style: learningStyle,
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/app/page.tsx` at line 633, The onboarding payload is casting
formData.style directly into OnboardingProfilePayload['learning_style'], which
can bypass the union restriction and send invalid values. Update the code in the
page.tsx form submission flow to validate formData.style against the allowed
learning_style options before building the payload, and only assign a value that
is already narrowed to the contract in the onboarding request object.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@frontend/src/app/page.tsx`:
- Around line 624-637: The onboarding save error is being swallowed in the
submit path, allowing the success/redirect flow to continue even when
submitOnboardingProfile fails. Update the handler around submitOnboardingProfile
in page.tsx so that the catch path stops the completion animation and dashboard
redirect, and only advances the success flow when the profile persistence call
succeeds. Keep the existing error logging, but ensure the branch after the
try/catch does not run as if onboarding completed when the request returns a
non-OK response.
---
Nitpick comments:
In `@frontend/src/app/page.tsx`:
- Line 633: The onboarding payload is casting formData.style directly into
OnboardingProfilePayload['learning_style'], which can bypass the union
restriction and send invalid values. Update the code in the page.tsx form
submission flow to validate formData.style against the allowed learning_style
options before building the payload, and only assign a value that is already
narrowed to the contract in the onboarding request object.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 38c0e9e7-6934-4cc9-a544-0dae0aca21f3

📥 Commits

Reviewing files that changed from the base of the PR and between 76fb251 and 830555e.

📒 Files selected for processing (1)
  • frontend/src/app/page.tsx

@AndresL230
AndresL230 merged commit d86edde into mainJun 30, 2026
6 checks passed
@AndresL230
AndresL230 deleted the fix/onboarding-completion-persistence branch August 2, 2026 18:30
Sign up for freeto 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

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(onboarding): persist completion so users aren't trapped in the Get Started loop - #284

Merged
AndresL230 merged 1 commit into
mainfrom
fix/onboarding-completion-persistence
Jun 30, 2026
Merged

fix(onboarding): persist completion so users aren't trapped in the Get Started loop#284
AndresL230 merged 1 commit into
mainfrom
fix/onboarding-completion-persistence

Conversation

@AndresL230

@AndresL230AndresL230 commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Problem

On staging, signing in repeatedly re-shows the "Get Started" / onboarding flow every time, even for an already-onboarded, approved user. Investigation (against the staging DB) showed the user row persists fine — the only thing stuck is the onboarding_completed boolean, which had been False since the account was created despite many sign-ins.

Root cause

page.tsx:handleOnboardingComplete posted to /api/onboarding/profile using the component-local API_URL = process.env.NEXT_PUBLIC_API_URL — a cross-origin subdomain (https://api.staging.saplinglearn.com) — without credentials: 'include'. The browser therefore dropped the sapling_session cookie, so the backend's require_self returned 401 and the onboarding_completed = True write (backend/routes/onboarding.py:42) never ran.

The failure was invisible because the call used await fetch(...) with no res.ok check inside a try/catch that only catches network errors — so the 401 was swallowed and the user was routed to /dashboard as if it succeeded, then bounced back to onboarding on the next load.

Reproduced live: POST https://api.staging.saplinglearn.com/api/onboarding/profile with no cookie → 401 {"detail":"Not authenticated"}.

Fix

Use the existing submitOnboardingProfile() helper from lib/api.ts, which goes through fetchJSON:

  • same-origin relative path (API_URL = '', proxied to the backend via the /api/* rewrite) → the sapling_session cookie is sent
  • credentials: 'include'
  • checks res.ok and throws on failure, so errors surface instead of being hidden

Blast radius

Audited all other direct NEXT_PUBLIC_API_URL usages — this was the only authenticated cross-origin fetch. The rest are non-authed (/api/onboarding/courses search) or top-level OAuth popup navigations (/api/auth/google), and middleware.ts sends the Cookie header explicitly server-side.

Verification

  • npm run typecheck (tsc --noEmit) passes with 0 errors.
  • Affected staging user's onboarding_completed was manually set to True to unblock immediately; this PR fixes the persistence path so it no longer recurs.

Note: main's Frontend CI check is independently red repo-wide (lockfile + eslint baseline), unrelated to this change.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved onboarding completion so profile details are saved more reliably.
    • Reduced issues caused by browser cookie and login-session handling during onboarding.
    • Kept the rest of the onboarding flow unchanged.

…g sticks
handleOnboardingComplete POSTed /api/onboarding/profile to the cross-origin
NEXT_PUBLIC_API_URL without credentials:'include', so the browser dropped the
sapling_session cookie and the backend's require_self returned 401. The
onboarding_completed flag never flipped to True — and because the code never
checked res.ok, the 401 was swallowed and users were routed to /dashboard as if
it worked, then bounced back to "Get Started" on every subsequent sign-in.
Switch to the existing submitOnboardingProfile() helper, which goes through the
same-origin lib/api.ts fetchJSON path (API_URL='' proxied via the /api/* rewrite,
credentials:'include', and an res.ok check that surfaces failures instead of
hiding them).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

In frontend/src/app/page.tsx, the onboarding profile persistence in handleOnboardingComplete is switched from a raw fetch POST to ${API_URL}/api/onboarding/profile to a call to the shared submitOnboardingProfile helper imported from @/lib/api, along with its OnboardingProfilePayload type.

Changes

Onboarding persistence refactor

Layer / File(s)Summary
Replace raw fetch with submitOnboardingProfile
frontend/src/app/page.tsx
Imports submitOnboardingProfile and OnboardingProfilePayload from @/lib/api; replaces the raw fetch POST in handleOnboardingComplete with the helper call, mapping the same form fields and casting learning_style.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~3 minutes

Poem

🐇 No more raw fetch, no cookie woes,
A helper now carries where onboarding goes.
submitOnboardingProfile hops into place,
Clean imports and casts with elegant grace.
The rabbit approves this tidy embrace! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title is specific and accurately summarizes the main change: persisting onboarding completion to stop the Get Started loop.
Description check✅ PassedThe description is detailed and covers the issue, fix, and verification, though it doesn't follow the template's exact section headings.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/onboarding-completion-persistence

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging830555eCommit Preview URL

Branch Preview URL
Jun 30 2026, 01:30 AM

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/app/page.tsx (1)

624-637: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Stop the success flow when onboarding persistence fails.

submitOnboardingProfile now throws on non-OK responses, but this catch logs the error and then falls through to the completion animation and dashboard redirect. If the profile save returns 401/500, onboarding_completed can remain false and recreate the loop this PR is fixing.

🐛 Proposed fix
 } catch (e) {
console.error('Failed to save onboarding profile:', e);
+ return;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/app/page.tsx` around lines 624 - 637, The onboarding save error
is being swallowed in the submit path, allowing the success/redirect flow to
continue even when submitOnboardingProfile fails. Update the handler around
submitOnboardingProfile in page.tsx so that the catch path stops the completion
animation and dashboard redirect, and only advances the success flow when the
profile persistence call succeeds. Keep the existing error logging, but ensure
the branch after the try/catch does not run as if onboarding completed when the
request returns a non-OK response.
🧹 Nitpick comments (1)
frontend/src/app/page.tsx (1)

633-633: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Avoid casting unvalidated learning styles into the API contract.

formData.style is typed as string, so the assertion can send invalid values despite OnboardingProfilePayload restricting learning_style to a fixed union.

♻️ Proposed refactor
+ const learningStyle = formData.style;+ if (+ learningStyle !== 'visual' &&+ learningStyle !== 'reading' &&+ learningStyle !== 'auditory' &&+ learningStyle !== 'hands-on' &&+ learningStyle !== 'mixed'+ ) {+ console.error('Invalid onboarding learning style:', learningStyle);+ return;+ }+
await submitOnboardingProfile({
user_id: userId,
first_name: formData.firstName,
last_name: formData.lastName,
year: formData.year,
majors: formData.majors,
minors: formData.minors,
course_ids: formData.course_ids,
- learning_style: formData.style as OnboardingProfilePayload['learning_style'],+ learning_style: learningStyle,
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/app/page.tsx` at line 633, The onboarding payload is casting
formData.style directly into OnboardingProfilePayload['learning_style'], which
can bypass the union restriction and send invalid values. Update the code in the
page.tsx form submission flow to validate formData.style against the allowed
learning_style options before building the payload, and only assign a value that
is already narrowed to the contract in the onboarding request object.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@frontend/src/app/page.tsx`:
- Around line 624-637: The onboarding save error is being swallowed in the
submit path, allowing the success/redirect flow to continue even when
submitOnboardingProfile fails. Update the handler around submitOnboardingProfile
in page.tsx so that the catch path stops the completion animation and dashboard
redirect, and only advances the success flow when the profile persistence call
succeeds. Keep the existing error logging, but ensure the branch after the
try/catch does not run as if onboarding completed when the request returns a
non-OK response.
---
Nitpick comments:
In `@frontend/src/app/page.tsx`:
- Line 633: The onboarding payload is casting formData.style directly into
OnboardingProfilePayload['learning_style'], which can bypass the union
restriction and send invalid values. Update the code in the page.tsx form
submission flow to validate formData.style against the allowed learning_style
options before building the payload, and only assign a value that is already
narrowed to the contract in the onboarding request object.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 38c0e9e7-6934-4cc9-a544-0dae0aca21f3

📥 Commits

Reviewing files that changed from the base of the PR and between 76fb251 and 830555e.

📒 Files selected for processing (1)
  • frontend/src/app/page.tsx

@AndresL230
AndresL230 merged commit d86edde into mainJun 30, 2026
6 checks passed
@AndresL230
AndresL230 deleted the fix/onboarding-completion-persistence branch August 2, 2026 18:30
Sign up for freeto 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

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(onboarding): persist completion so users aren't trapped in the Get Started loop - #284

Merged
AndresL230 merged 1 commit into
mainfrom
fix/onboarding-completion-persistence
Jun 30, 2026
Merged

fix(onboarding): persist completion so users aren't trapped in the Get Started loop#284
AndresL230 merged 1 commit into
mainfrom
fix/onboarding-completion-persistence

Conversation

@AndresL230

@AndresL230AndresL230 commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Problem

On staging, signing in repeatedly re-shows the "Get Started" / onboarding flow every time, even for an already-onboarded, approved user. Investigation (against the staging DB) showed the user row persists fine — the only thing stuck is the onboarding_completed boolean, which had been False since the account was created despite many sign-ins.

Root cause

page.tsx:handleOnboardingComplete posted to /api/onboarding/profile using the component-local API_URL = process.env.NEXT_PUBLIC_API_URL — a cross-origin subdomain (https://api.staging.saplinglearn.com) — without credentials: 'include'. The browser therefore dropped the sapling_session cookie, so the backend's require_self returned 401 and the onboarding_completed = True write (backend/routes/onboarding.py:42) never ran.

The failure was invisible because the call used await fetch(...) with no res.ok check inside a try/catch that only catches network errors — so the 401 was swallowed and the user was routed to /dashboard as if it succeeded, then bounced back to onboarding on the next load.

Reproduced live: POST https://api.staging.saplinglearn.com/api/onboarding/profile with no cookie → 401 {"detail":"Not authenticated"}.

Fix

Use the existing submitOnboardingProfile() helper from lib/api.ts, which goes through fetchJSON:

  • same-origin relative path (API_URL = '', proxied to the backend via the /api/* rewrite) → the sapling_session cookie is sent
  • credentials: 'include'
  • checks res.ok and throws on failure, so errors surface instead of being hidden

Blast radius

Audited all other direct NEXT_PUBLIC_API_URL usages — this was the only authenticated cross-origin fetch. The rest are non-authed (/api/onboarding/courses search) or top-level OAuth popup navigations (/api/auth/google), and middleware.ts sends the Cookie header explicitly server-side.

Verification

  • npm run typecheck (tsc --noEmit) passes with 0 errors.
  • Affected staging user's onboarding_completed was manually set to True to unblock immediately; this PR fixes the persistence path so it no longer recurs.

Note: main's Frontend CI check is independently red repo-wide (lockfile + eslint baseline), unrelated to this change.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved onboarding completion so profile details are saved more reliably.
    • Reduced issues caused by browser cookie and login-session handling during onboarding.
    • Kept the rest of the onboarding flow unchanged.

…g sticks
handleOnboardingComplete POSTed /api/onboarding/profile to the cross-origin
NEXT_PUBLIC_API_URL without credentials:'include', so the browser dropped the
sapling_session cookie and the backend's require_self returned 401. The
onboarding_completed flag never flipped to True — and because the code never
checked res.ok, the 401 was swallowed and users were routed to /dashboard as if
it worked, then bounced back to "Get Started" on every subsequent sign-in.
Switch to the existing submitOnboardingProfile() helper, which goes through the
same-origin lib/api.ts fetchJSON path (API_URL='' proxied via the /api/* rewrite,
credentials:'include', and an res.ok check that surfaces failures instead of
hiding them).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

In frontend/src/app/page.tsx, the onboarding profile persistence in handleOnboardingComplete is switched from a raw fetch POST to ${API_URL}/api/onboarding/profile to a call to the shared submitOnboardingProfile helper imported from @/lib/api, along with its OnboardingProfilePayload type.

Changes

Onboarding persistence refactor

Layer / File(s)Summary
Replace raw fetch with submitOnboardingProfile
frontend/src/app/page.tsx
Imports submitOnboardingProfile and OnboardingProfilePayload from @/lib/api; replaces the raw fetch POST in handleOnboardingComplete with the helper call, mapping the same form fields and casting learning_style.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~3 minutes

Poem

🐇 No more raw fetch, no cookie woes,
A helper now carries where onboarding goes.
submitOnboardingProfile hops into place,
Clean imports and casts with elegant grace.
The rabbit approves this tidy embrace! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title is specific and accurately summarizes the main change: persisting onboarding completion to stop the Get Started loop.
Description check✅ PassedThe description is detailed and covers the issue, fix, and verification, though it doesn't follow the template's exact section headings.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/onboarding-completion-persistence

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging830555eCommit Preview URL

Branch Preview URL
Jun 30 2026, 01:30 AM

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/app/page.tsx (1)

624-637: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Stop the success flow when onboarding persistence fails.

submitOnboardingProfile now throws on non-OK responses, but this catch logs the error and then falls through to the completion animation and dashboard redirect. If the profile save returns 401/500, onboarding_completed can remain false and recreate the loop this PR is fixing.

🐛 Proposed fix
 } catch (e) {
console.error('Failed to save onboarding profile:', e);
+ return;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/app/page.tsx` around lines 624 - 637, The onboarding save error
is being swallowed in the submit path, allowing the success/redirect flow to
continue even when submitOnboardingProfile fails. Update the handler around
submitOnboardingProfile in page.tsx so that the catch path stops the completion
animation and dashboard redirect, and only advances the success flow when the
profile persistence call succeeds. Keep the existing error logging, but ensure
the branch after the try/catch does not run as if onboarding completed when the
request returns a non-OK response.
🧹 Nitpick comments (1)
frontend/src/app/page.tsx (1)

633-633: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Avoid casting unvalidated learning styles into the API contract.

formData.style is typed as string, so the assertion can send invalid values despite OnboardingProfilePayload restricting learning_style to a fixed union.

♻️ Proposed refactor
+ const learningStyle = formData.style;+ if (+ learningStyle !== 'visual' &&+ learningStyle !== 'reading' &&+ learningStyle !== 'auditory' &&+ learningStyle !== 'hands-on' &&+ learningStyle !== 'mixed'+ ) {+ console.error('Invalid onboarding learning style:', learningStyle);+ return;+ }+
await submitOnboardingProfile({
user_id: userId,
first_name: formData.firstName,
last_name: formData.lastName,
year: formData.year,
majors: formData.majors,
minors: formData.minors,
course_ids: formData.course_ids,
- learning_style: formData.style as OnboardingProfilePayload['learning_style'],+ learning_style: learningStyle,
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/app/page.tsx` at line 633, The onboarding payload is casting
formData.style directly into OnboardingProfilePayload['learning_style'], which
can bypass the union restriction and send invalid values. Update the code in the
page.tsx form submission flow to validate formData.style against the allowed
learning_style options before building the payload, and only assign a value that
is already narrowed to the contract in the onboarding request object.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@frontend/src/app/page.tsx`:
- Around line 624-637: The onboarding save error is being swallowed in the
submit path, allowing the success/redirect flow to continue even when
submitOnboardingProfile fails. Update the handler around submitOnboardingProfile
in page.tsx so that the catch path stops the completion animation and dashboard
redirect, and only advances the success flow when the profile persistence call
succeeds. Keep the existing error logging, but ensure the branch after the
try/catch does not run as if onboarding completed when the request returns a
non-OK response.
---
Nitpick comments:
In `@frontend/src/app/page.tsx`:
- Line 633: The onboarding payload is casting formData.style directly into
OnboardingProfilePayload['learning_style'], which can bypass the union
restriction and send invalid values. Update the code in the page.tsx form
submission flow to validate formData.style against the allowed learning_style
options before building the payload, and only assign a value that is already
narrowed to the contract in the onboarding request object.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 38c0e9e7-6934-4cc9-a544-0dae0aca21f3

📥 Commits

Reviewing files that changed from the base of the PR and between 76fb251 and 830555e.

📒 Files selected for processing (1)
  • frontend/src/app/page.tsx

@AndresL230
AndresL230 merged commit d86edde into mainJun 30, 2026
6 checks passed
@AndresL230
AndresL230 deleted the fix/onboarding-completion-persistence branch August 2, 2026 18:30
Sign up for freeto 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

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(onboarding): persist completion so users aren't trapped in the Get Started loop - #284

Merged
AndresL230 merged 1 commit into
mainfrom
fix/onboarding-completion-persistence
Jun 30, 2026
Merged

fix(onboarding): persist completion so users aren't trapped in the Get Started loop#284
AndresL230 merged 1 commit into
mainfrom
fix/onboarding-completion-persistence

Conversation

@AndresL230

@AndresL230AndresL230 commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Problem

On staging, signing in repeatedly re-shows the "Get Started" / onboarding flow every time, even for an already-onboarded, approved user. Investigation (against the staging DB) showed the user row persists fine — the only thing stuck is the onboarding_completed boolean, which had been False since the account was created despite many sign-ins.

Root cause

page.tsx:handleOnboardingComplete posted to /api/onboarding/profile using the component-local API_URL = process.env.NEXT_PUBLIC_API_URL — a cross-origin subdomain (https://api.staging.saplinglearn.com) — without credentials: 'include'. The browser therefore dropped the sapling_session cookie, so the backend's require_self returned 401 and the onboarding_completed = True write (backend/routes/onboarding.py:42) never ran.

The failure was invisible because the call used await fetch(...) with no res.ok check inside a try/catch that only catches network errors — so the 401 was swallowed and the user was routed to /dashboard as if it succeeded, then bounced back to onboarding on the next load.

Reproduced live: POST https://api.staging.saplinglearn.com/api/onboarding/profile with no cookie → 401 {"detail":"Not authenticated"}.

Fix

Use the existing submitOnboardingProfile() helper from lib/api.ts, which goes through fetchJSON:

  • same-origin relative path (API_URL = '', proxied to the backend via the /api/* rewrite) → the sapling_session cookie is sent
  • credentials: 'include'
  • checks res.ok and throws on failure, so errors surface instead of being hidden

Blast radius

Audited all other direct NEXT_PUBLIC_API_URL usages — this was the only authenticated cross-origin fetch. The rest are non-authed (/api/onboarding/courses search) or top-level OAuth popup navigations (/api/auth/google), and middleware.ts sends the Cookie header explicitly server-side.

Verification

  • npm run typecheck (tsc --noEmit) passes with 0 errors.
  • Affected staging user's onboarding_completed was manually set to True to unblock immediately; this PR fixes the persistence path so it no longer recurs.

Note: main's Frontend CI check is independently red repo-wide (lockfile + eslint baseline), unrelated to this change.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved onboarding completion so profile details are saved more reliably.
    • Reduced issues caused by browser cookie and login-session handling during onboarding.
    • Kept the rest of the onboarding flow unchanged.

…g sticks
handleOnboardingComplete POSTed /api/onboarding/profile to the cross-origin
NEXT_PUBLIC_API_URL without credentials:'include', so the browser dropped the
sapling_session cookie and the backend's require_self returned 401. The
onboarding_completed flag never flipped to True — and because the code never
checked res.ok, the 401 was swallowed and users were routed to /dashboard as if
it worked, then bounced back to "Get Started" on every subsequent sign-in.
Switch to the existing submitOnboardingProfile() helper, which goes through the
same-origin lib/api.ts fetchJSON path (API_URL='' proxied via the /api/* rewrite,
credentials:'include', and an res.ok check that surfaces failures instead of
hiding them).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

In frontend/src/app/page.tsx, the onboarding profile persistence in handleOnboardingComplete is switched from a raw fetch POST to ${API_URL}/api/onboarding/profile to a call to the shared submitOnboardingProfile helper imported from @/lib/api, along with its OnboardingProfilePayload type.

Changes

Onboarding persistence refactor

Layer / File(s)Summary
Replace raw fetch with submitOnboardingProfile
frontend/src/app/page.tsx
Imports submitOnboardingProfile and OnboardingProfilePayload from @/lib/api; replaces the raw fetch POST in handleOnboardingComplete with the helper call, mapping the same form fields and casting learning_style.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~3 minutes

Poem

🐇 No more raw fetch, no cookie woes,
A helper now carries where onboarding goes.
submitOnboardingProfile hops into place,
Clean imports and casts with elegant grace.
The rabbit approves this tidy embrace! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title is specific and accurately summarizes the main change: persisting onboarding completion to stop the Get Started loop.
Description check✅ PassedThe description is detailed and covers the issue, fix, and verification, though it doesn't follow the template's exact section headings.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/onboarding-completion-persistence

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging830555eCommit Preview URL

Branch Preview URL
Jun 30 2026, 01:30 AM

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/app/page.tsx (1)

624-637: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Stop the success flow when onboarding persistence fails.

submitOnboardingProfile now throws on non-OK responses, but this catch logs the error and then falls through to the completion animation and dashboard redirect. If the profile save returns 401/500, onboarding_completed can remain false and recreate the loop this PR is fixing.

🐛 Proposed fix
 } catch (e) {
console.error('Failed to save onboarding profile:', e);
+ return;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/app/page.tsx` around lines 624 - 637, The onboarding save error
is being swallowed in the submit path, allowing the success/redirect flow to
continue even when submitOnboardingProfile fails. Update the handler around
submitOnboardingProfile in page.tsx so that the catch path stops the completion
animation and dashboard redirect, and only advances the success flow when the
profile persistence call succeeds. Keep the existing error logging, but ensure
the branch after the try/catch does not run as if onboarding completed when the
request returns a non-OK response.
🧹 Nitpick comments (1)
frontend/src/app/page.tsx (1)

633-633: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Avoid casting unvalidated learning styles into the API contract.

formData.style is typed as string, so the assertion can send invalid values despite OnboardingProfilePayload restricting learning_style to a fixed union.

♻️ Proposed refactor
+ const learningStyle = formData.style;+ if (+ learningStyle !== 'visual' &&+ learningStyle !== 'reading' &&+ learningStyle !== 'auditory' &&+ learningStyle !== 'hands-on' &&+ learningStyle !== 'mixed'+ ) {+ console.error('Invalid onboarding learning style:', learningStyle);+ return;+ }+
await submitOnboardingProfile({
user_id: userId,
first_name: formData.firstName,
last_name: formData.lastName,
year: formData.year,
majors: formData.majors,
minors: formData.minors,
course_ids: formData.course_ids,
- learning_style: formData.style as OnboardingProfilePayload['learning_style'],+ learning_style: learningStyle,
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/app/page.tsx` at line 633, The onboarding payload is casting
formData.style directly into OnboardingProfilePayload['learning_style'], which
can bypass the union restriction and send invalid values. Update the code in the
page.tsx form submission flow to validate formData.style against the allowed
learning_style options before building the payload, and only assign a value that
is already narrowed to the contract in the onboarding request object.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@frontend/src/app/page.tsx`:
- Around line 624-637: The onboarding save error is being swallowed in the
submit path, allowing the success/redirect flow to continue even when
submitOnboardingProfile fails. Update the handler around submitOnboardingProfile
in page.tsx so that the catch path stops the completion animation and dashboard
redirect, and only advances the success flow when the profile persistence call
succeeds. Keep the existing error logging, but ensure the branch after the
try/catch does not run as if onboarding completed when the request returns a
non-OK response.
---
Nitpick comments:
In `@frontend/src/app/page.tsx`:
- Line 633: The onboarding payload is casting formData.style directly into
OnboardingProfilePayload['learning_style'], which can bypass the union
restriction and send invalid values. Update the code in the page.tsx form
submission flow to validate formData.style against the allowed learning_style
options before building the payload, and only assign a value that is already
narrowed to the contract in the onboarding request object.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 38c0e9e7-6934-4cc9-a544-0dae0aca21f3

📥 Commits

Reviewing files that changed from the base of the PR and between 76fb251 and 830555e.

📒 Files selected for processing (1)
  • frontend/src/app/page.tsx

@AndresL230
AndresL230 merged commit d86edde into mainJun 30, 2026
6 checks passed
@AndresL230
AndresL230 deleted the fix/onboarding-completion-persistence branch August 2, 2026 18:30
Sign up for freeto 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

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(onboarding): persist completion so users aren't trapped in the Get Started loop - #284

Merged
AndresL230 merged 1 commit into
mainfrom
fix/onboarding-completion-persistence
Jun 30, 2026
Merged

fix(onboarding): persist completion so users aren't trapped in the Get Started loop#284
AndresL230 merged 1 commit into
mainfrom
fix/onboarding-completion-persistence

Conversation

@AndresL230

@AndresL230AndresL230 commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Problem

On staging, signing in repeatedly re-shows the "Get Started" / onboarding flow every time, even for an already-onboarded, approved user. Investigation (against the staging DB) showed the user row persists fine — the only thing stuck is the onboarding_completed boolean, which had been False since the account was created despite many sign-ins.

Root cause

page.tsx:handleOnboardingComplete posted to /api/onboarding/profile using the component-local API_URL = process.env.NEXT_PUBLIC_API_URL — a cross-origin subdomain (https://api.staging.saplinglearn.com) — without credentials: 'include'. The browser therefore dropped the sapling_session cookie, so the backend's require_self returned 401 and the onboarding_completed = True write (backend/routes/onboarding.py:42) never ran.

The failure was invisible because the call used await fetch(...) with no res.ok check inside a try/catch that only catches network errors — so the 401 was swallowed and the user was routed to /dashboard as if it succeeded, then bounced back to onboarding on the next load.

Reproduced live: POST https://api.staging.saplinglearn.com/api/onboarding/profile with no cookie → 401 {"detail":"Not authenticated"}.

Fix

Use the existing submitOnboardingProfile() helper from lib/api.ts, which goes through fetchJSON:

  • same-origin relative path (API_URL = '', proxied to the backend via the /api/* rewrite) → the sapling_session cookie is sent
  • credentials: 'include'
  • checks res.ok and throws on failure, so errors surface instead of being hidden

Blast radius

Audited all other direct NEXT_PUBLIC_API_URL usages — this was the only authenticated cross-origin fetch. The rest are non-authed (/api/onboarding/courses search) or top-level OAuth popup navigations (/api/auth/google), and middleware.ts sends the Cookie header explicitly server-side.

Verification

  • npm run typecheck (tsc --noEmit) passes with 0 errors.
  • Affected staging user's onboarding_completed was manually set to True to unblock immediately; this PR fixes the persistence path so it no longer recurs.

Note: main's Frontend CI check is independently red repo-wide (lockfile + eslint baseline), unrelated to this change.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved onboarding completion so profile details are saved more reliably.
    • Reduced issues caused by browser cookie and login-session handling during onboarding.
    • Kept the rest of the onboarding flow unchanged.

…g sticks
handleOnboardingComplete POSTed /api/onboarding/profile to the cross-origin
NEXT_PUBLIC_API_URL without credentials:'include', so the browser dropped the
sapling_session cookie and the backend's require_self returned 401. The
onboarding_completed flag never flipped to True — and because the code never
checked res.ok, the 401 was swallowed and users were routed to /dashboard as if
it worked, then bounced back to "Get Started" on every subsequent sign-in.
Switch to the existing submitOnboardingProfile() helper, which goes through the
same-origin lib/api.ts fetchJSON path (API_URL='' proxied via the /api/* rewrite,
credentials:'include', and an res.ok check that surfaces failures instead of
hiding them).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

In frontend/src/app/page.tsx, the onboarding profile persistence in handleOnboardingComplete is switched from a raw fetch POST to ${API_URL}/api/onboarding/profile to a call to the shared submitOnboardingProfile helper imported from @/lib/api, along with its OnboardingProfilePayload type.

Changes

Onboarding persistence refactor

Layer / File(s)Summary
Replace raw fetch with submitOnboardingProfile
frontend/src/app/page.tsx
Imports submitOnboardingProfile and OnboardingProfilePayload from @/lib/api; replaces the raw fetch POST in handleOnboardingComplete with the helper call, mapping the same form fields and casting learning_style.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~3 minutes

Poem

🐇 No more raw fetch, no cookie woes,
A helper now carries where onboarding goes.
submitOnboardingProfile hops into place,
Clean imports and casts with elegant grace.
The rabbit approves this tidy embrace! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title is specific and accurately summarizes the main change: persisting onboarding completion to stop the Get Started loop.
Description check✅ PassedThe description is detailed and covers the issue, fix, and verification, though it doesn't follow the template's exact section headings.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/onboarding-completion-persistence

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging830555eCommit Preview URL

Branch Preview URL
Jun 30 2026, 01:30 AM

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/app/page.tsx (1)

624-637: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Stop the success flow when onboarding persistence fails.

submitOnboardingProfile now throws on non-OK responses, but this catch logs the error and then falls through to the completion animation and dashboard redirect. If the profile save returns 401/500, onboarding_completed can remain false and recreate the loop this PR is fixing.

🐛 Proposed fix
 } catch (e) {
console.error('Failed to save onboarding profile:', e);
+ return;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/app/page.tsx` around lines 624 - 637, The onboarding save error
is being swallowed in the submit path, allowing the success/redirect flow to
continue even when submitOnboardingProfile fails. Update the handler around
submitOnboardingProfile in page.tsx so that the catch path stops the completion
animation and dashboard redirect, and only advances the success flow when the
profile persistence call succeeds. Keep the existing error logging, but ensure
the branch after the try/catch does not run as if onboarding completed when the
request returns a non-OK response.
🧹 Nitpick comments (1)
frontend/src/app/page.tsx (1)

633-633: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Avoid casting unvalidated learning styles into the API contract.

formData.style is typed as string, so the assertion can send invalid values despite OnboardingProfilePayload restricting learning_style to a fixed union.

♻️ Proposed refactor
+ const learningStyle = formData.style;+ if (+ learningStyle !== 'visual' &&+ learningStyle !== 'reading' &&+ learningStyle !== 'auditory' &&+ learningStyle !== 'hands-on' &&+ learningStyle !== 'mixed'+ ) {+ console.error('Invalid onboarding learning style:', learningStyle);+ return;+ }+
await submitOnboardingProfile({
user_id: userId,
first_name: formData.firstName,
last_name: formData.lastName,
year: formData.year,
majors: formData.majors,
minors: formData.minors,
course_ids: formData.course_ids,
- learning_style: formData.style as OnboardingProfilePayload['learning_style'],+ learning_style: learningStyle,
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/app/page.tsx` at line 633, The onboarding payload is casting
formData.style directly into OnboardingProfilePayload['learning_style'], which
can bypass the union restriction and send invalid values. Update the code in the
page.tsx form submission flow to validate formData.style against the allowed
learning_style options before building the payload, and only assign a value that
is already narrowed to the contract in the onboarding request object.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@frontend/src/app/page.tsx`:
- Around line 624-637: The onboarding save error is being swallowed in the
submit path, allowing the success/redirect flow to continue even when
submitOnboardingProfile fails. Update the handler around submitOnboardingProfile
in page.tsx so that the catch path stops the completion animation and dashboard
redirect, and only advances the success flow when the profile persistence call
succeeds. Keep the existing error logging, but ensure the branch after the
try/catch does not run as if onboarding completed when the request returns a
non-OK response.
---
Nitpick comments:
In `@frontend/src/app/page.tsx`:
- Line 633: The onboarding payload is casting formData.style directly into
OnboardingProfilePayload['learning_style'], which can bypass the union
restriction and send invalid values. Update the code in the page.tsx form
submission flow to validate formData.style against the allowed learning_style
options before building the payload, and only assign a value that is already
narrowed to the contract in the onboarding request object.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 38c0e9e7-6934-4cc9-a544-0dae0aca21f3

📥 Commits

Reviewing files that changed from the base of the PR and between 76fb251 and 830555e.

📒 Files selected for processing (1)
  • frontend/src/app/page.tsx

@AndresL230
AndresL230 merged commit d86edde into mainJun 30, 2026
6 checks passed
@AndresL230
AndresL230 deleted the fix/onboarding-completion-persistence branch August 2, 2026 18:30
Sign up for freeto 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

@AndresL230