diff --git a/cli/src/types/supabase.types.ts b/cli/src/types/supabase.types.ts index 2b26f41cf5..2b1e568410 100644 --- a/cli/src/types/supabase.types.ts +++ b/cli/src/types/supabase.types.ts @@ -335,6 +335,7 @@ export type Database = { rollout_paused_version_names: string[] name: string | null need_onboarding: boolean + onboarding: Json onboarding_completed_at: string | null owner_org: string retention: number @@ -367,6 +368,7 @@ export type Database = { rollout_paused_version_names?: string[] name?: string | null need_onboarding?: boolean + onboarding?: Json onboarding_completed_at?: string | null owner_org: string retention?: number @@ -399,6 +401,7 @@ export type Database = { rollout_paused_version_names?: string[] name?: string | null need_onboarding?: boolean + onboarding?: Json onboarding_completed_at?: string | null owner_org?: string retention?: number @@ -3640,6 +3643,7 @@ export type Database = { manifest_bundle_count: number name: string | null need_onboarding: boolean + onboarding: Json onboarding_completed_at: string | null owner_org: string retention: number @@ -4326,6 +4330,10 @@ export type Database = { Returns: boolean } mark_app_stats_refreshed: { Args: { p_app_id: string }; Returns: string } + mark_onboarding_feature_started: { + Args: { p_app_id: string, p_feature_key: string } + Returns: Json + } mass_edit_queue_messages_cf_ids: { Args: { updates: Database["public"]["CompositeTypes"]["message_update"][] diff --git a/docs/pr-assets/app-onboarding-dashboard.webp b/docs/pr-assets/app-onboarding-dashboard.webp new file mode 100644 index 0000000000..1344752ec1 Binary files /dev/null and b/docs/pr-assets/app-onboarding-dashboard.webp differ diff --git a/docs/pr-assets/getting-started-dashboard.webp b/docs/pr-assets/getting-started-dashboard.webp new file mode 100644 index 0000000000..e7020070ea Binary files /dev/null and b/docs/pr-assets/getting-started-dashboard.webp differ diff --git a/docs/pr-assets/getting-started-page-dark.webp b/docs/pr-assets/getting-started-page-dark.webp new file mode 100644 index 0000000000..ff86414f43 Binary files /dev/null and b/docs/pr-assets/getting-started-page-dark.webp differ diff --git a/docs/pr-assets/getting-started-page.webp b/docs/pr-assets/getting-started-page.webp new file mode 100644 index 0000000000..c6356fb672 Binary files /dev/null and b/docs/pr-assets/getting-started-page.webp differ diff --git a/messages/en.context.json b/messages/en.context.json index e6ef235495..f01ceb8f6b 100644 --- a/messages/en.context.json +++ b/messages/en.context.json @@ -680,7 +680,9 @@ "bundle-not-found-description": "Used in Capgo web console areas: pages/app. Role: helper or description text. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "bundle-number": "Used in Capgo web console areas: components/tables, pages/app. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "bundle-to-production-device": "Used in Capgo web console areas: pages/admin/dashboard. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "bundle-to-store-live": "Used in Capgo web console areas: pages/admin/dashboard. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "bundle-to-subscribed": "Used in Capgo web console areas: pages/admin/dashboard. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "bundle-to-testflight": "Used in Capgo web console areas: pages/admin/dashboard. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "bundle_uploads": "Used in Capgo web console areas: components/dashboard. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "bundles": "Used in Capgo web console areas: components/tables, constants, layouts, stores. Role: short UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "bundles-deleted": "Used in Capgo web console areas: components/tables. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", @@ -1265,6 +1267,13 @@ "disableAutoUpdateToMajor": "Used in Capgo web console areas: pages/app, services, types. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "disabled": "Used in Capgo web console areas: components, components/bundle, components/forms, components/organizations, pages. Role: short UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "disabled-capgo-updates": "Used in Capgo web console areas: components/dashboard. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "distribution-stage": "Used in Capgo web console areas: pages/admin/dashboard. Role: table column label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "distribution-stage-local-only": "Used in Capgo web console areas: pages/admin/dashboard. Role: short UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "distribution-stage-native-unknown": "Used in Capgo web console areas: pages/admin/dashboard. Role: short UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "distribution-stage-no-device": "Used in Capgo web console areas: pages/admin/dashboard. Role: short UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "distribution-stage-play-unknown": "Used in Capgo web console areas: pages/admin/dashboard. Role: short UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "distribution-stage-store-live": "Used in Capgo web console areas: pages/admin/dashboard. Role: short UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "distribution-stage-testflight": "Used in Capgo web console areas: pages/admin/dashboard. Role: short UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "discord": "Used in Capgo web console areas: components. Role: short UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "discord-username": "Used in Capgo web console areas: components/dashboard, pages/settings/account. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "discord-username-help": "Used in Capgo web console areas: components/dashboard, pages/settings/account. Role: helper or description text. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", @@ -1477,6 +1486,21 @@ "generated-new-apikey": "Used in Capgo web console areas: pages. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "get": "Used in Capgo web console areas: components/dashboard, services, types. Role: short UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "get-started": "Used in Capgo web console areas: pages. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "getting-started": "Used in Capgo web console areas: pages/app, components. Role: section or dialog title. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "getting-started-action-explore": "Used in Capgo web console areas: pages/app. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "getting-started-action-setup": "Used in Capgo web console areas: pages/app. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "getting-started-action-validate": "Used in Capgo web console areas: pages/app. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "getting-started-complete": "Used in Capgo web console areas: pages/app. Role: UI sentence. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "getting-started-complete-desc": "Used in Capgo web console areas: pages/app. Role: helper or description text. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "getting-started-count": "Used in Capgo web console areas: pages/app. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "getting-started-description": "Used in Capgo web console areas: pages/app. Role: helper or description text. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "getting-started-dismiss": "Used in Capgo web console areas: components. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "getting-started-done": "Used in Capgo web console areas: pages/app. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "getting-started-essential": "Used in Capgo web console areas: pages/app. Role: section or dialog title. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "getting-started-grow": "Used in Capgo web console areas: pages/app. Role: section or dialog title. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "getting-started-live-update": "Used in Capgo web console areas: pages/app. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "getting-started-live-update-desc": "Used in Capgo web console areas: pages/app. Role: helper or description text. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "getting-started-percent": "Used in Capgo web console areas: pages/app. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "github-stars-trend": "Used in Capgo web console. Role: UI label about \"github stars trend\". Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "github-username": "Used in Capgo web console areas: components/dashboard, composables, pages/settings/account. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "github-username-confirm": "Used in Capgo web console areas: components/dashboard. Role: short UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", @@ -1931,6 +1955,14 @@ "onboarding-invite-option-title": "Used in Capgo web console areas: components/dashboard. Role: section or dialog title. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "onboarding-manual-setup-link": "Used in Capgo web console areas: components/dashboard. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "onboarding-manual-setup-prefix": "Used in Capgo web console areas: components/dashboard. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "onboarding-next-builder": "Used in Capgo web console areas: pages/app. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "onboarding-next-builder-desc": "Used in Capgo web console areas: pages/app. Role: helper or description text. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "onboarding-next-cli-install": "Used in Capgo web console areas: pages/app. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "onboarding-next-cli-install-desc": "Used in Capgo web console areas: pages/app. Role: helper or description text. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "onboarding-next-ota-device": "Used in Capgo web console areas: pages/app. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "onboarding-next-ota-device-desc": "Used in Capgo web console areas: pages/app. Role: helper or description text. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "onboarding-next-ota-store": "Used in Capgo web console areas: pages/app. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "onboarding-next-ota-store-desc": "Used in Capgo web console areas: pages/app. Role: helper or description text. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "onboarding-prerequisites-built": "Used in Capgo web console areas: components/dashboard. Role: UI sentence. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "onboarding-prerequisites-capacitor": "Used in Capgo web console areas: components/dashboard. Role: UI sentence. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "onboarding-prerequisites-cli-desc": "Used in Capgo web console areas: components/dashboard. Role: helper or description text. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", @@ -2285,6 +2317,10 @@ "rbac-system-enabled-body": "Used in Capgo web console. Role: UI sentence about \"rbac system enabled body\". Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "read-the-documentation": "Used in Capgo web console. Role: UI label about \"read the documentation\". Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "read-value": "Used in Capgo web console areas: pages. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "reached-app-store-live": "Used in Capgo web console areas: pages/admin/dashboard. Role: current App Store live count among orgs that uploaded a bundle in 7 days, not a sequential funnel step. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "reached-app-store-live-within-7-days": "Used in Capgo web console areas: pages/admin/dashboard. Role: trend of current App Store live among that signup day's 7-day bundle orgs. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "reached-testflight": "Used in Capgo web console areas: pages/admin/dashboard. Role: current TestFlight-only count among orgs that uploaded a bundle in 7 days, parallel to App Store live. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", + "reached-testflight-within-7-days": "Used in Capgo web console areas: pages/admin/dashboard. Role: trend of current TestFlight-only among that signup day's 7-day bundle orgs. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "reason-optional": "Used in Capgo web console areas: components/dashboard. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "reason-placeholder": "Used in Capgo web console areas: components/dashboard. Role: input placeholder. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", "recent-releases-active": "Used in Capgo web console areas: components/dashboard. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.", diff --git a/messages/en.json b/messages/en.json index d68e741cd8..952b4957d6 100644 --- a/messages/en.json +++ b/messages/en.json @@ -270,11 +270,47 @@ "admin-debug-trigger-toast": "Run replication toast simulation", "activation-telemetry-unavailable": "Activation telemetry is unavailable for this date range. Select a range within the last 3 months.", "bundle-to-production-device": "Bundle → Production device", + "bundle-to-testflight": "Bundle → TestFlight", + "bundle-to-store-live": "Bundle → App Store live", "completed-update-download": "Completed update download", - "completed-update-download-within-7-days": "Completed Update Download (within 7 days)", + "completed-update-download-within-7-days": "Completed update download (within 7 days)", "production-device-to-update-download": "Production device → Update download", "production-plugin-device": "Production plugin device", - "production-plugin-device-within-7-days": "Production Plugin Device (within 7 days)", + "production-plugin-device-within-7-days": "Production plugin device (within 7 days)", + "reached-testflight": "Currently TestFlight", + "reached-testflight-within-7-days": "Currently TestFlight (7-day bundle orgs)", + "reached-app-store-live": "Currently App Store live", + "reached-app-store-live-within-7-days": "Currently App Store live (7-day bundle orgs)", + "distribution-stage": "Distribution", + "distribution-stage-no-device": "No device", + "distribution-stage-local-only": "Local only", + "distribution-stage-native-unknown": "Native unknown", + "distribution-stage-play-unknown": "Play (track unknown)", + "distribution-stage-testflight": "TestFlight", + "distribution-stage-store-live": "App Store live", + "onboarding-next-cli-install": "Install Capgo on a device", + "onboarding-next-cli-install-desc": "Run the CLI install command, then open the app so Capgo can see a real device.", + "onboarding-next-ota-device": "Open the app on a device", + "onboarding-next-ota-device-desc": "Capgo has not seen a TestFlight or store install yet. Launch the app from TestFlight or a store build.", + "onboarding-next-ota-store": "Ship a live store build", + "onboarding-next-ota-store-desc": "Devices are on TestFlight or an unknown store track. A live App Store install is the next proof point.", + "onboarding-next-builder": "Finish your first native build", + "onboarding-next-builder-desc": "A native build was started but has not succeeded yet. Complete it to keep going.", + "getting-started": "Getting started", + "getting-started-description": "Complete these steps to launch live updates for this app.", + "getting-started-count": "{done} of {total}", + "getting-started-percent": "{percent}%", + "getting-started-essential": "Essential setup", + "getting-started-grow": "Grow your app", + "getting-started-done": "Done", + "getting-started-action-setup": "Set up", + "getting-started-action-validate": "Validate", + "getting-started-action-explore": "Explore", + "getting-started-dismiss": "Hide getting started", + "getting-started-complete": "Live Update is set up", + "getting-started-complete-desc": "You can keep using this checklist for production checks and Builder.", + "getting-started-live-update": "Ship a live update", + "getting-started-live-update-desc": "Upload a bundle so devices can receive an over-the-air update.", "all-billing-cycles": "All billing cycles", "all-bundles": "All bundles", "all-plans": "All plans", diff --git a/playwright/e2e/compatibility-events.spec.ts b/playwright/e2e/compatibility-events.spec.ts index 8c30663650..ab9bc94274 100644 --- a/playwright/e2e/compatibility-events.spec.ts +++ b/playwright/e2e/compatibility-events.spec.ts @@ -81,6 +81,50 @@ async function mockCompatibilityEvents(page: Page, rows: () => Record { + if (route.request().method() !== 'GET') { + await route.fallback() + return + } + + const response = await route.fetch() + const contentType = response.headers()['content-type'] ?? '' + if (!contentType.includes('json')) { + await route.fulfill({ response }) + return + } + + const json = await response.json() as unknown + const patchRow = (row: Record) => { + if (row.app_id !== APP_ID) + return row + return { ...row, onboarding: ledger } + } + const body = Array.isArray(json) + ? json.map(row => (row && typeof row === 'object' ? patchRow(row as Record) : row)) + : (json && typeof json === 'object' && 'app_id' in json + ? patchRow(json as Record) + : json) + + const headers = { ...response.headers() } + delete headers['content-length'] + delete headers['content-encoding'] + await route.fulfill({ + status: response.status(), + headers, + body: JSON.stringify(body), + }) + }) +} + async function mockStoreReleaseValidationStatus(page: Page) { await page.route('**/rest/v1/app_versions*', async (route: Route) => { const url = new URL(route.request().url()) @@ -114,34 +158,30 @@ test.describe('Compatibility events', () => { }, TEST_USER_ID) }) - test('shows the store release validation alert before opening the modal', async ({ page }) => { + test('opens store release validation from getting started', async ({ page }) => { await mockCompatibilityEvents(page, () => []) - + await mockGettingStartedLedger(page) await mockStoreReleaseValidationStatus(page) - await page.goto(`/app/${APP_ID}`) + await page.goto(`/app/${APP_ID}/getting-started`) - const alert = page.locator('[data-test="store-release-validation-alert"]') + const storeStep = page.locator('[data-test="getting-started-step-store_release"]') const modal = page.getByRole('dialog') - await expect(alert).toBeVisible() - await expect(alert.getByText('Production release check')).toBeVisible() - await expect(alert.getByText('Live Update bundle detected')).toBeVisible() + await expect(page.locator('[data-test="getting-started-page"]')).toBeVisible() + await expect(page.getByRole('button', { name: /^bundles$/i })).toHaveCount(0) + await expect(storeStep).toBeVisible() + await expect(storeStep.locator('[data-test="getting-started-step-action"]')).toBeVisible() await expect(modal).not.toBeVisible() - await alert.locator('[data-test="store-release-validation-open"]').click() + await storeStep.locator('[data-test="getting-started-step-action"]').click() await expect(modal).toBeVisible() await expect(modal.getByText('Is this app published with Capgo in production?')).toBeVisible() await modal.getByRole('button', { name: 'Later' }).click() await expect(modal).not.toBeVisible() - await expect(alert).toBeVisible() await page.waitForTimeout(1000) await expect(modal).not.toBeVisible() - await alert.locator('[data-test="store-release-validation-dismiss"]').click() - await expect(alert).not.toBeVisible() - await page.reload() - await expect(alert).toBeVisible() - await alert.locator('[data-test="store-release-validation-open"]').click() + await storeStep.locator('[data-test="getting-started-step-action"]').click() await expect(modal).toBeVisible() await modal.getByRole('button', { name: 'Yes, app is published' }).click() @@ -158,11 +198,6 @@ test.describe('Compatibility events', () => { await mockCompatibilityEvents(page, () => [unresolvedEvent()]) await page.goto(`/app/${APP_ID}`) - - const storeReleaseValidationAlert = page.locator('[data-test="store-release-validation-alert"]') - await storeReleaseValidationAlert.waitFor({ state: 'visible', timeout: 5000 }).catch(() => undefined) - if (await storeReleaseValidationAlert.isVisible()) - await storeReleaseValidationAlert.locator('[data-test="store-release-validation-dismiss"]').click() // The banner exposes a stable data-test hook; keep the copy assertions for // content correctness but locate the CTA via its data-test attribute. const banner = page.locator('[data-test="compatibility-banner"]') diff --git a/playwright/visual-diff.config.ts b/playwright/visual-diff.config.ts index e18d58de3c..dbbdebc246 100644 --- a/playwright/visual-diff.config.ts +++ b/playwright/visual-diff.config.ts @@ -18,6 +18,7 @@ export const visualDiffRoutes: VisualDiffRoute[] = [ { slug: 'dashboard', path: '/dashboard', auth: true }, { slug: 'apps', path: '/apps', auth: true }, { slug: 'app-overview', path: '/app/com.demo.app', auth: true }, + { slug: 'app-getting-started', path: '/app/com.demo.app/getting-started', auth: true }, { slug: 'app-settings', path: '/app/com.demo.app/settings', auth: true }, { slug: 'app-settings-access', path: '/app/com.demo.app/settings/access', auth: true }, { slug: 'channels', path: '/app/com.demo.app/channels', auth: true }, diff --git a/read_replicate/schema_replicate.catalog.json b/read_replicate/schema_replicate.catalog.json index f7b40ad9f5..660431ed92 100644 --- a/read_replicate/schema_replicate.catalog.json +++ b/read_replicate/schema_replicate.catalog.json @@ -530,6 +530,16 @@ "table": "apps", "type": "timestamp with time zone" }, + { + "default": "'{}'::jsonb", + "generated": "", + "identity": "", + "name": "onboarding", + "notNull": true, + "position": 31, + "table": "apps", + "type": "jsonb" + }, { "default": "now()", "generated": "", @@ -1770,6 +1780,13 @@ "type": "u", "valid": true }, + { + "definition": "CHECK (jsonb_typeof(onboarding) = 'object'::text AND (NOT onboarding ? 'features'::text OR jsonb_typeof(onboarding -> 'features'::text) = 'object'::text))", + "name": "apps_onboarding_valid", + "table": "apps", + "type": "c", + "valid": true + }, { "definition": "PRIMARY KEY (app_id)", "name": "apps_pkey", @@ -2136,6 +2153,20 @@ "table": "apps", "valid": true }, + { + "constraintOwned": false, + "definition": "CREATE INDEX idx_apps_onboarding_ota_stage ON public.apps USING btree (((((onboarding -> 'features'::text) -> 'ota'::text) ->> 'stage'::text)))", + "name": "idx_apps_onboarding_ota_stage", + "table": "apps", + "valid": true + }, + { + "constraintOwned": false, + "definition": "CREATE INDEX idx_apps_onboarding_refreshed_at ON public.apps USING btree (COALESCE((onboarding ->> 'refreshed_at'::text), ''::text), app_id)", + "name": "idx_apps_onboarding_refreshed_at", + "table": "apps", + "valid": true + }, { "constraintOwned": false, "definition": "CREATE UNIQUE INDEX idx_apps_owner_org_app_id ON public.apps USING btree (owner_org, app_id)", diff --git a/read_replicate/schema_replicate.sql b/read_replicate/schema_replicate.sql index ec28434c82..7b9496dc09 100644 --- a/read_replicate/schema_replicate.sql +++ b/read_replicate/schema_replicate.sql @@ -104,7 +104,9 @@ CREATE TABLE public.apps ( rollout_paused_version_names character varying[] DEFAULT '{}'::character varying[] NOT NULL, created_from_onboarding boolean DEFAULT false NOT NULL, onboarding_completed_at timestamp with time zone, - CONSTRAINT apps_build_timeout_seconds_check CHECK (((build_timeout_seconds >= 300) AND (build_timeout_seconds <= 21600))) + onboarding jsonb DEFAULT '{}'::jsonb NOT NULL, + CONSTRAINT apps_build_timeout_seconds_check CHECK (((build_timeout_seconds >= 300) AND (build_timeout_seconds <= 21600))), + CONSTRAINT apps_onboarding_valid CHECK (((jsonb_typeof(onboarding) = 'object'::text) AND ((NOT (onboarding ? 'features'::text)) OR (jsonb_typeof((onboarding -> 'features'::text)) = 'object'::text)))) ); ALTER TABLE ONLY public.apps REPLICA IDENTITY FULL; @@ -824,6 +826,20 @@ CREATE INDEX idx_apps_created_at ON public.apps USING btree (created_at); CREATE INDEX idx_apps_default_upload_channel ON public.apps USING btree (default_upload_channel); +-- +-- Name: idx_apps_onboarding_ota_stage; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_apps_onboarding_ota_stage ON public.apps USING btree (((((onboarding -> 'features'::text) -> 'ota'::text) ->> 'stage'::text))); + + +-- +-- Name: idx_apps_onboarding_refreshed_at; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_apps_onboarding_refreshed_at ON public.apps USING btree (COALESCE((onboarding ->> 'refreshed_at'::text), ''::text), app_id); + + -- -- Name: idx_apps_owner_org_app_id; Type: INDEX; Schema: public; Owner: - -- diff --git a/src/components.d.ts b/src/components.d.ts index 6489a232e4..04f83178b7 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -26,6 +26,7 @@ declare module 'vue' { AppNotFoundModal: typeof import('./components/AppNotFoundModal.vue')['default'] AppOnboardingFlow: typeof import('./components/dashboard/AppOnboardingFlow.vue')['default'] AppOnboardingIconInput: typeof import('./components/dashboard/AppOnboardingIconInput.vue')['default'] + AppOnboardingNextStep: typeof import('./components/dashboard/AppOnboardingNextStep.vue')['default'] AppPageFrame: typeof import('./components/dashboard/AppPageFrame.vue')['default'] AppPageNotFound: typeof import('./components/dashboard/AppPageNotFound.vue')['default'] AppSetting: typeof import('./components/dashboard/AppSetting.vue')['default'] @@ -75,6 +76,7 @@ declare module 'vue' { DropdownProfile: typeof import('./components/dashboard/DropdownProfile.vue')['default'] FailedCard: typeof import('./components/FailedCard.vue')['default'] FilterModal: typeof import('./components/FilterModal.vue')['default'] + GettingStartedNav: typeof import('./components/dashboard/GettingStartedNav.vue')['default'] GitHubProfileDialog: typeof import('./components/dashboard/GitHubProfileDialog.vue')['default'] GroupsRbacManager: typeof import('./components/organization/GroupsRbacManager.vue')['default'] HistoryTable: typeof import('./components/tables/HistoryTable.vue')['default'] @@ -98,6 +100,7 @@ declare module 'vue' { RbacPermissionOnlyModal: typeof import('./components/RbacPermissionOnlyModal.vue')['default'] ReleaseBanner: typeof import('./components/dashboard/ReleaseBanner.vue')['default'] ReleaseStatusCard: typeof import('./components/dashboard/ReleaseStatusCard.vue')['default'] + RoleCapabilitiesHint: typeof import('./components/forms/RoleCapabilitiesHint.vue')['default'] RoleSelect: typeof import('./components/forms/RoleSelect.vue')['default'] RoleSelectionModal: typeof import('./components/modals/RoleSelectionModal.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] diff --git a/src/components/Sidebar.vue b/src/components/Sidebar.vue index 62b36def8e..a8419b54fa 100644 --- a/src/components/Sidebar.vue +++ b/src/components/Sidebar.vue @@ -21,6 +21,7 @@ import { shouldConfirmOnboardingDashboardExploration, } from '~/utils/onboardingRedirect' import DropdownProfile from '../components/dashboard/DropdownProfile.vue' +import GettingStartedNav from '../components/dashboard/GettingStartedNav.vue' const props = defineProps <{ sidebarOpen: boolean @@ -194,6 +195,8 @@ const tabs = computed(() => { + +
diff --git a/src/components/dashboard/AppOnboardingFlow.vue b/src/components/dashboard/AppOnboardingFlow.vue index e88f8966b7..797f90d059 100644 --- a/src/components/dashboard/AppOnboardingFlow.vue +++ b/src/components/dashboard/AppOnboardingFlow.vue @@ -130,6 +130,45 @@ const planNameOrder = ['Solo', 'Maker', 'Team', 'Enterprise'] as const const localCommand = isLocal(config.supaHost) ? ` --supa-host ${config.supaHost} --supa-anon ${config.supaKey}` : '' const usesBuilderSetupCommand = computed(() => selectedIntent.value === 'builder') +const markedOnboardingFeatures = new Set() + +async function markOnboardingFeatureStarted(featureKey: 'cli_install' | 'ota' | 'builder') { + const appId = createdApp.value?.app_id + if (!appId) + return + + const markKey = `${appId}:${featureKey}` + if (markedOnboardingFeatures.has(markKey)) + return + + for (let attempt = 0; attempt < 2; attempt++) { + markedOnboardingFeatures.add(markKey) + const { data, error } = await supabase.rpc('mark_onboarding_feature_started', { + p_app_id: appId, + p_feature_key: featureKey, + }) + if (!error) { + organizationStore.updateAppOnboarding(appId, data) + return + } + + markedOnboardingFeatures.delete(markKey) + if (attempt === 0) { + await new Promise(resolve => setTimeout(resolve, 400)) + continue + } + console.error('Failed to mark onboarding feature started', error) + } +} + +watch([flowStep, createdApp, usesBuilderSetupCommand], () => { + if (!createdApp.value) + return + if (flowStep.value === 'install' || (flowStep.value === 'setup' && !usesBuilderSetupCommand.value)) + void markOnboardingFeatureStarted('cli_install') + if (flowStep.value === 'setup') + void markOnboardingFeatureStarted(usesBuilderSetupCommand.value ? 'builder' : 'ota') +}) const cliSubcommand = computed(() => usesBuilderSetupCommand.value ? 'build init' : 'i') const cliCommand = computed(() => { const key = apiKey.value @@ -977,7 +1016,7 @@ async function seedDemoData() { name: createdApp.value.name ?? null, ownerOrgId: currentOrg.value.gid, }) - router.push(`/app/${encodeURIComponent(createdApp.value.app_id)}?refresh=true`) + router.push(`/app/${encodeURIComponent(createdApp.value.app_id)}/getting-started`) } catch (error) { console.error('Cannot seed demo data', error) @@ -1086,7 +1125,7 @@ function openDashboard() { }) } allowOnboardingDashboardExploration(onboardingUserId.value, createdApp.value.app_id) - router.push(`/app/${encodeURIComponent(createdApp.value.app_id)}`) + router.push(`/app/${encodeURIComponent(createdApp.value.app_id)}/getting-started`) } onMounted(async () => { diff --git a/src/components/dashboard/GettingStartedNav.vue b/src/components/dashboard/GettingStartedNav.vue new file mode 100644 index 0000000000..fc3cdcbb06 --- /dev/null +++ b/src/components/dashboard/GettingStartedNav.vue @@ -0,0 +1,135 @@ + + + diff --git a/src/components/dashboard/StoreReleaseValidationModal.vue b/src/components/dashboard/StoreReleaseValidationModal.vue index decb441fc9..083eab7e1c 100644 --- a/src/components/dashboard/StoreReleaseValidationModal.vue +++ b/src/components/dashboard/StoreReleaseValidationModal.vue @@ -363,16 +363,15 @@ function closeModal() { setupSuccess.value = '' } -function dismissPrompt() { - closeModal() - hasDismissedPrompt.value = true -} - function openModal() { isOpen.value = true void queueReminder() } +defineExpose({ + openModal, +}) + function confirmPublished() { hasConfirmedPublished.value = true void loadChannels() @@ -514,65 +513,9 @@ onUnmounted(() => {