Skip to content

Commit 22e9f63

Browse files
authored
Merge branch 'dev' into fix/github-config-link-flow
2 parents c80c89f + 07af469 commit 22e9f63

13 files changed

Lines changed: 586 additions & 77 deletions

File tree

apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client-parts/project-onboarding-wizard.test.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,8 @@ vi.mock("./link-existing-onboarding", () => ({
137137
}));
138138

139139
import { ProjectOnboardingWizard } from "./project-onboarding-wizard";
140-
import { normalizeProjectOnboardingState, REQUIRED_APP_IDS } from "./shared";
140+
import { normalizeProjectOnboardingState, orderedAppIds, REQUIRED_APP_IDS } from "./shared";
141+
import { ALL_APPS } from "@stackframe/stack-shared/dist/apps/apps-config";
141142

142143
afterEach(() => {
143144
cleanup();
@@ -156,6 +157,16 @@ describe("ProjectOnboardingWizard", () => {
156157
expect(normalizedState.selected_apps).toEqual(REQUIRED_APP_IDS);
157158
});
158159

160+
it("does not offer alpha apps during app selection", () => {
161+
const alphaAppIds = Object.entries(ALL_APPS)
162+
.filter(([, app]) => app.stage === "alpha")
163+
.map(([appId]) => appId);
164+
165+
for (const alphaAppId of alphaAppIds) {
166+
expect(orderedAppIds()).not.toContain(alphaAppId);
167+
}
168+
});
169+
159170
it("completes onboarding automatically after Stripe setup returns successfully", async () => {
160171
const setStatus = vi.fn(async () => {});
161172
const onComplete = vi.fn();

apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client-parts/shared.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export const SIGN_IN_METHODS: Array<{ id: SignInMethod, label: string }> = [
2222
export const REQUIRED_APP_IDS: AppId[] = ["authentication", "emails"];
2323
export const PRIMARY_APP_IDS: AppId[] = ["authentication", "emails", "payments", "analytics"];
2424
export const ALL_APP_IDS = Object.keys(ALL_APPS) as AppId[];
25+
export const ONBOARDING_APP_IDS = ALL_APP_IDS.filter((appId) => ALL_APPS[appId].stage !== "alpha");
2526
export const OAUTH_SIGN_IN_METHODS: SignInMethod[] = ["google", "github", "microsoft"];
2627

2728
export type ProjectOnboardingState = {
@@ -149,11 +150,12 @@ export function isProjectOnboardingStatus(value: unknown): value is ProjectOnboa
149150
}
150151

151152
export function orderedAppIds() {
152-
const primarySet = new Set(PRIMARY_APP_IDS);
153-
const secondary = ALL_APP_IDS.filter((appId) => !primarySet.has(appId)).sort((a, b) => {
153+
const primary = PRIMARY_APP_IDS.filter((appId) => ONBOARDING_APP_IDS.some((onboardingAppId) => onboardingAppId === appId));
154+
const primarySet = new Set(primary);
155+
const secondary = ONBOARDING_APP_IDS.filter((appId) => !primarySet.has(appId)).sort((a, b) => {
154156
return stringCompare(ALL_APPS[a].displayName, ALL_APPS[b].displayName);
155157
});
156-
return [...PRIMARY_APP_IDS, ...secondary];
158+
return [...primary, ...secondary];
157159
}
158160

159161
export function normalizeTrustedDomain(input: string): string {

docs-mintlify/guides/getting-started/setup.mdx

Lines changed: 195 additions & 41 deletions
Large diffs are not rendered by default.

docs-mintlify/snippets/home-prompt-island.jsx

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";
2+
import { UserAvatar, useStackApp } from "@stackframe/tanstack-start";
3+
import type { CurrentUser } from "@stackframe/tanstack-start";
4+
5+
type AuthDemoCardProps = {
6+
title: string,
7+
eyebrow: string,
8+
description: string,
9+
user: CurrentUser | null,
10+
code: string,
11+
};
12+
13+
export function AuthDemoCard(props: AuthDemoCardProps) {
14+
const app = useStackApp();
15+
const userLabel = props.user?.displayName ?? props.user?.primaryEmail ?? props.user?.id;
16+
17+
return (
18+
<section className="grid w-full gap-6">
19+
<div className="rounded-lg border border-zinc-200 bg-white p-8 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
20+
<p className="mb-2 text-sm font-medium text-zinc-500 dark:text-zinc-400">{props.eyebrow}</p>
21+
<h1 className="text-3xl font-semibold tracking-tight">{props.title}</h1>
22+
<p className="mt-4 max-w-2xl text-zinc-600 dark:text-zinc-300">{props.description}</p>
23+
24+
<div className="mt-8 rounded-lg border border-zinc-200 p-5 dark:border-zinc-800">
25+
{props.user ? (
26+
<div className="flex flex-col gap-4 sm:flex-row sm:items-center">
27+
<UserAvatar user={props.user} size={64} />
28+
<div className="min-w-0">
29+
<p className="text-sm text-zinc-500 dark:text-zinc-400">Resolved Stack Auth user</p>
30+
<p className="truncate text-xl font-semibold">{userLabel}</p>
31+
<p className="mt-1 break-all font-mono text-sm text-zinc-500 dark:text-zinc-400">{props.user.id}</p>
32+
</div>
33+
</div>
34+
) : (
35+
<div>
36+
<p className="text-lg font-semibold">No signed-in user</p>
37+
<p className="mt-1 text-sm text-zinc-500 dark:text-zinc-400">
38+
This route rendered the signed-out branch from Stack Auth.
39+
</p>
40+
<div className="mt-4 flex flex-wrap gap-3">
41+
<button className="rounded-md bg-zinc-950 px-4 py-2 text-sm font-medium text-white transition-colors hover:transition-none dark:bg-white dark:text-zinc-950" onClick={() => runAsynchronouslyWithAlert(app.redirectToSignIn())}>
42+
Sign in
43+
</button>
44+
<button className="rounded-md border border-zinc-300 px-4 py-2 text-sm font-medium transition-colors hover:bg-zinc-100 hover:transition-none dark:border-zinc-700 dark:hover:bg-zinc-800" onClick={() => runAsynchronouslyWithAlert(app.redirectToSignUp())}>
45+
Sign up
46+
</button>
47+
</div>
48+
</div>
49+
)}
50+
</div>
51+
</div>
52+
53+
<div className="rounded-lg border border-zinc-200 bg-zinc-950 p-5 text-zinc-100 shadow-sm dark:border-zinc-800">
54+
<div className="mb-3 flex items-center justify-between gap-3">
55+
<h2 className="text-sm font-semibold uppercase text-zinc-400">Usage snippet</h2>
56+
</div>
57+
<pre className="overflow-x-auto text-sm leading-6"><code>{props.code}</code></pre>
58+
</div>
59+
</section>
60+
);
61+
}
Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Link } from "@tanstack/react-router";
22
import { UserButton } from "@stackframe/tanstack-start";
3-
import { useEffect, useState } from "react";
43

54
export function Header() {
65
return (
@@ -11,23 +10,20 @@ export function Header() {
1110
<Link to="/" className="font-semibold tracking-tight">
1211
Stack TanStack Demo
1312
</Link>
13+
<Link to="/ssr" className="text-sm text-zinc-600 hover:text-zinc-950 hover:transition-none dark:text-zinc-300 dark:hover:text-white">
14+
SSR
15+
</Link>
16+
<Link to="/client" className="text-sm text-zinc-600 hover:text-zinc-950 hover:transition-none dark:text-zinc-300 dark:hover:text-white">
17+
Client
18+
</Link>
1419
<Link to="/protected" className="text-sm text-zinc-600 hover:text-zinc-950 hover:transition-none dark:text-zinc-300 dark:hover:text-white">
1520
Protected
1621
</Link>
1722
</nav>
18-
<ClientMountedUserButton />
23+
<UserButton />
1924
</div>
2025
</header>
2126
<div className="h-14" />
2227
</>
2328
);
2429
}
25-
26-
function ClientMountedUserButton() {
27-
const [isMounted, setIsMounted] = useState(false);
28-
useEffect(() => {
29-
setIsMounted(true);
30-
}, []);
31-
32-
return isMounted ? <UserButton /> : <div className="h-9 w-9" />;
33-
}

examples/tanstack-start-demo/src/routeTree.gen.ts

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,27 @@
99
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
1010

1111
import { Route as rootRouteImport } from './routes/__root'
12+
import { Route as SsrRouteImport } from './routes/ssr'
1213
import { Route as ProtectedRouteImport } from './routes/protected'
14+
import { Route as ClientRouteImport } from './routes/client'
1315
import { Route as IndexRouteImport } from './routes/index'
1416
import { Route as HandlerSplatRouteImport } from './routes/handler/$'
1517

18+
const SsrRoute = SsrRouteImport.update({
19+
id: '/ssr',
20+
path: '/ssr',
21+
getParentRoute: () => rootRouteImport,
22+
} as any)
1623
const ProtectedRoute = ProtectedRouteImport.update({
1724
id: '/protected',
1825
path: '/protected',
1926
getParentRoute: () => rootRouteImport,
2027
} as any)
28+
const ClientRoute = ClientRouteImport.update({
29+
id: '/client',
30+
path: '/client',
31+
getParentRoute: () => rootRouteImport,
32+
} as any)
2133
const IndexRoute = IndexRouteImport.update({
2234
id: '/',
2335
path: '/',
@@ -31,43 +43,65 @@ const HandlerSplatRoute = HandlerSplatRouteImport.update({
3143

3244
export interface FileRoutesByFullPath {
3345
'/': typeof IndexRoute
46+
'/client': typeof ClientRoute
3447
'/protected': typeof ProtectedRoute
48+
'/ssr': typeof SsrRoute
3549
'/handler/$': typeof HandlerSplatRoute
3650
}
3751
export interface FileRoutesByTo {
3852
'/': typeof IndexRoute
53+
'/client': typeof ClientRoute
3954
'/protected': typeof ProtectedRoute
55+
'/ssr': typeof SsrRoute
4056
'/handler/$': typeof HandlerSplatRoute
4157
}
4258
export interface FileRoutesById {
4359
__root__: typeof rootRouteImport
4460
'/': typeof IndexRoute
61+
'/client': typeof ClientRoute
4562
'/protected': typeof ProtectedRoute
63+
'/ssr': typeof SsrRoute
4664
'/handler/$': typeof HandlerSplatRoute
4765
}
4866
export interface FileRouteTypes {
4967
fileRoutesByFullPath: FileRoutesByFullPath
50-
fullPaths: '/' | '/protected' | '/handler/$'
68+
fullPaths: '/' | '/client' | '/protected' | '/ssr' | '/handler/$'
5169
fileRoutesByTo: FileRoutesByTo
52-
to: '/' | '/protected' | '/handler/$'
53-
id: '__root__' | '/' | '/protected' | '/handler/$'
70+
to: '/' | '/client' | '/protected' | '/ssr' | '/handler/$'
71+
id: '__root__' | '/' | '/client' | '/protected' | '/ssr' | '/handler/$'
5472
fileRoutesById: FileRoutesById
5573
}
5674
export interface RootRouteChildren {
5775
IndexRoute: typeof IndexRoute
76+
ClientRoute: typeof ClientRoute
5877
ProtectedRoute: typeof ProtectedRoute
78+
SsrRoute: typeof SsrRoute
5979
HandlerSplatRoute: typeof HandlerSplatRoute
6080
}
6181

6282
declare module '@tanstack/react-router' {
6383
interface FileRoutesByPath {
84+
'/ssr': {
85+
id: '/ssr'
86+
path: '/ssr'
87+
fullPath: '/ssr'
88+
preLoaderRoute: typeof SsrRouteImport
89+
parentRoute: typeof rootRouteImport
90+
}
6491
'/protected': {
6592
id: '/protected'
6693
path: '/protected'
6794
fullPath: '/protected'
6895
preLoaderRoute: typeof ProtectedRouteImport
6996
parentRoute: typeof rootRouteImport
7097
}
98+
'/client': {
99+
id: '/client'
100+
path: '/client'
101+
fullPath: '/client'
102+
preLoaderRoute: typeof ClientRouteImport
103+
parentRoute: typeof rootRouteImport
104+
}
71105
'/': {
72106
id: '/'
73107
path: '/'
@@ -87,7 +121,9 @@ declare module '@tanstack/react-router' {
87121

88122
const rootRouteChildren: RootRouteChildren = {
89123
IndexRoute: IndexRoute,
124+
ClientRoute: ClientRoute,
90125
ProtectedRoute: ProtectedRoute,
126+
SsrRoute: SsrRoute,
91127
HandlerSplatRoute: HandlerSplatRoute,
92128
}
93129
export const routeTree = rootRouteImport

examples/tanstack-start-demo/src/routes/__root.tsx

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,50 @@ function RootComponent() {
4444
return (
4545
<StackProvider app={stackApp}>
4646
<StackTheme>
47-
<div className="min-h-screen bg-zinc-100 text-zinc-950 dark:bg-zinc-950 dark:text-zinc-50">
48-
<Header />
49-
<main className="mx-auto flex min-h-[calc(100vh-3.5rem)] max-w-5xl px-4 py-8">
50-
<Suspense fallback={null}>
51-
<Outlet />
52-
</Suspense>
53-
</main>
54-
</div>
47+
<AppShell>
48+
<Suspense fallback={<RouteLoadingState />}>
49+
<Outlet />
50+
</Suspense>
51+
</AppShell>
5552
</StackTheme>
5653
</StackProvider>
5754
);
5855
}
56+
57+
function AppShell({ children }: { children: ReactNode }) {
58+
return (
59+
<div className="min-h-screen bg-zinc-100 text-zinc-950 dark:bg-zinc-950 dark:text-zinc-50">
60+
<Header />
61+
<main className="mx-auto flex min-h-[calc(100vh-3.5rem)] max-w-5xl px-4 py-8">
62+
{children}
63+
</main>
64+
</div>
65+
);
66+
}
67+
68+
function RouteLoadingState() {
69+
return (
70+
<section className="grid w-full place-items-center">
71+
<div className="w-full max-w-2xl rounded-lg border border-zinc-200 bg-white p-8 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
72+
<div className="flex flex-col gap-5 sm:flex-row sm:items-center">
73+
<div className="h-24 w-24 shrink-0 rounded-full bg-zinc-200 dark:bg-zinc-800" />
74+
<div className="min-w-0 flex-1">
75+
<div className="h-4 w-24 rounded bg-zinc-200 dark:bg-zinc-800" />
76+
<div className="mt-3 h-9 w-full max-w-md rounded bg-zinc-200 dark:bg-zinc-800" />
77+
</div>
78+
</div>
79+
<div className="mt-8 grid gap-3 text-sm">
80+
<div className="grid gap-1 sm:grid-cols-[8rem_1fr]">
81+
<div className="h-5 w-16 rounded bg-zinc-200 dark:bg-zinc-800" />
82+
<div className="h-5 w-full rounded bg-zinc-200 dark:bg-zinc-800" />
83+
</div>
84+
<div className="grid gap-1 sm:grid-cols-[8rem_1fr]">
85+
<div className="h-5 w-20 rounded bg-zinc-200 dark:bg-zinc-800" />
86+
<div className="h-5 w-12 rounded bg-zinc-200 dark:bg-zinc-800" />
87+
</div>
88+
</div>
89+
<div className="mt-8 h-9 w-20 rounded-md bg-zinc-200 dark:bg-zinc-800" />
90+
</div>
91+
</section>
92+
);
93+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { useUser } from "@stackframe/tanstack-start";
2+
import { createFileRoute } from "@tanstack/react-router";
3+
import { AuthDemoCard } from "~/components/auth-demo-card";
4+
5+
export const Route = createFileRoute("/client")({
6+
ssr: false,
7+
component: ClientAuthDemoPage,
8+
});
9+
10+
const clientSnippet = `import { useUser } from "@stackframe/tanstack-start";
11+
import { createFileRoute } from "@tanstack/react-router";
12+
13+
export const Route = createFileRoute("/client")({
14+
ssr: false,
15+
component: ClientAuthDemoPage,
16+
});
17+
18+
function ClientAuthDemoPage() {
19+
// This route is skipped during SSR. The user is resolved
20+
// in the browser from the client token store.
21+
const user = useUser({ includeRestricted: true });
22+
23+
return <div>{user?.displayName ?? "Signed out"}</div>;
24+
}`;
25+
26+
function ClientAuthDemoPage() {
27+
const user = useUser({ includeRestricted: true });
28+
29+
return (
30+
<AuthDemoCard
31+
eyebrow="Client-only route"
32+
title="Stack Auth user fetched in the browser"
33+
description="This route opts out of SSR with ssr: false. The UI is rendered on the client, and Stack Auth resolves the current user from the browser token store."
34+
user={user}
35+
code={clientSnippet}
36+
/>
37+
);
38+
}

examples/tanstack-start-demo/src/routes/index.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";
22
import { UserAvatar, useStackApp, useUser } from "@stackframe/tanstack-start";
3-
import { createFileRoute } from "@tanstack/react-router";
3+
import { Link, createFileRoute } from "@tanstack/react-router";
44

55
export const Route = createFileRoute("/")({
66
component: HomePage,
@@ -19,6 +19,14 @@ function HomePage() {
1919
<p className="mt-4 text-zinc-600 dark:text-zinc-300">
2020
This example uses <code className="rounded bg-zinc-100 px-1.5 py-0.5 text-sm dark:bg-zinc-800">@stackframe/tanstack-start</code> with file-based routes and Stack Auth handler pages.
2121
</p>
22+
<div className="mt-6 flex flex-wrap gap-3">
23+
<Link to="/ssr" className="rounded-md border border-zinc-300 px-4 py-2 text-sm font-medium transition-colors hover:bg-zinc-100 hover:transition-none dark:border-zinc-700 dark:hover:bg-zinc-800">
24+
SSR demo
25+
</Link>
26+
<Link to="/client" className="rounded-md border border-zinc-300 px-4 py-2 text-sm font-medium transition-colors hover:bg-zinc-100 hover:transition-none dark:border-zinc-700 dark:hover:bg-zinc-800">
27+
Client demo
28+
</Link>
29+
</div>
2230
<div className="mt-6 flex flex-wrap gap-3">
2331
<button className="rounded-md bg-zinc-950 px-4 py-2 text-sm font-medium text-white transition-colors hover:transition-none dark:bg-white dark:text-zinc-950" onClick={() => runAsynchronouslyWithAlert(app.redirectToSignIn())}>
2432
Sign in
@@ -66,6 +74,12 @@ function HomePage() {
6674
</dl>
6775

6876
<div className="mt-8 flex flex-wrap gap-3">
77+
<Link to="/ssr" className="rounded-md border border-zinc-300 px-4 py-2 text-sm font-medium transition-colors hover:bg-zinc-100 hover:transition-none dark:border-zinc-700 dark:hover:bg-zinc-800">
78+
SSR demo
79+
</Link>
80+
<Link to="/client" className="rounded-md border border-zinc-300 px-4 py-2 text-sm font-medium transition-colors hover:bg-zinc-100 hover:transition-none dark:border-zinc-700 dark:hover:bg-zinc-800">
81+
Client demo
82+
</Link>
6983
<button className="rounded-md bg-red-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-red-700 hover:transition-none" onClick={() => runAsynchronouslyWithAlert(app.redirectToSignOut())}>
7084
Sign out
7185
</button>

0 commit comments

Comments
 (0)