From 81b580c89dbb30d537b6092cab35162f5f498f1a Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 23 May 2026 00:32:06 +0000 Subject: [PATCH 1/2] Fix onboarding shared OAuth provider persistence --- .../project-onboarding-wizard.test.tsx | 90 ++++++++++++++++++- .../project-onboarding-wizard.tsx | 30 +++---- .../new-project/page-client-parts/shared.ts | 6 +- 3 files changed, 102 insertions(+), 24 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 71494dbb04..7eb8007722 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 @@ -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 }) =>
{children}
, @@ -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( + {})} + 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(); + }); }); 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 fc101414bf..a120b5fbbe 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 @@ -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", - 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 () => { diff --git a/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client-parts/shared.ts b/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client-parts/shared.ts index fee08aa524..a7c4772028 100644 --- a/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client-parts/shared.ts +++ b/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client-parts/shared.ts @@ -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, From 98d48d3330db69723f306427afaa8abd544e4e07 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 23 May 2026 00:40:44 +0000 Subject: [PATCH 2/2] Stabilize onboarding OAuth persistence test --- .../project-onboarding-wizard.test.tsx | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 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 7eb8007722..a7918b85d9 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 @@ -291,23 +291,23 @@ describe("ProjectOnboardingWizard", () => { 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, + 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, }, - "auth.oauth.providers.github": null, - "auth.oauth.providers.microsoft": null, - }, - pushable: false, + pushable: false, + }); + expect(setStatus).toHaveBeenCalledWith("completed"); + expect(clearOnboardingState).toHaveBeenCalled(); + expect(onComplete).toHaveBeenCalled(); }); - expect(setStatus).toHaveBeenCalledWith("completed"); - expect(clearOnboardingState).toHaveBeenCalled(); - expect(onComplete).toHaveBeenCalled(); }); });