feat(frontend): app-first onboarding flow#2552
Conversation
New users configure their app first, persist draft data in session storage, then create an organization with a suggested name from the app before the app record is created and onboarding continues. Co-authored-by: Cursor <cursoragent@cursor.com>
📝 WalkthroughWalkthroughIntroduces an app-first onboarding sequence where new users configure their app at ChangesApp-first onboarding flow
Sequence DiagramsequenceDiagram
participant User
participant AuthGuard as auth.ts guard
participant AppPage as /onboarding/app
participant AppFlow as AppOnboardingFlow (preOrg)
participant DraftStorage as sessionStorage
participant OrgPage as /onboarding/organization
participant CreateService as createOnboardingAppFromDraft
participant Supabase as Supabase API
User->>AuthGuard: navigate (no organization)
AuthGuard->>AppPage: redirect to /onboarding/app
AppPage->>AppFlow: render with preOrg=true
AppFlow->>DraftStorage: loadOnboardingAppDraft(userId)
User->>AppFlow: select intent, choose existing-no, fill app name
AppFlow->>DraftStorage: saveOnboardingAppDraft(draft, userId)
AppFlow->>OrgPage: navigate to /onboarding/organization
OrgPage->>DraftStorage: loadOnboardingAppDraft(userId)
DraftStorage-->>OrgPage: OnboardingAppDraft
OrgPage->>User: show app preview, org form, app-name mode option
User->>OrgPage: fill org details, click finish
OrgPage->>CreateService: createOnboardingAppFromDraft(draft, orgId, orgName)
CreateService->>Supabase: POST /functions/app (with candidate app_ids)
CreateService->>Supabase: upload icon to Storage org/<orgId>/<appId>/icon
CreateService->>Supabase: update apps.icon_url
CreateService-->>OrgPage: {app, appIdFeedback}
OrgPage->>DraftStorage: clearOnboardingAppDraft(userId)
OrgPage->>User: navigate to /app/new?resume&step=choice
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
Comment |
Merging this PR will not alter performance
Comparing Footnotes
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cbe34212b0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Fix pre-org onboarding loading, add missing data-test hooks, improve the local capture script auth/worktree handling, and attach screenshots plus an animated WebP walkthrough. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3ade240f10
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Update auth guard tests for /onboarding/app redirects, fix SonarCloud reliability issues in AppOnboardingFlow, and share slugify helpers to reduce new-code duplication. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9818138d6a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Remove dead ternary and redundant icon assignment, and reuse shared slugify helper to clear reliability and duplication findings. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 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.
Inline comments:
In `@docs/pr/app-first-onboarding/README.md`:
- Around line 5-9: The command examples in this documentation section use bun
directly (bun run supabase:start, bun run supabase:db:reset, bun backend) but
should follow the npx convention as per coding guidelines for front-facing
instructions. Replace all direct bun command invocations with their npx-wrapped
equivalents throughout the affected sections. This same formatting issue applies
to multiple locations in the file beyond the initial lines shown, so ensure
consistency across all command examples in the documentation.
In `@playwright/e2e/register.spec.ts`:
- Line 53: The waitForURL call with the regex pattern is too strict about query
parameter ordering, assuming resume appears before step which can cause flaky
test failures. Modify the regex pattern to match the URL regardless of query
parameter order, either by creating a pattern that matches both possible
orderings (resume then step, or step then resume) or by using a more flexible
matching approach that validates both query parameters are present without
depending on their sequence.
In `@scripts/capture-app-first-onboarding-media.mjs`:
- Around line 114-119: The code currently uses videos[0] from the readdirSync
result to select the input video file, but readdirSync does not guarantee any
particular order, so this could convert stale media instead of the latest
recording. Sort the videos array by file modification time in descending order
before selecting the first element, ensuring that the newest recording is
deterministically picked for conversion. Check the file stats using statSync for
each video in the outDir and sort based on mtimeMs to ensure the most recently
created video is used as the input for the conversion.
In `@src/components/dashboard/AppOnboardingFlow.vue`:
- Around line 549-553: The router.push call in the handleContinue method uses a
fixed path string that loses existing query parameters from the current route.
Modify the navigation to preserve query context by using an object format for
router.push that includes the target path along with the current route's query
parameters, ensuring the `to` parameter and other navigation context are
maintained when moving to the organization onboarding step.
In `@src/modules/auth.ts`:
- Around line 389-391: The onboarding redirect call in the condition checking
organizationsLoaded and !organizationStore.hasOrganizations is not preserving
the original destination URL that the user was trying to access. Modify the
next('/onboarding/app') call to include the to.fullPath as a query parameter in
the redirect, ensuring users are returned to their intended destination after
completing onboarding, consistent with how the redirect is handled in earlier
branches of this same navigation guard.
In `@src/services/onboardingAppCreate.ts`:
- Around line 69-115: The uploadIconFromDraft function currently allows
unhandled errors from icon decoding, fetching, or uploading to bubble up and
fail app creation, causing duplicate app IDs on user retries. Wrap the entire
body of the uploadIconFromDraft function in a try-catch block to ensure any
errors during icon processing are caught and logged without interrupting the app
creation flow. In the catch block, log the error with console.error and return
early so the function fails silently, making icon upload a best-effort operation
that does not block app creation success.
In `@src/utils/onboardingAppDraft.ts`:
- Around line 39-43: The optional string fields iosStoreUrl, androidStoreUrl,
iconDataUrl, storeIconDataUrl, and storeScreenshotUrl in the deserialization
logic lack runtime type validation. Add type checks to ensure each of these
fields from the parsed object is either a string or null/undefined before
assigning them. This prevents corrupted sessionStorage values from propagating
through the onboarding flow and causing crashes during string operations later.
Validate the type of each field explicitly, either through individual checks or
a helper function, to guarantee only valid string or null values are assigned.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: e9c9d1d1-4433-45cc-be2e-03f901df1774
⛔ Files ignored due to path filters (4)
docs/pr/app-first-onboarding/01-app-onboarding.pngis excluded by!**/*.pngdocs/pr/app-first-onboarding/02-app-details-filled.pngis excluded by!**/*.pngdocs/pr/app-first-onboarding/03-organization-onboarding.pngis excluded by!**/*.pngdocs/pr/app-first-onboarding/04-organization-app-name-mode.pngis excluded by!**/*.png
📒 Files selected for processing (14)
docs/pr/app-first-onboarding/README.mddocs/pr/app-first-onboarding/app-first-onboarding.webpmessages/en.jsonplaywright/e2e/register.spec.tsscripts/capture-app-first-onboarding-media.mjssrc/components/dashboard/AppOnboardingFlow.vuesrc/modules/auth.tssrc/pages/onboarding/app.vuesrc/pages/onboarding/organization.vuesrc/route-map.d.tssrc/services/onboardingAppCreate.tssrc/utils/onboardingAppDraft.tssrc/utils/onboardingSlug.tstests/auth-sso-provisioning.unit.test.ts
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
Cap-go/capacitor-updater(manual)
Unify onboarding on one page with review fixes: native compliance for /onboarding/app, user-scoped drafts, app ID validation, images-bucket icon upload, auth redirect query preservation, and safer media capture. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 41cab50313
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const cliCommand = computed(() => `npx @capgo/cli@latest ${cliSubcommand.value} ${apiKey.value ?? '[APIKEY]'}${localCommand}`) | ||
| const redactedCliCommand = computed(() => `npx @capgo/cli@latest ${cliSubcommand.value} [YOUR_CAPGO_API_KEY]${localCommand}`) |
There was a problem hiding this comment.
Pass the API key with --apikey for build init
When the user selects the Builder intent, this generates npx @capgo/cli@latest build init <key>, but I checked cli/src/index.ts and build init is declared as .command('init') with .option('-a, --apikey <apikey>') and no positional API-key argument. In that Builder onboarding path the copied command and AI prompt therefore do not populate options.apikey (and may be rejected as an extra argument), so the setup will not link the user's Capgo account; emit build init -a <key> for this subcommand.
Useful? React with 👍 / 👎.
|
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 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.
Inline comments:
In `@src/components/dashboard/AppOnboardingFlow.vue`:
- Line 37: The import statements in AppOnboardingFlow.vue do not follow the
required alphabetical ordering as flagged by the linter. Fix the import order by
ensuring the Lucide icon imports are sorted alphabetically among themselves, and
move the `~/utils/appId` import (which contains `isValidAppId`) to appear before
the `~/utils/onboardingAppDraft` import. Run `bun lint:fix` to automatically
resolve these import ordering violations before validation.
- Around line 191-204: The linter requires additional newline spacing for
consistency with project code style in multiple locations within the
AppOnboardingFlow.vue file. Add blank lines to separate logical sections: after
the planStops computed property definition (around the closing brace before the
return statement), between the userCountStops and selectedUserCountStop computed
properties, and between other computed properties like canShowOrgDetails and
canCreatePreOrgOrganization. You can fix this automatically by running bun
lint:fix, or manually add the required newlines to match the project's
formatting standards for computed property separation.
- Line 196: The return statement in AppOnboardingFlow.vue uses loose equality
operator `==` when comparing planStops.length with planNameOrder.length, which
can lead to unexpected type coercion. Replace the `==` operator with `===`
(strict equality) in the ternary expression that returns either planStops or
fallbackUserCountStops to ensure type-safe comparison.
In `@src/services/onboardingAppCreate.ts`:
- Around line 83-89: In the base64 binary conversion logic where Uint8Array is
created from the binary string returned by atob(), replace the charCodeAt(0)
method call within the mapping function with charCodeAt(0) instead. The
charCodeAt(0) API is semantically correct for extracting byte values from
base64-decoded strings, as it directly returns the byte value without the
possibility of returning undefined, eliminating the need for the ?? 0 fallback
operator.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 80b1d9b2-0919-47fe-831b-e9a56bed874c
⛔ Files ignored due to path filters (4)
docs/pr/app-first-onboarding/01-intent-step.pngis excluded by!**/*.pngdocs/pr/app-first-onboarding/02-app-details-filled.pngis excluded by!**/*.pngdocs/pr/app-first-onboarding/03-organization-step.pngis excluded by!**/*.pngdocs/pr/app-first-onboarding/04-setup-command.pngis excluded by!**/*.png
📒 Files selected for processing (13)
docs/pr/app-first-onboarding/README.mddocs/pr/app-first-onboarding/app-first-onboarding.webpmessages/en.jsonplaywright/e2e/register.spec.tsscripts/capture-app-first-onboarding-media.mjssrc/components/dashboard/AppOnboardingFlow.vuesrc/modules/auth.tssrc/pages/onboarding/app.vuesrc/pages/onboarding/organization.vuesrc/services/nativeCompliance.tssrc/services/onboardingAppCreate.tssrc/utils/appId.tssrc/utils/onboardingAppDraft.ts
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
Cap-go/capacitor-updater(manual)
| clearOnboardingAppDraft, | ||
| loadOnboardingAppDraft, | ||
| } from '~/utils/onboardingAppDraft' | ||
| import { isValidAppId } from '~/utils/appId' |
There was a problem hiding this comment.
Fix import order as flagged by linter.
The linter indicates import order violations that should be resolved before commit.
🧹 Run lint:fix to resolve
As per coding guidelines, run bun lint:fix before validation to automatically fix import ordering and other style issues.
Expected order:
- Line 21: Lucide icon imports should be sorted alphabetically
- Line 37:
~/utils/appIdshould come before~/utils/onboardingAppDraft
Also applies to: 21-25
🧰 Tools
🪛 GitHub Check: Run tests
[failure] 37-37:
Expected "/utils/appId" to come before "/utils/onboardingAppDraft"
🤖 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 `@src/components/dashboard/AppOnboardingFlow.vue` at line 37, The import
statements in AppOnboardingFlow.vue do not follow the required alphabetical
ordering as flagged by the linter. Fix the import order by ensuring the Lucide
icon imports are sorted alphabetically among themselves, and move the
`~/utils/appId` import (which contains `isValidAppId`) to appear before the
`~/utils/onboardingAppDraft` import. Run `bun lint:fix` to automatically resolve
these import ordering violations before validation.
Source: Linters/SAST tools
| if (!plan?.mau) return [] | ||
| const mau = Number(plan.mau) | ||
| if (!Number.isFinite(mau) || mau <= 0) return [] | ||
| return [{ value: mau, label: formatUserCount(mau, plan.name === 'Enterprise'), planName: plan.name }] | ||
| }) | ||
| return planStops.length == planNameOrder.length ? planStops : fallbackUserCountStops | ||
| }) | ||
| const selectedUserCountStop = computed<UserCountStop | null>(() => estimatedUsersIndex.value === null ? null : userCountStops.value[Math.min(estimatedUsersIndex.value, userCountStops.value.length - 1)] ?? null) | ||
| const canShowOrgDetails = computed(() => orgMode.value !== null) | ||
| const canCreatePreOrgOrganization = computed(() => { | ||
| if (!orgMode.value || !orgNameInput.value.trim()) return false | ||
| if (existingApp.value === true) return selectedUserCountStop.value !== null | ||
| return true | ||
| }) |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | 💤 Low value
Add missing newlines per linter requirements.
The linter flagged multiple locations needing newline spacing for consistency with the project's code style.
Run bun lint:fix to automatically apply the required newline formatting throughout the changed sections.
Also applies to: 223-232
🧰 Tools
🪛 GitHub Check: Run tests
[failure] 202-202:
Expect newline after if
[failure] 201-201:
Expect newline after if
[failure] 196-196:
Expected '===' and instead saw '=='
[failure] 193-193:
Expect newline after if
[failure] 191-191:
Expect newline after if
🤖 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 `@src/components/dashboard/AppOnboardingFlow.vue` around lines 191 - 204, The
linter requires additional newline spacing for consistency with project code
style in multiple locations within the AppOnboardingFlow.vue file. Add blank
lines to separate logical sections: after the planStops computed property
definition (around the closing brace before the return statement), between the
userCountStops and selectedUserCountStop computed properties, and between other
computed properties like canShowOrgDetails and canCreatePreOrgOrganization. You
can fix this automatically by running bun lint:fix, or manually add the required
newlines to match the project's formatting standards for computed property
separation.
Source: Linters/SAST tools
| if (!Number.isFinite(mau) || mau <= 0) return [] | ||
| return [{ value: mau, label: formatUserCount(mau, plan.name === 'Enterprise'), planName: plan.name }] | ||
| }) | ||
| return planStops.length == planNameOrder.length ? planStops : fallbackUserCountStops |
There was a problem hiding this comment.
Replace loose equality with strict equality.
Line 196 uses == instead of ===, which can cause unexpected type coercion bugs.
🔧 Suggested fix
- return planStops.length == planNameOrder.length ? planStops : fallbackUserCountStops
+ return planStops.length === planNameOrder.length ? planStops : fallbackUserCountStops📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| return planStops.length == planNameOrder.length ? planStops : fallbackUserCountStops | |
| return planStops.length === planNameOrder.length ? planStops : fallbackUserCountStops |
🧰 Tools
🪛 GitHub Check: Run tests
[failure] 196-196:
Expected '===' and instead saw '=='
🤖 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 `@src/components/dashboard/AppOnboardingFlow.vue` at line 196, The return
statement in AppOnboardingFlow.vue uses loose equality operator `==` when
comparing planStops.length with planNameOrder.length, which can lead to
unexpected type coercion. Replace the `==` operator with `===` (strict equality)
in the ternary expression that returns either planStops or
fallbackUserCountStops to ensure type-safe comparison.
Source: Linters/SAST tools
| const contentType = /^data:([^;]+)/.exec(header)?.[1] ?? 'image/png' | ||
| if (!payload) | ||
| return | ||
|
|
||
| const binary = atob(payload) | ||
| const bytes = Uint8Array.from(binary, char => char.codePointAt(0) ?? 0) | ||
| blob = new Blob([bytes], { type: contentType }) |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | ⚡ Quick win
Use charCodeAt(0) instead of codePointAt(0) for base64 binary conversion.
The atob() function returns a binary string where each character represents a byte (0–255). To extract byte values, charCodeAt(0) is the semantically correct API—it always returns a number and directly maps to the character's byte value. codePointAt(0) is intended for Unicode code points and can return undefined, requiring the ?? 0 fallback.
🔧 Proposed fix
- const bytes = Uint8Array.from(binary, char => char.codePointAt(0) ?? 0)
+ const bytes = Uint8Array.from(binary, char => char.charCodeAt(0))📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const contentType = /^data:([^;]+)/.exec(header)?.[1] ?? 'image/png' | |
| if (!payload) | |
| return | |
| const binary = atob(payload) | |
| const bytes = Uint8Array.from(binary, char => char.codePointAt(0) ?? 0) | |
| blob = new Blob([bytes], { type: contentType }) | |
| const contentType = /^data:([^;]+)/.exec(header)?.[1] ?? 'image/png' | |
| if (!payload) | |
| return | |
| const binary = atob(payload) | |
| const bytes = Uint8Array.from(binary, char => char.charCodeAt(0)) | |
| blob = new Blob([bytes], { type: contentType }) |
🧰 Tools
🪛 OpenGrep (1.22.0)
[ERROR] 83-83: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.
(coderabbit.command-injection.exec-js)
🤖 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 `@src/services/onboardingAppCreate.ts` around lines 83 - 89, In the base64
binary conversion logic where Uint8Array is created from the binary string
returned by atob(), replace the charCodeAt(0) method call within the mapping
function with charCodeAt(0) instead. The charCodeAt(0) API is semantically
correct for extracting byte values from base64-decoded strings, as it directly
returns the byte value without the possibility of returning undefined,
eliminating the need for the ?? 0 fallback operator.


Summary (AI generated)
/onboarding/appinstead of/onboarding/organization.Motivation (AI generated)
Creating an organization before the app felt backwards for new Capgo users. Most people arrive knowing which app they want to set up, not what their org should be called. This reorder keeps momentum and uses the app name as a sensible default for the org.
Business Impact (AI generated)
Flow (AI generated)
Media (AI generated)
Docker was not available in the capture environment, so screenshots and WebP were not attached here.
Regenerate locally with:
Then in another terminal:
See
docs/pr/app-first-onboarding/README.md.Test Plan (AI generated)
/onboarding/appsource=org-switcher) still works without app draftbun test:frontregister specGenerated with AI
Made with Cursor
Summary by CodeRabbit
Release Notes
New Features
Bug Fixes
Documentation
Tests