mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
Fix onboarding shared OAuth provider persistence (#1477)
This commit is contained in:
parent
4f6eebd79f
commit
7e70b11d80
@ -2,7 +2,9 @@
|
||||
|
||||
import type { ButtonHTMLAttributes, ReactNode } from "react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { cleanup, render, waitFor } from "@testing-library/react";
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
|
||||
const mockUpdateConfig = vi.hoisted(() => vi.fn(async () => true));
|
||||
|
||||
vi.mock("@/components/design-components", () => ({
|
||||
DesignCard: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
@ -82,7 +84,7 @@ vi.mock("@/lib/env", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/config-update", () => ({
|
||||
useUpdateConfig: () => vi.fn(async () => true),
|
||||
useUpdateConfig: () => mockUpdateConfig,
|
||||
}));
|
||||
|
||||
vi.mock("@stackframe/stack", () => ({
|
||||
@ -91,7 +93,8 @@ vi.mock("@stackframe/stack", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("@stackframe/stack-shared/dist/utils/oauth", () => ({
|
||||
allProviders: [],
|
||||
allProviders: ["google", "github", "microsoft", "spotify"],
|
||||
sharedProviders: ["google", "github", "microsoft", "spotify"],
|
||||
}));
|
||||
|
||||
vi.mock("@stackframe/stack-shared/dist/utils/promises", () => ({
|
||||
@ -142,6 +145,7 @@ import { ALL_APPS } from "@stackframe/stack-shared/dist/apps/apps-config";
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
mockUpdateConfig.mockClear();
|
||||
});
|
||||
|
||||
describe("ProjectOnboardingWizard", () => {
|
||||
@ -226,4 +230,84 @@ describe("ProjectOnboardingWizard", () => {
|
||||
});
|
||||
expect(onComplete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("persists shared OAuth providers selected during onboarding before completing", async () => {
|
||||
const setStatus = vi.fn(async () => {});
|
||||
const clearOnboardingState = vi.fn(async () => {});
|
||||
const onComplete = vi.fn();
|
||||
const app = {
|
||||
setupPayments: vi.fn(async () => ({ url: "https://example.com" })),
|
||||
useEmailThemes: () => [],
|
||||
useStripeAccountInfo: () => null,
|
||||
};
|
||||
const 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,
|
||||
};
|
||||
|
||||
render(
|
||||
<ProjectOnboardingWizard
|
||||
project={project as never}
|
||||
status="welcome"
|
||||
onboardingState={{
|
||||
selected_config_choice: "create-new",
|
||||
selected_apps: ["authentication", "emails"],
|
||||
selected_sign_in_methods: ["credential", "google"],
|
||||
selected_email_theme_id: "default",
|
||||
selected_payments_country: "US",
|
||||
}}
|
||||
mode={null}
|
||||
setMode={vi.fn()}
|
||||
setStatus={setStatus}
|
||||
setOnboardingState={vi.fn(async () => {})}
|
||||
clearOnboardingState={clearOnboardingState}
|
||||
onComplete={onComplete}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Get Started" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateConfig).toHaveBeenCalledTimes(2);
|
||||
expect(mockUpdateConfig).toHaveBeenNthCalledWith(2, {
|
||||
adminApp: app,
|
||||
configUpdate: {
|
||||
"auth.oauth.providers.google": {
|
||||
type: "google",
|
||||
isShared: true,
|
||||
allowSignIn: true,
|
||||
allowConnectedAccounts: true,
|
||||
},
|
||||
"auth.oauth.providers.github": null,
|
||||
"auth.oauth.providers.microsoft": null,
|
||||
},
|
||||
pushable: false,
|
||||
});
|
||||
expect(setStatus).toHaveBeenCalledWith("completed");
|
||||
expect(clearOnboardingState).toHaveBeenCalled();
|
||||
expect(onComplete).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -33,7 +33,6 @@ import { AdminOwnedProject, AuthPage } from "@stackframe/stack";
|
||||
import { type AppId } from "@stackframe/stack-shared/dist/apps/apps-config";
|
||||
import { type EnvironmentConfigOverrideOverride } from "@stackframe/stack-shared/dist/config/schema";
|
||||
import { projectOnboardingStatusValues, type ProjectOnboardingStatus } from "@stackframe/stack-shared/dist/schema-fields";
|
||||
import { allProviders } from "@stackframe/stack-shared/dist/utils/oauth";
|
||||
import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
@ -61,6 +60,7 @@ import {
|
||||
PRIMARY_APP_IDS,
|
||||
type ProjectOnboardingState,
|
||||
REQUIRED_APP_IDS,
|
||||
SHARED_OAUTH_SIGN_IN_METHODS,
|
||||
SIGN_IN_METHODS,
|
||||
type SignInMethod,
|
||||
} from "./shared";
|
||||
@ -236,8 +236,8 @@ export function ProjectOnboardingWizard(props: {
|
||||
credentialEnabled: signInMethods.has("credential"),
|
||||
magicLinkEnabled: signInMethods.has("magicLink"),
|
||||
passkeyEnabled: signInMethods.has("passkey"),
|
||||
oauthProviders: (allProviders as readonly string[])
|
||||
.filter((providerId) => signInMethods.has(providerId as SignInMethod))
|
||||
oauthProviders: SHARED_OAUTH_SIGN_IN_METHODS
|
||||
.filter((providerId) => signInMethods.has(providerId))
|
||||
.map((providerId) => ({ id: providerId, type: "shared" as const })),
|
||||
},
|
||||
};
|
||||
@ -319,26 +319,16 @@ export function ProjectOnboardingWizard(props: {
|
||||
}, [completeConfig.emails.selectedThemeId, isDevelopmentEnvironment, selectedApps, selectedEmailThemeId, signInMethods]);
|
||||
|
||||
const buildEnvironmentOAuthConfigUpdate = useCallback(() => {
|
||||
return {
|
||||
"auth.oauth.providers.google": signInMethods.has("google") ? {
|
||||
type: "google",
|
||||
const configUpdate: EnvironmentConfigOverrideOverride = {};
|
||||
for (const providerId of SHARED_OAUTH_SIGN_IN_METHODS) {
|
||||
configUpdate[`auth.oauth.providers.${providerId}`] = signInMethods.has(providerId) ? {
|
||||
type: providerId,
|
||||
isShared: true,
|
||||
allowSignIn: true,
|
||||
allowConnectedAccounts: true,
|
||||
} : null,
|
||||
"auth.oauth.providers.github": signInMethods.has("github") ? {
|
||||
type: "github",
|
||||
isShared: true,
|
||||
allowSignIn: true,
|
||||
allowConnectedAccounts: true,
|
||||
} : null,
|
||||
"auth.oauth.providers.microsoft": signInMethods.has("microsoft") ? {
|
||||
type: "microsoft",
|
||||
isShared: true,
|
||||
allowSignIn: true,
|
||||
allowConnectedAccounts: true,
|
||||
} : null,
|
||||
};
|
||||
} : null;
|
||||
}
|
||||
return configUpdate;
|
||||
}, [signInMethods]);
|
||||
|
||||
const finalizeOnboarding = useCallback(async () => {
|
||||
|
||||
@ -2,6 +2,7 @@ import { stackAppInternalsSymbol } from "@/lib/stack-app-internals";
|
||||
import { AdminOwnedProject } from "@stackframe/stack";
|
||||
import { ALL_APPS, type AppId } from "@stackframe/stack-shared/dist/apps/apps-config";
|
||||
import { projectOnboardingStatusValues, type ProjectOnboardingStatus } from "@stackframe/stack-shared/dist/schema-fields";
|
||||
import { sharedProviders } from "@stackframe/stack-shared/dist/utils/oauth";
|
||||
import { stringCompare } from "@stackframe/stack-shared/dist/utils/strings";
|
||||
|
||||
const PROJECT_ONBOARDING_STATUSES = projectOnboardingStatusValues;
|
||||
@ -23,7 +24,10 @@ export const REQUIRED_APP_IDS: AppId[] = ["authentication", "emails"];
|
||||
export const PRIMARY_APP_IDS: AppId[] = ["authentication", "emails", "payments", "analytics"];
|
||||
export const ALL_APP_IDS = Object.keys(ALL_APPS) as AppId[];
|
||||
export const ONBOARDING_APP_IDS = ALL_APP_IDS.filter((appId) => ALL_APPS[appId].stage !== "alpha");
|
||||
export const OAUTH_SIGN_IN_METHODS: SignInMethod[] = ["google", "github", "microsoft"];
|
||||
export const OAUTH_SIGN_IN_METHODS = ["google", "github", "microsoft"] satisfies SignInMethod[];
|
||||
export const SHARED_OAUTH_SIGN_IN_METHODS = sharedProviders.filter((provider): provider is (typeof sharedProviders)[number] & SignInMethod => {
|
||||
return OAUTH_SIGN_IN_METHODS.some((method) => method === provider);
|
||||
});
|
||||
|
||||
export type ProjectOnboardingState = {
|
||||
selected_config_choice: OnboardingConfigChoice,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user