66 type AuthProvider ,
77} from "../../lib/supabaseClient" ;
88import { CollectBrand } from "../CollectBrand" ;
9- import { Icon } from "../Icon" ;
9+ import { Icon , type IconName } from "../Icon" ;
1010import { isAppleMobileBrowser , isStandaloneApp } from "../../lib/platform" ;
1111import { CodeSignIn } from "./CodeSignIn" ;
1212import { EmailLinkForm } from "./EmailLinkForm" ;
@@ -23,22 +23,42 @@ interface AuthScreenProps {
2323 onPasswordSet ?: ( ) => void ;
2424}
2525
26- /** Sign-in methods, in the order the screen offers them. */
27- type EntryMode = "provider" | "link" | "password" | "code" ;
26+ /** The backup methods, in the order the list offers them. */
27+ type BackupMethod = "link" | "password" | "code" ;
2828
29- const modeTitle : Record < EntryMode , string > = {
30- provider : "Sign in." ,
31- link : "Sign in with a link." ,
32- password : "Sign in with a password." ,
33- code : "Sign in with a code." ,
34- } ;
35-
36- const modeAction : Record < EntryMode , string > = {
37- provider : "Continue with Google or Apple" ,
38- link : "Email me a sign-in link" ,
39- password : "Use an email address and password" ,
40- code : "Use a code from your administrator or another device" ,
41- } ;
29+ const backupMethods : {
30+ id : BackupMethod ;
31+ icon : IconName ;
32+ title : string ;
33+ detail : string ;
34+ heading : string ;
35+ lede : string ;
36+ } [ ] = [
37+ {
38+ id : "link" ,
39+ icon : "send" ,
40+ title : "Email me a sign-in link" ,
41+ detail : "Opens collect from your inbox." ,
42+ heading : "Sign in with a link." ,
43+ lede : "We send a one-time link to the address on your account." ,
44+ } ,
45+ {
46+ id : "password" ,
47+ icon : "key" ,
48+ title : "Sign in with a password" ,
49+ detail : "For an account that already has one." ,
50+ heading : "Sign in with a password." ,
51+ lede : "Use the email address and password on your account." ,
52+ } ,
53+ {
54+ id : "code" ,
55+ icon : "phone" ,
56+ title : "Sign in with a code" ,
57+ detail : "Eight characters, from your administrator or a signed-in device." ,
58+ heading : "Sign in with a code." ,
59+ lede : "Enter the code you were given, or request a fresh one below." ,
60+ } ,
61+ ] ;
4262
4363function isLocalDevelopmentOrigin ( ) : boolean {
4464 if ( typeof window === "undefined" ) return false ;
@@ -51,11 +71,10 @@ function isLocalDevelopmentOrigin(): boolean {
5171/**
5272 * The single sign-in surface for both installed apps.
5373 *
54- * Providers come first: they need no email, so a deployment never depends on a
55- * mail quota to admit people. Every other method stays available underneath,
56- * each named after the authentication it performs, as Apple's "Managing
57- * accounts" guidance requires. Only methods this deployment actually offers
58- * are shown.
74+ * One decision at a time. Providers come first: they need no email, so a
75+ * deployment never depends on a mail quota to admit people. Everything else
76+ * sits in one list of named methods, and choosing one opens that method alone
77+ * with an explicit way back — no screen ever shows two ways in at once.
5978 */
6079export function AuthScreen ( {
6180 configured,
@@ -71,7 +90,7 @@ export function AuthScreen({
7190 configured ? knownAuthProviders ( ) : [ ] ,
7291 ) ;
7392 const [ providersChecked , setProvidersChecked ] = useState ( false ) ;
74- const [ mode , setMode ] = useState < EntryMode | null > ( null ) ;
93+ const [ method , setMethod ] = useState < BackupMethod | null > ( null ) ;
7594 const [ callbackIssue , setCallbackIssue ] = useState < string | null > ( ( ) =>
7695 authCallbackError ( ) ,
7796 ) ;
@@ -92,18 +111,7 @@ export function AuthScreen({
92111 } ;
93112 } , [ configured ] ) ;
94113
95- // Before the deployment answers, keep the screen quiet rather than guessing
96- // a method that may not exist here.
97- const entryMode : EntryMode =
98- mode ??
99- ( providers . length ? "provider" : role === "admin" ? "link" : "code" ) ;
100- const alternatives = (
101- [ "provider" , "link" , "password" , "code" ] as const
102- ) . filter (
103- ( candidate ) =>
104- candidate !== entryMode &&
105- ( candidate !== "provider" || providers . length > 0 ) ,
106- ) ;
114+ const chosen = backupMethods . find ( ( candidate ) => candidate . id === method ) ;
107115
108116 if ( requirePasswordSetup ) {
109117 return (
@@ -122,19 +130,34 @@ export function AuthScreen({
122130 < CollectBrand />
123131 </ div >
124132 < section className = "auth-card" aria-labelledby = "auth-title" >
133+ { chosen && (
134+ < button
135+ type = "button"
136+ className = "text-button auth-back"
137+ onClick = { ( ) => {
138+ setMethod ( null ) ;
139+ setCallbackIssue ( null ) ;
140+ } }
141+ >
142+ < Icon name = "chevron-left" size = { 16 } /> All sign-in options
143+ </ button >
144+ ) }
145+
125146 < h1 id = "auth-title" >
126- { role === "admin" && entryMode === "provider"
127- ? "Admin sign in."
128- : modeTitle [ entryMode ] }
147+ { chosen
148+ ? chosen . heading
149+ : role === "admin"
150+ ? "Admin sign in."
151+ : "Sign in." }
129152 </ h1 >
130153 < p >
131154 { ! configured
132155 ? "Authentication is not configured for this deployment."
133- : entryMode === "provider"
134- ? "Sign in to reach the projects you contribute to. Your email address stays your identifier."
135- : entryMode === "code"
136- ? "Enter the code your administrator issued, or request a new one below ."
137- : "Use the email address your invitation was sent to ." }
156+ : chosen
157+ ? chosen . lede
158+ : providers . length
159+ ? "Sign in to reach the projects you contribute to ."
160+ : "Choose how to sign in ." }
138161 </ p >
139162
140163 { configured && (
@@ -145,48 +168,64 @@ export function AuthScreen({
145168 </ p >
146169 ) }
147170
148- { entryMode === "provider" && (
149- < ProviderSignIn
150- providers = { providers }
151- surface = { role }
152- onFailure = { ( ) => setCallbackIssue ( null ) }
153- />
154- ) }
155- { entryMode === "link" && (
156- < EmailLinkForm showLocalRedirectHint = { showLocalRedirectHint } />
157- ) }
158- { entryMode === "password" && < PasswordForm /> }
159- { entryMode === "code" && < CodeSignIn autoFocus = { mode === "code" } /> }
160-
161- { entryMode === "provider" && standalone && (
162- < p className = "auth-config-note" >
163- < Icon name = "info" size = { 16 } />
164- < span >
165- The provider opens in the browser. If it does not return to
166- this app, sign in there and use a code from the signed-in
167- browser.
168- </ span >
169- </ p >
170- ) }
171+ { chosen ? (
172+ < >
173+ { chosen . id === "link" && (
174+ < EmailLinkForm
175+ showLocalRedirectHint = { showLocalRedirectHint }
176+ />
177+ ) }
178+ { chosen . id === "password" && < PasswordForm /> }
179+ { chosen . id === "code" && < CodeSignIn autoFocus /> }
180+ </ >
181+ ) : (
182+ < >
183+ < ProviderSignIn
184+ providers = { providers }
185+ surface = { role }
186+ onFailure = { ( ) => setCallbackIssue ( null ) }
187+ />
188+
189+ { providers . length > 0 && standalone && (
190+ < p className = "auth-config-note" >
191+ < Icon name = "info" size = { 16 } />
192+ < span >
193+ The provider opens in the browser. If it does not return
194+ to this app, sign in there and use a code from the
195+ signed-in browser.
196+ </ span >
197+ </ p >
198+ ) }
171199
172- { providersChecked && alternatives . length > 0 && (
173- < details className = "auth-alternatives" >
174- < summary > Other ways to sign in</ summary >
175- { alternatives . map ( ( candidate ) => (
176- < button
177- key = { candidate }
178- type = "button"
179- className = "text-button"
180- onClick = { ( ) => {
181- setMode ( candidate ) ;
182- setCallbackIssue ( null ) ;
183- } }
184- >
185- { modeAction [ candidate ] } { " " }
186- < Icon name = "arrow-right" size = { 15 } />
187- </ button >
188- ) ) }
189- </ details >
200+ { providersChecked && (
201+ < >
202+ { providers . length > 0 && (
203+ < p className = "auth-list-heading" > Other ways to sign in</ p >
204+ ) }
205+ < ul className = "auth-method-list" >
206+ { backupMethods . map ( ( candidate ) => (
207+ < li key = { candidate . id } >
208+ < button
209+ type = "button"
210+ className = "auth-method"
211+ onClick = { ( ) => {
212+ setMethod ( candidate . id ) ;
213+ setCallbackIssue ( null ) ;
214+ } }
215+ >
216+ < Icon name = { candidate . icon } size = { 19 } />
217+ < span className = "auth-method-copy" >
218+ < strong > { candidate . title } </ strong >
219+ < span > { candidate . detail } </ span >
220+ </ span >
221+ < Icon name = "chevron-right" size = { 17 } />
222+ </ button >
223+ </ li >
224+ ) ) }
225+ </ ul >
226+ </ >
227+ ) }
228+ </ >
190229 ) }
191230 </ >
192231 ) }
@@ -197,7 +236,7 @@ export function AuthScreen({
197236 </ button >
198237 ) }
199238
200- { showInstallHint && (
239+ { showInstallHint && ! chosen && (
201240 < details className = "auth-install-help" >
202241 < summary >
203242 < Icon name = "plus" size = { 16 } /> Add collect to Home Screen
0 commit comments