Skip to content

Commit 7112f49

Browse files
refactor onboarding page to use DesignDialog and new setting
Replace the legacy card/ActionDialog flow with the shared DesignDialog, DesignButton close affordances, and the settings-strip control component. Made-with: Cursor
1 parent 22b12a3 commit 7112f49

1 file changed

Lines changed: 130 additions & 104 deletions

File tree

  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/onboarding

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/onboarding/page-client.tsx

Lines changed: 130 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
"use client";
22

33
import { useAdminApp } from "@/app/(main)/(protected)/projects/[projectId]/use-admin-app";
4-
import { DesignBadge } from "@/components/design-components";
5-
import { DesignCard } from "@/components/design-components";
6-
import { ActionDialog, Spinner, Switch } from "@/components/ui";
4+
import {
5+
DesignBadge,
6+
DesignButton,
7+
DesignDialog,
8+
DesignDialogClose,
9+
} from "@/components/design-components";
10+
import { Typography } from "@/components/ui";
711
import { useUpdateConfig } from "@/lib/config-update";
8-
import { ShieldCheck } from "@phosphor-icons/react";
12+
import { WarningCircle } from "@phosphor-icons/react";
913
import type { RestrictedReason } from "@stackframe/stack-shared/dist/schema-fields";
1014
import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";
1115
import { useState } from "react";
1216
import { AppEnabledGuard } from "../app-enabled-guard";
1317
import { PageLayout } from "../page-layout";
18+
import { OnboardingEmailVerificationSetting } from "./onboarding-email-verification-setting";
1419

1520
type AffectedUser = {
1621
id: string,
@@ -20,13 +25,115 @@ type AffectedUser = {
2025
};
2126

2227
type PendingChange = {
23-
title: string,
24-
description: string,
2528
affectedUsers: AffectedUser[],
2629
totalAffectedCount: number,
2730
onConfirm: () => Promise<void>,
2831
};
2932

33+
function EnableEmailVerificationDialog({
34+
pendingChange,
35+
onDismiss,
36+
}: {
37+
pendingChange: PendingChange | null,
38+
onDismiss: () => void,
39+
}) {
40+
return (
41+
<DesignDialog
42+
open={pendingChange != null}
43+
onOpenChange={(open) => {
44+
if (!open) {
45+
onDismiss();
46+
}
47+
}}
48+
size="lg"
49+
icon={WarningCircle}
50+
title="Enable email verification?"
51+
description="Existing users who have not verified will need to complete verification the next time they open your app."
52+
headerContent={
53+
pendingChange != null && pendingChange.totalAffectedCount > 0 ? (
54+
<p className="text-sm text-muted-foreground">
55+
<span className="font-semibold tabular-nums text-foreground">
56+
{pendingChange.totalAffectedCount.toLocaleString()}
57+
</span>
58+
{" "}
59+
user
60+
{pendingChange.totalAffectedCount === 1 ? "" : "s"}
61+
{" "}
62+
may be asked to verify on their next app open. The list below is a sample; totals may be higher.
63+
</p>
64+
) : null
65+
}
66+
footer={(
67+
<>
68+
<DesignDialogClose asChild>
69+
<DesignButton variant="secondary" size="sm">
70+
<span>Cancel</span>
71+
</DesignButton>
72+
</DesignDialogClose>
73+
<DesignButton
74+
size="sm"
75+
onClick={async () => {
76+
if (pendingChange == null) return;
77+
await pendingChange.onConfirm();
78+
}}
79+
>
80+
<span>Enable</span>
81+
</DesignButton>
82+
</>
83+
)}
84+
>
85+
{pendingChange != null && (
86+
<div className="flex flex-col gap-3">
87+
{pendingChange.affectedUsers.length > 0 && (
88+
<div>
89+
<Typography variant="secondary" className="mb-2 text-[10px] font-semibold uppercase tracking-wider">
90+
Sample accounts
91+
</Typography>
92+
<div className="max-h-[min(200px,35vh)] overflow-y-auto rounded-xl bg-background/60 ring-1 ring-foreground/[0.06]">
93+
<ul className="divide-y divide-foreground/[0.06]">
94+
{pendingChange.affectedUsers.map((user) => (
95+
<li
96+
key={user.id}
97+
className="flex flex-col gap-0.5 px-3 py-2.5 sm:flex-row sm:items-center sm:justify-between sm:gap-3"
98+
>
99+
<div className="min-w-0">
100+
<span className="block truncate text-sm font-medium text-foreground">
101+
{user.displayName || user.primaryEmail || "Anonymous user"}
102+
</span>
103+
{user.displayName && user.primaryEmail && (
104+
<span className="block truncate text-xs text-muted-foreground">
105+
{user.primaryEmail}
106+
</span>
107+
)}
108+
</div>
109+
<div className="w-fit shrink-0">
110+
<DesignBadge
111+
label={user.restrictedReason.type === "email_not_verified" ? "Unverified" : "Anonymous"}
112+
color="orange"
113+
size="sm"
114+
/>
115+
</div>
116+
</li>
117+
))}
118+
</ul>
119+
{pendingChange.totalAffectedCount > pendingChange.affectedUsers.length && (
120+
<p className="border-t border-foreground/[0.06] px-3 py-2 text-xs text-muted-foreground">
121+
+
122+
{" "}
123+
{(pendingChange.totalAffectedCount - pendingChange.affectedUsers.length).toLocaleString()}
124+
{" "}
125+
more not shown in this sample
126+
</p>
127+
)}
128+
</div>
129+
</div>
130+
)}
131+
</div>
132+
)}
133+
</DesignDialog>
134+
);
135+
}
136+
30137
export default function PageClient() {
31138
const stackAdminApp = useAdminApp();
32139
const project = stackAdminApp.useProject();
@@ -39,7 +146,6 @@ export default function PageClient() {
39146
const handleEmailVerificationChange = async (checked: boolean) => {
40147
setIsToggling(true);
41148
try {
42-
// If enabling email verification, check for affected users first
43149
if (checked && !projectConfig.onboarding.requireEmailVerification) {
44150
// any cast needed: previewAffectedUsersByOnboardingChange is a dynamically-typed admin API method
45151
const preview = await (stackAdminApp as any).previewAffectedUsersByOnboardingChange(
@@ -49,8 +155,6 @@ export default function PageClient() {
49155

50156
if (preview.totalAffectedCount > 0) {
51157
setPendingChange({
52-
title: "Enable email verification requirement",
53-
description: `This change will require ${preview.totalAffectedCount} user${preview.totalAffectedCount === 1 ? '' : 's'} to verify their email before they can continue using your application. They will be prompted to do so the next time they visit your application.`,
54158
affectedUsers: preview.affectedUsers,
55159
totalAffectedCount: preview.totalAffectedCount,
56160
onConfirm: async () => {
@@ -66,7 +170,6 @@ export default function PageClient() {
66170
}
67171
}
68172

69-
// No affected users or disabling — apply directly
70173
await updateConfig({
71174
adminApp: stackAdminApp,
72175
configUpdate: { "onboarding.requireEmailVerification": checked },
@@ -79,101 +182,24 @@ export default function PageClient() {
79182

80183
return (
81184
<AppEnabledGuard appId="onboarding">
82-
<PageLayout title="Onboarding">
83-
<DesignCard gradient="default" glassmorphic>
84-
<div className="flex flex-col gap-4">
85-
{/* Header row: icon + title + badge + switch */}
86-
<div className="flex items-start justify-between gap-4">
87-
<div className="flex items-center gap-2 min-w-0">
88-
<div className="p-1.5 rounded-lg bg-foreground/[0.06] dark:bg-foreground/[0.04]">
89-
<ShieldCheck className="h-3.5 w-3.5 text-foreground/70 dark:text-muted-foreground" />
90-
</div>
91-
<span className="text-xs font-semibold text-foreground uppercase tracking-wider">
92-
Email Verification
93-
</span>
94-
<DesignBadge
95-
label={isEnabled ? "Enabled" : "Disabled"}
96-
color={isEnabled ? "green" : "red"}
97-
size="sm"
98-
/>
99-
</div>
100-
<div className="flex items-center flex-shrink-0">
101-
{isToggling ? (
102-
<div className="flex items-center justify-center h-5 w-9">
103-
<Spinner size={14} className="text-muted-foreground" />
104-
</div>
105-
) : (
106-
<Switch
107-
checked={isEnabled}
108-
onCheckedChange={(checked) => {
109-
runAsynchronouslyWithAlert(handleEmailVerificationChange(checked));
110-
}}
111-
/>
112-
)}
113-
</div>
114-
</div>
115-
116-
{/* Description */}
117-
<p className="text-sm text-muted-foreground leading-relaxed">
118-
{isEnabled
119-
? "Users who haven\u2019t verified their primary email will need to complete verification before they can continue. Unverified users are filtered out by default when listing users, and will be redirected to verify when using the SDK with redirect options."
120-
: "Email verification is not required. Users can access your application without verifying their email address."
121-
}
122-
</p>
123-
</div>
124-
</DesignCard>
125-
126-
<ActionDialog
127-
open={!!pendingChange}
128-
onClose={() => setPendingChange(null)}
129-
title="Enable email verification?"
130-
danger
131-
okButton={{
132-
label: "Enable",
133-
onClick: async () => {
134-
await pendingChange?.onConfirm();
135-
},
136-
}}
137-
cancelButton={{
138-
label: "Cancel",
139-
}}
140-
>
141-
{pendingChange && (
142-
<div className="flex flex-col gap-3">
143-
<p className="text-sm text-muted-foreground">
144-
{pendingChange.totalAffectedCount} existing user{pendingChange.totalAffectedCount === 1 ? '' : 's'} will
145-
need to verify their email next time they visit your app.
146-
</p>
185+
<PageLayout
186+
title="Onboarding"
187+
description="Control first-run requirements so users meet your app’s trust bar before they continue."
188+
>
189+
<div className="flex flex-col gap-4">
190+
<OnboardingEmailVerificationSetting
191+
isEnabled={isEnabled}
192+
isToggling={isToggling}
193+
onCheckedChange={(checked: boolean) => {
194+
runAsynchronouslyWithAlert(handleEmailVerificationChange(checked));
195+
}}
196+
/>
197+
</div>
147198

148-
{pendingChange.affectedUsers.length > 0 && (
149-
<div className="flex flex-col gap-1.5">
150-
{pendingChange.affectedUsers.map((user) => (
151-
<div key={user.id} className="flex items-center gap-2 text-sm">
152-
<span className="text-foreground truncate">
153-
{user.displayName || user.primaryEmail || "Anonymous user"}
154-
</span>
155-
{user.displayName && user.primaryEmail && (
156-
<span className="text-muted-foreground truncate text-xs">
157-
{user.primaryEmail}
158-
</span>
159-
)}
160-
<DesignBadge
161-
label={user.restrictedReason.type === "email_not_verified" ? "Unverified" : "Anonymous"}
162-
color="orange"
163-
size="sm"
164-
/>
165-
</div>
166-
))}
167-
{pendingChange.totalAffectedCount > pendingChange.affectedUsers.length && (
168-
<p className="text-xs text-muted-foreground">
169-
+ {pendingChange.totalAffectedCount - pendingChange.affectedUsers.length} more
170-
</p>
171-
)}
172-
</div>
173-
)}
174-
</div>
175-
)}
176-
</ActionDialog>
199+
<EnableEmailVerificationDialog
200+
pendingChange={pendingChange}
201+
onDismiss={() => setPendingChange(null)}
202+
/>
177203
</PageLayout>
178204
</AppEnabledGuard>
179205
);

0 commit comments

Comments
 (0)