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.
This commit is contained in:
mantrakp04 2026-06-29 09:59:49 -07:00
parent b7394e1bd3
commit 835206c582
2 changed files with 108 additions and 22 deletions

View File

@ -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<string, string>());
vi.mock("@/components/design-components", () => ({
DesignCard: ({ children }: { children: ReactNode }) => <div>{children}</div>,
@ -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<T>() {
@ -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(
<ProjectOnboardingWizard
project={{
id: "proj_123",
config: {
credentialEnabled: true,
magicLinkEnabled: false,
passkeyEnabled: false,
oauthProviders: [],
},
useConfig: () => ({
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();

View File

@ -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 () => {