diff --git a/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client-parts/project-onboarding-wizard.test.tsx b/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client-parts/project-onboarding-wizard.test.tsx index d655fc664..a0375c954 100644 --- a/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client-parts/project-onboarding-wizard.test.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client-parts/project-onboarding-wizard.test.tsx @@ -5,6 +5,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; const mockUpdateConfig = vi.hoisted(() => vi.fn(async () => true)); +const mockPublicEnvVars = vi.hoisted(() => new Map()); vi.mock("@/components/design-components", () => ({ DesignCard: ({ children }: { children: ReactNode }) =>
{children}
, @@ -83,7 +84,7 @@ vi.mock("@/components/ui", () => ({ })); vi.mock("@/lib/env", () => ({ - getPublicEnvVar: () => "false", + getPublicEnvVar: (key: string) => mockPublicEnvVars.get(key) ?? "false", })); vi.mock("@/components/config-update", () => ({ @@ -152,6 +153,7 @@ import { ALL_APPS, getParentAppId, type AppId } from "@hexclave/shared/dist/apps afterEach(() => { cleanup(); mockUpdateConfig.mockClear(); + mockPublicEnvVars.clear(); }); function createDeferred() { @@ -1021,6 +1023,95 @@ describe("ProjectOnboardingWizard", () => { }); }); + it("waits for Get Started before applying RDE onboarding config", async () => { + mockPublicEnvVars.set("NEXT_PUBLIC_STACK_IS_REMOTE_DEVELOPMENT_ENVIRONMENT", "true"); + const saveOnboardingProgress = vi.fn(async () => {}); + const onComplete = vi.fn(); + const getPushedConfigSource = vi.fn(async () => ({ type: "unlinked" })); + const app = { + setupPayments: vi.fn(async () => ({ url: "https://example.com" })), + listEmailThemes: vi.fn(async () => []), + getStripeAccountInfo: vi.fn(async () => null), + useEmailThemes: () => [], + useStripeAccountInfo: () => null, + }; + + render( + ({ + apps: { + installed: { + authentication: { enabled: true }, + emails: { enabled: true }, + payments: { enabled: false }, + }, + }, + domains: { + trustedDomains: {}, + }, + emails: { + selectedThemeId: "default", + server: {}, + }, + }), + app, + getPushedConfigSource, + } as never} + status="welcome" + onboardingState={{ + selected_config_choice: "create-new", + selected_apps: ["authentication", "emails", "analytics"], + selected_sign_in_methods: ["credential", "google"], + selected_email_theme_id: "default", + selected_payments_country: "US", + }} + mode={null} + setMode={vi.fn()} + saveOnboardingProgress={saveOnboardingProgress} + onComplete={onComplete} + />, + ); + + await Promise.resolve(); + + expect(getPushedConfigSource).not.toHaveBeenCalled(); + expect(mockUpdateConfig).not.toHaveBeenCalled(); + + fireEvent.click(screen.getByRole("button", { name: "Get Started" })); + + await waitFor(() => { + expect(mockUpdateConfig).toHaveBeenCalledTimes(1); + expect(mockUpdateConfig).toHaveBeenCalledWith({ + adminApp: app, + configUpdate: { + "auth.password.allowSignIn": true, + "emails.selectedThemeId": "default", + "apps.installed.authentication.enabled": true, + "apps.installed.emails.enabled": true, + "apps.installed.analytics.enabled": true, + "auth.oauth.providers.google": { + type: "google", + allowSignIn: true, + allowConnectedAccounts: true, + }, + "auth.oauth.providers.github": null, + "auth.oauth.providers.microsoft": null, + }, + pushable: true, + }); + expect(saveOnboardingProgress).toHaveBeenCalledWith({ status: "completed", onboardingState: null }); + expect(onComplete).toHaveBeenCalled(); + }); + }); + it("waits for the in-flight welcome config save before marking onboarding completed", async () => { const saveOnboardingProgress = vi.fn(async () => {}); const onComplete = vi.fn(); diff --git a/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client-parts/project-onboarding-wizard.tsx b/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client-parts/project-onboarding-wizard.tsx index d54572af5..a073ee00a 100644 --- a/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client-parts/project-onboarding-wizard.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client-parts/project-onboarding-wizard.tsx @@ -330,25 +330,17 @@ export function ProjectOnboardingWizard(props: { configUpdate[`apps.installed.${appId}.enabled`] = true; } } - if (isLocalEmulator) { - configUpdate["auth.oauth.providers.google"] = signInMethods.has("google") ? { - type: "google", - allowSignIn: true, - allowConnectedAccounts: true, - } : null; - configUpdate["auth.oauth.providers.github"] = signInMethods.has("github") ? { - type: "github", - allowSignIn: true, - allowConnectedAccounts: true, - } : null; - configUpdate["auth.oauth.providers.microsoft"] = signInMethods.has("microsoft") ? { - type: "microsoft", - allowSignIn: true, - allowConnectedAccounts: true, - } : null; + if (isDevelopmentEnvironment) { + for (const providerId of SHARED_OAUTH_SIGN_IN_METHODS) { + configUpdate[`auth.oauth.providers.${providerId}`] = signInMethods.has(providerId) ? { + type: providerId, + allowSignIn: true, + allowConnectedAccounts: true, + } : null; + } } return configUpdate; - }, [completeConfig.emails.selectedThemeId, isLocalEmulator, selectedApps, selectedEmailThemeId, signInMethods]); + }, [completeConfig.emails.selectedThemeId, isDevelopmentEnvironment, selectedApps, selectedEmailThemeId, signInMethods]); const buildEnvironmentOAuthConfigUpdate = useCallback(() => { const configUpdate: EnvironmentConfigOverrideOverride = {}; @@ -377,7 +369,7 @@ export function ProjectOnboardingWizard(props: { return false; } - if (!isLocalEmulator) { + if (!isDevelopmentEnvironment) { const providersUpdated = await updateConfig({ adminApp: props.project.app, configUpdate: buildEnvironmentOAuthConfigUpdate(), @@ -392,17 +384,20 @@ export function ProjectOnboardingWizard(props: { }, [ buildBranchConfigUpdate, buildEnvironmentOAuthConfigUpdate, + isDevelopmentEnvironment, isLinkExistingMode, - isLocalEmulator, props.project.app, updateConfig, ]); useEffect(() => { - if (status !== "welcome" || isLinkExistingMode || isLocalEmulator || finalConfigSavePromiseRef.current != null) { + if (status !== "welcome" || isLinkExistingMode || isDevelopmentEnvironment || finalConfigSavePromiseRef.current != null) { return; } + // Cloud onboarding can quietly pre-save unlinked config. In a development + // environment that same save opens the visible local config apply dialog, so + // it must only start from the final user action. finalConfigSavePromiseRef.current = (async () => { const pushedConfigSource = await props.project.getPushedConfigSource(); if (pushedConfigSource.type !== "unlinked") { @@ -411,7 +406,7 @@ export function ProjectOnboardingWizard(props: { return await saveFinalConfig(); })(); runAsynchronously(finalConfigSavePromiseRef.current, { noErrorLogging: true }); - }, [isLinkExistingMode, isLocalEmulator, props.project, saveFinalConfig, status]); + }, [isDevelopmentEnvironment, isLinkExistingMode, props.project, saveFinalConfig, status]); const finalizeOnboarding = useCallback(async () => { await runWithSaving(async () => {