From 835206c5827a9f19018ac1e91503784bd4cdf4a0 Mon Sep 17 00:00:00 2001 From: mantrakp04 Date: Mon, 29 Jun 2026 09:59:49 -0700 Subject: [PATCH] fix: update ProjectOnboardingWizard to use environment-based OAuth configuration - Refactored the onboarding logic to differentiate between local emulator and development environments for OAuth provider configuration. - Introduced a new mock for public environment variables in tests to enhance test coverage. - Added a test case to ensure the onboarding process correctly waits for user actions before applying configuration updates. These changes improve the onboarding experience and ensure proper handling of OAuth settings based on the environment. --- .../project-onboarding-wizard.test.tsx | 93 ++++++++++++++++++- .../project-onboarding-wizard.tsx | 37 ++++---- 2 files changed, 108 insertions(+), 22 deletions(-) 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 () => {