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();
});
});