11"use client" ;
22
33import { 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" ;
711import { useUpdateConfig } from "@/lib/config-update" ;
8- import { ShieldCheck } from "@phosphor-icons/react" ;
12+ import { WarningCircle } from "@phosphor-icons/react" ;
913import type { RestrictedReason } from "@stackframe/stack-shared/dist/schema-fields" ;
1014import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises" ;
1115import { useState } from "react" ;
1216import { AppEnabledGuard } from "../app-enabled-guard" ;
1317import { PageLayout } from "../page-layout" ;
18+ import { OnboardingEmailVerificationSetting } from "./onboarding-email-verification-setting" ;
1419
1520type AffectedUser = {
1621 id : string ,
@@ -20,13 +25,115 @@ type AffectedUser = {
2025} ;
2126
2227type 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+
30137export 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