mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-30 21:01:54 +08:00
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:
parent
b7394e1bd3
commit
835206c582
@ -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();
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user