Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
Expand Down Expand Up @@ -82,7 +84,7 @@ vi.mock("@/lib/env", () => ({
}));

vi.mock("@/lib/config-update", () => ({
useUpdateConfig: () => vi.fn(async () => true),
useUpdateConfig: () => mockUpdateConfig,
}));

vi.mock("@stackframe/stack", () => ({
Expand All @@ -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", () => ({
Expand Down Expand Up @@ -142,6 +145,7 @@ import { ALL_APPS } from "@stackframe/stack-shared/dist/apps/apps-config";

afterEach(() => {
cleanup();
mockUpdateConfig.mockClear();
});

describe("ProjectOnboardingWizard", () => {
Expand Down Expand Up @@ -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();
});
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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 })),
},
};
Expand Down Expand Up @@ -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",
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",
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,
};
} : null;
}
return configUpdate;
}, [signInMethods]);

const finalizeOnboarding = useCallback(async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand Down
Loading