@ankhorage/studio 2.0.1 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +1 -1
  2. package/dist/host/layout/auth/resolveAuthLayoutPlan.d.ts +1 -1
  3. package/dist/host/layout/auth/resolveAuthLayoutPlan.d.ts.map +1 -1
  4. package/dist/host/layout/auth/resolveAuthLayoutPlan.js +24 -1
  5. package/dist/host/layout/auth/resolveAuthLayoutPlan.js.map +1 -1
  6. package/dist/host/layout/layoutGenerator.d.ts.map +1 -1
  7. package/dist/host/layout/layoutGenerator.js +45 -20
  8. package/dist/host/layout/layoutGenerator.js.map +1 -1
  9. package/dist/host/layout/templates/auth/adapter.js +1 -1
  10. package/dist/host/layout/templates/auth/adapter.js.map +1 -1
  11. package/dist/host/layout/templates/auth/callback.d.ts.map +1 -1
  12. package/dist/host/layout/templates/auth/callback.js +80 -42
  13. package/dist/host/layout/templates/auth/callback.js.map +1 -1
  14. package/dist/host/layout/templates/auth/form.d.ts +2 -0
  15. package/dist/host/layout/templates/auth/form.d.ts.map +1 -0
  16. package/dist/host/layout/templates/auth/form.js +238 -0
  17. package/dist/host/layout/templates/auth/form.js.map +1 -0
  18. package/dist/host/layout/templates/auth/navigation.d.ts +10 -0
  19. package/dist/host/layout/templates/auth/navigation.d.ts.map +1 -0
  20. package/dist/host/layout/templates/auth/navigation.js +183 -0
  21. package/dist/host/layout/templates/auth/navigation.js.map +1 -0
  22. package/dist/host/layout/templates/auth/oauth.d.ts.map +1 -1
  23. package/dist/host/layout/templates/auth/oauth.js +146 -217
  24. package/dist/host/layout/templates/auth/oauth.js.map +1 -1
  25. package/dist/host/layout/templates/auth/oauthCompletion.d.ts +6 -0
  26. package/dist/host/layout/templates/auth/oauthCompletion.d.ts.map +1 -0
  27. package/dist/host/layout/templates/auth/oauthCompletion.js +147 -0
  28. package/dist/host/layout/templates/auth/oauthCompletion.js.map +1 -0
  29. package/dist/host/layout/templates/auth/oauthState.d.ts +2 -0
  30. package/dist/host/layout/templates/auth/oauthState.d.ts.map +1 -0
  31. package/dist/host/layout/templates/auth/oauthState.js +45 -0
  32. package/dist/host/layout/templates/auth/oauthState.js.map +1 -0
  33. package/dist/host/layout/templates/auth/screen.d.ts +5 -2
  34. package/dist/host/layout/templates/auth/screen.d.ts.map +1 -1
  35. package/dist/host/layout/templates/auth/screen.js +296 -531
  36. package/dist/host/layout/templates/auth/screen.js.map +1 -1
  37. package/dist/host/layout/templates/auth/session.js +6 -6
  38. package/dist/host/layout/templates/auth/signOut.d.ts.map +1 -1
  39. package/dist/host/layout/templates/auth/signOut.js +10 -9
  40. package/dist/host/layout/templates/auth/signOut.js.map +1 -1
  41. package/dist/host/layout/templates/index.d.ts +5 -1
  42. package/dist/host/layout/templates/index.d.ts.map +1 -1
  43. package/dist/host/layout/templates/index.js +5 -1
  44. package/dist/host/layout/templates/index.js.map +1 -1
  45. package/dist/host/layout/templates/rootLayout.d.ts.map +1 -1
  46. package/dist/host/layout/templates/rootLayout.js +9 -149
  47. package/dist/host/layout/templates/rootLayout.js.map +1 -1
  48. package/dist/host/orchestrator/generatedAppFiles.d.ts.map +1 -1
  49. package/dist/host/orchestrator/generatedAppFiles.js +6 -4
  50. package/dist/host/orchestrator/generatedAppFiles.js.map +1 -1
  51. package/dist/host/orchestrator/generatedRouteCleanup.js +1 -1
  52. package/dist/host/orchestrator/generatedRouteCleanup.js.map +1 -1
  53. package/dist/host/orchestrator/scaffolder.js +3 -3
  54. package/dist/host/orchestrator/scaffolder.js.map +1 -1
  55. package/dist/host/orchestrator/templates.d.ts.map +1 -1
  56. package/dist/host/orchestrator/templates.js +54 -52
  57. package/dist/host/orchestrator/templates.js.map +1 -1
  58. package/dist/projectDeployAssetPicker.d.ts +2 -1
  59. package/dist/projectDeployAssetPicker.d.ts.map +1 -1
  60. package/dist/projectDeployAssetPicker.js +27 -21
  61. package/dist/projectDeployAssetPicker.js.map +1 -1
  62. package/package.json +5 -4
@@ -1,350 +1,324 @@
1
1
  import { escapeStringLiteral } from '../../utils/escapeStringLiteral';
2
2
  import { routeNameToGroupedHref } from '../utils/routes';
3
3
  import { toSafeComponentName } from '../utils/strings';
4
- const AUTH_SCREEN_CONTAINER_PADDING = 24;
5
- const AUTH_SCREEN_CARD_MAX_WIDTH = 560;
6
4
  function serializeStringArrayLiteral(values) {
7
5
  return `[${values.map((value) => `'${escapeStringLiteral(value)}'`).join(', ')}]`;
8
6
  }
9
7
  export function getAuthScreenTsx(args) {
10
- const { initialMode, screenName, title, signInRoute, signUpRoute, postSignInRoute, signInIdentifiers, signUpRequiredFields, signUpOptionalFields, signUpPolicy, oauthProviders = [], } = args;
11
- const safeName = toSafeComponentName(screenName);
12
- const oauthEnabled = oauthProviders.length > 0;
13
- const postSignInTarget = routeNameToGroupedHref(postSignInRoute, 'app');
14
- const signUpSessionMessage = signUpPolicy === 'autoSignIn'
15
- ? ` await setStoredAuthSession(result.data);
16
- router.replace(POST_SIGN_IN_ROUTE);`
17
- : ` await clearStoredAuthSession();
18
- router.replace(SIGN_IN_ROUTE);`;
19
- const oauthImport = oauthEnabled
20
- ? `import {
21
- generatedOAuthProviderItems,
22
- startOAuthAuthorization,
23
- } from '@/auth/oauth';\n`
24
- : '';
25
- const oauthState = oauthEnabled
26
- ? ` const [oauthLoadingProvider, setOAuthLoadingProvider] = useState<string | null>(null);\n`
27
- : '';
28
- const oauthHandler = oauthEnabled
29
- ? `
30
- async function handleOAuthProviderPress(providerId: string) {
31
- if (loading || oauthLoadingProvider !== null) return;
32
- setError(null);
33
- setInfo(null);
34
- setOAuthLoadingProvider(providerId);
35
- try {
36
- const outcome = await startOAuthAuthorization(providerId);
37
- if (outcome.status === 'authenticated') {
38
- router.replace(POST_SIGN_IN_ROUTE);
39
- return;
40
- }
41
- if (outcome.status === 'cancelled') {
42
- setInfo(outcome.message);
43
- return;
44
- }
45
- setError(outcome.message);
46
- } catch (caught) {
47
- setError(getErrorMessage(caught));
48
- } finally {
49
- setOAuthLoadingProvider(null);
50
- }
51
- }
52
- `
53
- : '';
54
- const oauthJsx = oauthEnabled
55
- ? `
56
- <OAuthProviderList
57
- disabled={loading}
58
- onProviderPress={handleOAuthProviderPress}
59
- providers={generatedOAuthProviderItems.map((provider) => ({
60
- ...provider,
61
- disabled:
62
- oauthLoadingProvider !== null && oauthLoadingProvider !== provider.id,
63
- loading: oauthLoadingProvider === provider.id,
64
- }))}
65
- />
66
- <View style={styles.separatorRow}>
67
- <View style={[styles.separatorLine, { backgroundColor: theme.colors.border }]} />
68
- <Text emphasis="muted" variant="bodySmall">
69
- or continue with password
70
- </Text>
71
- <View style={[styles.separatorLine, { backgroundColor: theme.colors.border }]} />
72
- </View>
73
- `
8
+ const safeName = toSafeComponentName(args.screenName);
9
+ return `import { GeneratedAuthScreen } from '@/screens/auth-screen';
10
+
11
+ export default function ${safeName}Screen() {
12
+ return <GeneratedAuthScreen initialMode="${args.initialMode}" title="${escapeStringLiteral(args.title ?? args.screenName)}" />;
13
+ }
14
+ `;
15
+ }
16
+ export function getAuthScreenRuntimeTsx(args) {
17
+ const oauthEnabled = (args.oauthProviders?.length ?? 0) > 0;
18
+ const oauthImports = oauthEnabled
19
+ ? `import { generatedOAuthProviderItems, startOAuthAuthorization } from '@/auth/oauth';\n`
74
20
  : '';
75
- const zoraOAuthImport = oauthEnabled ? ' OAuthProviderList,\n' : '';
76
- const formLoading = oauthEnabled ? 'loading || oauthLoadingProvider !== null' : 'loading';
21
+ const oauthZoraImport = oauthEnabled ? ' OAuthProviderList,\n' : '';
22
+ const oauthController = oauthEnabled
23
+ ? getOAuthControllerSource()
24
+ : `
25
+ function handleOAuthProviderPress(): Promise<void> {
26
+ return Promise.resolve();
27
+ }`;
28
+ const oauthView = oauthEnabled ? getOAuthViewSource() : '';
29
+ const formLoading = oauthEnabled
30
+ ? 'controller.loading || controller.oauthLoadingProvider !== null'
31
+ : 'controller.loading';
32
+ const postSignInTarget = routeNameToGroupedHref(args.postSignInRoute, 'app');
77
33
  return `import type { AppManifest } from '@ankhorage/contracts';
78
- import type { AuthIdentifier, AuthSession } from '@ankhorage/contracts/auth';
34
+ import { ManifestProvider } from '@ankhorage/runtime';
79
35
  import {
80
- type AuthIdentifierKind,
81
- ${zoraOAuthImport} SignInForm,
36
+ ${oauthZoraImport} SignInForm,
82
37
  type SignInFormValues,
83
38
  SignUpForm,
84
- type SignUpFormField,
85
39
  type SignUpFormValues,
86
40
  Text,
87
41
  useZoraTheme,
88
42
  } from '@ankhorage/zora';
89
- import { ManifestProvider } from '@ankhorage/runtime';
90
- import { isEmail, isPhone, isUsername } from '@ankhorage/utility/regex';
91
43
  import ankhConfig from '@root/ankh.config.json';
92
44
  import { Stack, useRouter } from 'expo-router';
93
- import React, { useMemo, useState } from 'react';
45
+ import { useMemo, useState } from 'react';
94
46
  import { StyleSheet, View } from 'react-native';
95
47
 
96
48
  import { authAdapter } from '@/auth/adapter';
97
- ${oauthImport}import { clearStoredAuthSession, setStoredAuthSession } from '@/auth/session';
98
-
99
- const SIGN_IN_IDENTIFIERS: string[] = ${serializeStringArrayLiteral(signInIdentifiers)};
100
- const SIGN_UP_REQUIRED_FIELDS: string[] = ${serializeStringArrayLiteral(signUpRequiredFields)};
101
- const SIGN_UP_OPTIONAL_FIELDS: string[] = ${serializeStringArrayLiteral(signUpOptionalFields)};
102
- const SIGN_IN_ROUTE = '${escapeStringLiteral(routeNameToGroupedHref(signInRoute, 'auth'))}';
103
- const SIGN_UP_ROUTE = '${escapeStringLiteral(routeNameToGroupedHref(signUpRoute, 'auth'))}';
49
+ import {
50
+ type AuthSubmitValues,
51
+ buildAuthIdentifierInput,
52
+ buildSignUpFields,
53
+ buildSignUpProfile,
54
+ getFormValue,
55
+ isAuthSession,
56
+ resolveAuthIdentifiers,
57
+ resolveIdentifierFieldDefinition,
58
+ validateAuthSubmitValues,
59
+ } from '@/auth/form';
60
+ ${oauthImports}import { clearStoredAuthSession, setStoredAuthSession } from '@/auth/session';
61
+
62
+ const SIGN_IN_IDENTIFIERS: string[] = ${serializeStringArrayLiteral(args.signInIdentifiers)};
63
+ const SIGN_UP_REQUIRED_FIELDS: string[] = ${serializeStringArrayLiteral(args.signUpRequiredFields)};
64
+ const SIGN_UP_OPTIONAL_FIELDS: string[] = ${serializeStringArrayLiteral(args.signUpOptionalFields)};
65
+ const SIGN_IN_ROUTE = '${escapeStringLiteral(routeNameToGroupedHref(args.signInRoute, 'auth'))}';
66
+ const SIGN_UP_ROUTE = '${escapeStringLiteral(routeNameToGroupedHref(args.signUpRoute, 'auth'))}';
104
67
  const POST_SIGN_IN_ROUTE = '${escapeStringLiteral(postSignInTarget)}';
68
+ const AUTO_SIGN_IN_AFTER_SIGN_UP = ${String(args.signUpPolicy === 'autoSignIn')};
105
69
  const fallbackManifest = ankhConfig as unknown as AppManifest;
106
- const authScreenOptions = {
107
- title: '${escapeStringLiteral(title ?? screenName)}',
108
- };
109
70
 
110
71
  type AuthMode = 'signIn' | 'signUp';
111
72
 
112
- interface AuthSubmitValues {
73
+ interface AuthScreenController {
74
+ authIdentifiers: ReturnType<typeof resolveAuthIdentifiers>;
75
+ error: string | null;
76
+ handleOAuthProviderPress: (providerId: string) => Promise<void>;
77
+ handleSignInSubmit: (values: SignInFormValues) => Promise<void>;
78
+ handleSignUpSubmit: (values: SignUpFormValues) => Promise<void>;
79
+ identifierField: ReturnType<typeof resolveIdentifierFieldDefinition>;
80
+ info: string | null;
81
+ loading: boolean;
113
82
  mode: AuthMode;
114
- identifier: string;
115
- password: string;
116
- firstName: string;
117
- lastName: string;
118
- displayName: string;
83
+ oauthLoadingProvider: string | null;
84
+ showSignIn: () => void;
85
+ showSignUp: () => void;
86
+ signUpFields: ReturnType<typeof buildSignUpFields>;
119
87
  }
120
88
 
121
- function getErrorMessage(caught: unknown): string {
122
- if (caught instanceof Error) return caught.message;
123
- if (typeof caught === 'string') return caught;
124
- return 'Unknown auth error';
89
+ export function GeneratedAuthScreen({
90
+ initialMode,
91
+ title,
92
+ }: {
93
+ initialMode: AuthMode;
94
+ title: string;
95
+ }) {
96
+ const controller = useAuthScreenController(initialMode);
97
+ const { theme } = useZoraTheme();
98
+ return (
99
+ <ManifestProvider manifest={fallbackManifest}>
100
+ <View style={[styles.container, { backgroundColor: theme.colors.background }]}>
101
+ <Stack.Screen options={{ title }} />
102
+ <View
103
+ style={[
104
+ styles.card,
105
+ { backgroundColor: theme.colors.surface, borderColor: theme.colors.border },
106
+ ]}
107
+ >
108
+ <AuthScreenContent borderColor={theme.colors.border} controller={controller} />
109
+ </View>
110
+ </View>
111
+ </ManifestProvider>
112
+ );
125
113
  }
126
114
 
127
- export default function ${safeName}Screen() {
115
+ function useAuthScreenController(initialMode: AuthMode): AuthScreenController {
128
116
  const router = useRouter();
129
- const { theme } = useZoraTheme();
130
-
131
- const [mode, setMode] = useState<AuthMode>('${initialMode}');
117
+ const [mode, setMode] = useState<AuthMode>(initialMode);
132
118
  const [loading, setLoading] = useState(false);
133
- ${oauthState} const [error, setError] = useState<string | null>(null);
119
+ const [oauthLoadingProvider, setOAuthLoadingProvider] = useState<string | null>(null);
120
+ const [error, setError] = useState<string | null>(null);
134
121
  const [info, setInfo] = useState<string | null>(null);
135
-
136
122
  const identifierField = useMemo(() => resolveIdentifierFieldDefinition(SIGN_IN_IDENTIFIERS), []);
137
123
  const authIdentifiers = useMemo(() => resolveAuthIdentifiers(SIGN_IN_IDENTIFIERS), []);
138
124
  const signUpFields = useMemo(
139
125
  () =>
140
126
  buildSignUpFields({
141
127
  identifierField,
142
- configuredFields: unique([...SIGN_UP_REQUIRED_FIELDS, ...SIGN_UP_OPTIONAL_FIELDS]),
128
+ configuredFields: [...new Set([...SIGN_UP_REQUIRED_FIELDS, ...SIGN_UP_OPTIONAL_FIELDS])],
143
129
  requiredFields: SIGN_UP_REQUIRED_FIELDS,
144
130
  }),
145
131
  [identifierField],
146
132
  );
147
-
148
- function showSignIn() {
149
- setMode('signIn');
133
+ const showMode = (nextMode: AuthMode) => {
134
+ setMode(nextMode);
150
135
  setError(null);
151
136
  setInfo(null);
152
- router.replace(SIGN_IN_ROUTE);
153
- }
137
+ router.replace(nextMode === 'signIn' ? SIGN_IN_ROUTE : SIGN_UP_ROUTE);
138
+ };
139
+ const submit = (values: AuthSubmitValues) =>
140
+ submitAuthFormAsync({ router, setError, setInfo, setLoading, values });
141
+ const handleSignInSubmit = (values: SignInFormValues) => submit(createSignInSubmitValues(values));
142
+ const handleSignUpSubmit = (values: SignUpFormValues) => submit(createSignUpSubmitValues(values));${oauthController}
143
+ return {
144
+ authIdentifiers,
145
+ error,
146
+ handleOAuthProviderPress,
147
+ handleSignInSubmit,
148
+ handleSignUpSubmit,
149
+ identifierField,
150
+ info,
151
+ loading,
152
+ mode,
153
+ oauthLoadingProvider,
154
+ showSignIn: () => showMode('signIn'),
155
+ showSignUp: () => showMode('signUp'),
156
+ signUpFields,
157
+ };
158
+ }
154
159
 
155
- function showSignUp() {
156
- setMode('signUp');
157
- setError(null);
158
- setInfo(null);
159
- router.replace(SIGN_UP_ROUTE);
160
- }
160
+ function AuthScreenContent({
161
+ borderColor,
162
+ controller,
163
+ }: {
164
+ borderColor: string;
165
+ controller: AuthScreenController;
166
+ }) {
167
+ return (
168
+ <>
169
+ <Text variant="lead" weight="semiBold">
170
+ {controller.mode === 'signIn' ? 'Sign in' : 'Create account'}
171
+ </Text>
172
+ <Text emphasis="muted" variant="bodySmall">
173
+ {controller.identifierField.helper}
174
+ </Text>${oauthView}
175
+ <AuthForm controller={controller} />
176
+ {controller.info ? (
177
+ <Text color="success" variant="bodySmall">
178
+ {controller.info}
179
+ </Text>
180
+ ) : null}
181
+ </>
182
+ );
183
+ }
161
184
 
162
- async function handleSignInSubmit(values: SignInFormValues) {
163
- await submitAuthForm({
164
- mode: 'signIn',
165
- identifier: values.identifier,
166
- password: values.secret,
167
- firstName: '',
168
- lastName: '',
169
- displayName: '',
170
- });
185
+ function AuthForm({ controller }: { controller: AuthScreenController }) {
186
+ if (controller.mode === 'signIn') {
187
+ return (
188
+ <SignInForm
189
+ error={controller.error}
190
+ identifierLabel={controller.identifierField.label}
191
+ identifiers={controller.authIdentifiers}
192
+ loading={${formLoading}}
193
+ onSignUp={controller.showSignUp}
194
+ onSubmit={controller.handleSignInSubmit}
195
+ signUpLabel="Need an account? Sign up"
196
+ submitLabel="Sign in"
197
+ />
198
+ );
171
199
  }
200
+ return (
201
+ <SignUpForm
202
+ error={controller.error}
203
+ fields={controller.signUpFields}
204
+ loading={${formLoading}}
205
+ onSignIn={controller.showSignIn}
206
+ onSubmit={controller.handleSignUpSubmit}
207
+ signInLabel="Already have an account? Sign in"
208
+ submitLabel="Create account"
209
+ />
210
+ );
211
+ }
172
212
 
173
- async function handleSignUpSubmit(values: SignUpFormValues) {
174
- await submitAuthForm({
175
- mode: 'signUp',
176
- identifier: getFormValue(values, 'identifier'),
177
- password: getFormValue(values, 'password'),
178
- firstName: getFormValue(values, 'firstName'),
179
- lastName: getFormValue(values, 'lastName'),
180
- displayName: getFormValue(values, 'displayName'),
181
- });
213
+ async function submitAuthFormAsync(args: {
214
+ router: ReturnType<typeof useRouter>;
215
+ setError: (message: string | null) => void;
216
+ setInfo: (message: string | null) => void;
217
+ setLoading: (loading: boolean) => void;
218
+ values: AuthSubmitValues;
219
+ }): Promise<void> {
220
+ const { router, setError, setInfo, setLoading, values } = args;
221
+ setError(null);
222
+ setInfo(null);
223
+ const validationError = validateAuthSubmitValues(
224
+ values,
225
+ SIGN_IN_IDENTIFIERS,
226
+ SIGN_UP_REQUIRED_FIELDS,
227
+ );
228
+ if (validationError) {
229
+ setError(validationError);
230
+ return;
182
231
  }
183
- ${oauthHandler}
184
- async function submitAuthForm(values: AuthSubmitValues) {
185
- const { mode, identifier, password, firstName, lastName, displayName } = values;
186
-
187
- setError(null);
188
- setInfo(null);
189
-
190
- const trimmedIdentifier = identifier.trim();
191
- const normalizedPassword = password;
192
- if (!trimmedIdentifier || normalizedPassword.length === 0) {
193
- setError('Enter both credentials before continuing.');
194
- return;
195
- }
196
-
197
- const identifierValidationError = validateIdentifier(trimmedIdentifier, SIGN_IN_IDENTIFIERS);
198
- if (identifierValidationError) {
199
- setError(identifierValidationError);
200
- return;
201
- }
202
-
203
- if (normalizedPassword.length < 6) {
204
- setError('Password must be at least 6 characters.');
205
- return;
206
- }
207
-
208
- if (mode === 'signUp') {
209
- const signUpValidationError = validateSignUpInput({
210
- identifier: trimmedIdentifier,
211
- password: normalizedPassword,
212
- requiredFields: SIGN_UP_REQUIRED_FIELDS,
213
- firstName,
214
- lastName,
215
- displayName,
216
- });
217
- if (signUpValidationError) {
218
- setError(signUpValidationError);
219
- return;
220
- }
221
- }
222
-
223
- const authIdentifier = buildAuthIdentifierInput(trimmedIdentifier, SIGN_IN_IDENTIFIERS);
224
- if (!authIdentifier) {
225
- setError('Unable to resolve the configured sign-in identifier.');
226
- return;
227
- }
228
-
229
- setLoading(true);
230
- try {
231
- if (mode === 'signIn') {
232
- const result = await authAdapter.signIn({
233
- identifier: authIdentifier,
234
- password: normalizedPassword,
235
- });
236
-
237
- if (!result.ok) {
238
- setError(result.error.message);
239
- return;
240
- }
241
-
242
- if (!result.data) {
243
- setError('Sign in succeeded without a session.');
244
- return;
245
- }
246
-
247
- await setStoredAuthSession(result.data);
248
- router.replace(POST_SIGN_IN_ROUTE);
249
- return;
250
- }
251
-
252
- const result = await authAdapter.signUp({
253
- identifier: authIdentifier,
254
- password: normalizedPassword,
255
- profile: buildSignUpProfile({
256
- firstName,
257
- lastName,
258
- displayName,
259
- }),
260
- });
261
-
262
- if (!result.ok) {
263
- setError(result.error.message);
264
- return;
265
- }
232
+ const identifier = buildAuthIdentifierInput(values.identifier, SIGN_IN_IDENTIFIERS);
233
+ if (!identifier) {
234
+ setError('Unable to resolve the configured sign-in identifier.');
235
+ return;
236
+ }
237
+ setLoading(true);
238
+ try {
239
+ if (values.mode === 'signIn') await signInAsync(identifier, values.password, router, setError);
240
+ else await signUpAsync(identifier, values, router, setError);
241
+ } catch (caught) {
242
+ setError(getErrorMessage(caught));
243
+ } finally {
244
+ setLoading(false);
245
+ }
246
+ }
266
247
 
267
- if (isAuthSession(result.data)) {
268
- ${signUpSessionMessage}
269
- return;
270
- }
248
+ async function signInAsync(
249
+ identifier: NonNullable<ReturnType<typeof buildAuthIdentifierInput>>,
250
+ password: string,
251
+ router: ReturnType<typeof useRouter>,
252
+ setError: (message: string) => void,
253
+ ): Promise<void> {
254
+ const result = await authAdapter.signIn({ identifier, password });
255
+ if (!result.ok) return setError(result.error.message);
256
+ if (!result.data) return setError('Sign in succeeded without a session.');
257
+ await setStoredAuthSession(result.data);
258
+ router.replace(POST_SIGN_IN_ROUTE);
259
+ }
271
260
 
272
- router.replace(SIGN_IN_ROUTE);
273
- } catch (caught) {
274
- setError(getErrorMessage(caught));
275
- } finally {
276
- setLoading(false);
277
- }
261
+ async function signUpAsync(
262
+ identifier: NonNullable<ReturnType<typeof buildAuthIdentifierInput>>,
263
+ values: AuthSubmitValues,
264
+ router: ReturnType<typeof useRouter>,
265
+ setError: (message: string) => void,
266
+ ): Promise<void> {
267
+ const result = await authAdapter.signUp({
268
+ identifier,
269
+ password: values.password,
270
+ profile: buildSignUpProfile(values),
271
+ });
272
+ if (!result.ok) return setError(result.error.message);
273
+ if (isAuthSession(result.data) && AUTO_SIGN_IN_AFTER_SIGN_UP) {
274
+ await setStoredAuthSession(result.data);
275
+ router.replace(POST_SIGN_IN_ROUTE);
276
+ return;
278
277
  }
278
+ await clearStoredAuthSession();
279
+ router.replace(SIGN_IN_ROUTE);
280
+ }
279
281
 
280
- return (
281
- <ManifestProvider manifest={fallbackManifest}>
282
- <View
283
- style={{
284
- flex: 1,
285
- backgroundColor: theme.colors.background,
286
- justifyContent: 'center',
287
- alignItems: 'center',
288
- padding: ${AUTH_SCREEN_CONTAINER_PADDING},
289
- }}
290
- >
291
- <Stack.Screen options={authScreenOptions} />
292
- <View
293
- style={[
294
- styles.card,
295
- {
296
- backgroundColor: theme.colors.surface,
297
- borderColor: theme.colors.border,
298
- },
299
- ]}
300
- >
301
- <Text variant="lead" weight="semiBold">
302
- {mode === 'signIn' ? 'Sign in' : 'Create account'}
303
- </Text>
304
- <Text emphasis="muted" variant="bodySmall">
305
- {identifierField.helper}
306
- </Text>
307
- ${oauthJsx}
308
- {mode === 'signIn' ? (
309
- <SignInForm
310
- error={error}
311
- identifierLabel={identifierField.label}
312
- identifiers={authIdentifiers}
313
- loading={${formLoading}}
314
- onSignUp={showSignUp}
315
- onSubmit={handleSignInSubmit}
316
- signUpLabel="Need an account? Sign up"
317
- submitLabel="Sign in"
318
- />
319
- ) : (
320
- <SignUpForm
321
- error={error}
322
- fields={signUpFields}
323
- loading={${formLoading}}
324
- onSignIn={showSignIn}
325
- onSubmit={handleSignUpSubmit}
326
- signInLabel="Already have an account? Sign in"
327
- submitLabel="Create account"
328
- />
329
- )}
282
+ function createSignInSubmitValues(values: SignInFormValues): AuthSubmitValues {
283
+ return {
284
+ mode: 'signIn',
285
+ identifier: values.identifier,
286
+ password: values.secret,
287
+ firstName: '',
288
+ lastName: '',
289
+ displayName: '',
290
+ };
291
+ }
330
292
 
331
- {info ? (
332
- <Text color="success" variant="bodySmall">
333
- {info}
334
- </Text>
335
- ) : null}
336
- </View>
337
- </View>
338
- </ManifestProvider>
339
- );
293
+ function createSignUpSubmitValues(values: SignUpFormValues): AuthSubmitValues {
294
+ return {
295
+ mode: 'signUp',
296
+ identifier: getFormValue(values, 'identifier'),
297
+ password: getFormValue(values, 'password'),
298
+ firstName: getFormValue(values, 'firstName'),
299
+ lastName: getFormValue(values, 'lastName'),
300
+ displayName: getFormValue(values, 'displayName'),
301
+ };
302
+ }
303
+
304
+ function getErrorMessage(caught: unknown): string {
305
+ if (caught instanceof Error) return caught.message;
306
+ if (typeof caught === 'string') return caught;
307
+ return 'Unknown auth error';
340
308
  }
341
309
 
342
310
  const styles = StyleSheet.create({
311
+ container: {
312
+ alignItems: 'center',
313
+ flex: 1,
314
+ justifyContent: 'center',
315
+ padding: 24,
316
+ },
343
317
  card: {
344
318
  borderRadius: 24,
345
319
  borderWidth: 1,
346
320
  gap: 16,
347
- maxWidth: ${AUTH_SCREEN_CARD_MAX_WIDTH},
321
+ maxWidth: 560,
348
322
  padding: 24,
349
323
  width: '100%',
350
324
  },
@@ -358,255 +332,46 @@ const styles = StyleSheet.create({
358
332
  gap: 12,
359
333
  },
360
334
  });
361
-
362
- function buildAuthIdentifierInput(
363
- identifier: string,
364
- identifiers: string[],
365
- ): AuthIdentifier | null {
366
- const normalizedIdentifier = identifier.trim();
367
- const resolvedIdentifiers = resolveAuthIdentifiers(identifiers);
368
-
369
- if (resolvedIdentifiers.includes('email') && isEmail(normalizedIdentifier)) {
370
- return { kind: 'email' as const, value: normalizedIdentifier };
371
- }
372
- if (resolvedIdentifiers.includes('phone') && isPhone(normalizedIdentifier)) {
373
- return { kind: 'phone' as const, value: normalizedIdentifier };
374
- }
375
- if (resolvedIdentifiers.includes('username') && isUsername(normalizedIdentifier)) {
376
- return { kind: 'username' as const, value: normalizedIdentifier };
377
- }
378
-
379
- const [fallbackKind] = resolvedIdentifiers;
380
- return fallbackKind ? { kind: fallbackKind, value: normalizedIdentifier } : null;
381
- }
382
-
383
- function buildSignUpProfile(args: { firstName: string; lastName: string; displayName: string }) {
384
- const { firstName, lastName, displayName } = args;
385
- const profile: Record<string, string> = {};
386
- if (firstName.trim()) profile.firstName = firstName.trim();
387
- if (lastName.trim()) profile.lastName = lastName.trim();
388
- if (displayName.trim()) profile.displayName = displayName.trim();
389
- return Object.keys(profile).length > 0 ? profile : undefined;
390
- }
391
-
392
- function isAuthSession(value: unknown): value is AuthSession {
393
- if (!isRecord(value)) return false;
394
- const { accessToken, user } = value;
395
- if (typeof accessToken !== 'string' || accessToken.length === 0) return false;
396
- if (!isRecord(user)) return false;
397
- const { id: userId } = user;
398
- return typeof userId === 'string' && userId.length > 0;
399
- }
400
-
401
- function isRecord(value: unknown): value is Record<string, unknown> {
402
- return typeof value === 'object' && value !== null && !Array.isArray(value);
403
- }
404
-
405
- function getFormValue(values: SignUpFormValues, name: string): string {
406
- return values[name] ?? '';
407
- }
408
-
409
- function buildSignUpFields(args: {
410
- identifierField: ReturnType<typeof resolveIdentifierFieldDefinition>;
411
- configuredFields: string[];
412
- requiredFields: string[];
413
- }): SignUpFormField[] {
414
- const { identifierField, configuredFields, requiredFields } = args;
415
- const fields: SignUpFormField[] = [
416
- {
417
- name: 'identifier',
418
- label: identifierField.label,
419
- helperText: identifierField.helper,
420
- placeholder: identifierField.placeholder,
421
- type: identifierField.type,
422
- autoCapitalize: 'none',
423
- keyboardType: identifierField.keyboardType,
424
- required:
425
- hasConfiguredSignUpField(requiredFields, 'email') ||
426
- hasConfiguredSignUpField(requiredFields, 'phone') ||
427
- hasConfiguredSignUpField(requiredFields, 'username'),
428
- },
429
- {
430
- name: 'password',
431
- label: 'Password',
432
- type: 'password',
433
- required: hasConfiguredSignUpField(requiredFields, 'password'),
434
- },
435
- ];
436
-
437
- if (hasConfiguredSignUpField(configuredFields, 'firstname')) {
438
- fields.push({
439
- name: 'firstName',
440
- label: 'First name',
441
- type: 'text',
442
- required: hasConfiguredSignUpField(requiredFields, 'firstname'),
443
- });
444
- }
445
- if (hasConfiguredSignUpField(configuredFields, 'lastname')) {
446
- fields.push({
447
- name: 'lastName',
448
- label: 'Last name',
449
- type: 'text',
450
- required: hasConfiguredSignUpField(requiredFields, 'lastname'),
451
- });
452
- }
453
- if (hasConfiguredSignUpField(configuredFields, 'displayname')) {
454
- fields.push({
455
- name: 'displayName',
456
- label: 'Display name',
457
- type: 'text',
458
- required: hasConfiguredSignUpField(requiredFields, 'displayname'),
459
- });
460
- }
461
- return fields;
462
- }
463
-
464
- function hasConfiguredSignUpField(fields: string[], field: string): boolean {
465
- return fields.some((value) => value.trim().toLowerCase() === field);
466
- }
467
-
468
- function resolveAuthIdentifiers(identifiers: string[]): AuthIdentifierKind[] {
469
- const resolved: AuthIdentifierKind[] = [];
470
- for (const identifier of identifiers) {
471
- const normalized = identifier.trim().toLowerCase();
472
- if (isAuthIdentifierKind(normalized) && !resolved.includes(normalized)) {
473
- resolved.push(normalized);
474
- }
475
- }
476
- return resolved.length > 0 ? resolved : ['email'];
477
- }
478
-
479
- function isAuthIdentifierKind(value: string): value is AuthIdentifierKind {
480
- return value === 'email' || value === 'phone' || value === 'username';
481
- }
482
-
483
- function resolveIdentifierFieldDefinition(identifiers: string[]) {
484
- const set = new Set(identifiers.map((identifier) => identifier.trim().toLowerCase()));
485
- const supportsEmail = set.has('email');
486
- const supportsPhone = set.has('phone');
487
- const supportsUsername = set.has('username');
488
-
489
- if (supportsEmail && !supportsPhone && !supportsUsername) {
490
- return {
491
- label: 'Email',
492
- placeholder: 'hello@example.com',
493
- helper: 'Use your email to continue.',
494
- type: 'email' as const,
495
- keyboardType: 'email-address' as const,
496
- };
497
- }
498
- if (supportsPhone && !supportsEmail && !supportsUsername) {
499
- return {
500
- label: 'Phone',
501
- placeholder: '+1 555 123 4567',
502
- helper: 'Use your phone to continue.',
503
- type: 'tel' as const,
504
- keyboardType: 'phone-pad' as const,
505
- };
506
- }
507
- if (supportsUsername && !supportsEmail && !supportsPhone) {
508
- return {
509
- label: 'Username',
510
- placeholder: 'your-username',
511
- helper: 'Use your username to continue.',
512
- type: 'text' as const,
513
- keyboardType: 'default' as const,
514
- };
515
- }
516
- return {
517
- label: supportsUsername ? 'Identifier' : 'Email or phone',
518
- placeholder: 'Email or phone',
519
- helper: 'Use your configured identifier to continue.',
520
- type: 'text' as const,
521
- keyboardType: 'default' as const,
522
- };
523
- }
524
-
525
- function validateIdentifier(identifier: string, identifiers: string[]): string | null {
526
- const normalizedIdentifier = identifier.trim();
527
- const set = new Set(identifiers.map((entry) => entry.trim().toLowerCase()));
528
- const allowed: { label: string; matches: boolean }[] = [];
529
- if (set.has('email')) allowed.push({ label: 'email address', matches: isEmail(normalizedIdentifier) });
530
- if (set.has('phone')) allowed.push({ label: 'phone number', matches: isPhone(normalizedIdentifier) });
531
- if (set.has('username')) allowed.push({ label: 'username', matches: isUsername(normalizedIdentifier) });
532
- if (allowed.length === 0 || allowed.some((entry) => entry.matches)) return null;
533
- if (allowed.length === 1 && allowed[0]?.label === 'username') {
534
- return 'Username must be at least 3 characters and use letters, numbers, dot, underscore, or dash.';
535
- }
536
- if (allowed.length === 1) return 'Use a valid ' + (allowed[0]?.label ?? 'identifier') + '.';
537
- if (allowed.length === 2) {
538
- return (
539
- 'Use a valid ' +
540
- (allowed[0]?.label ?? 'identifier') +
541
- ' or ' +
542
- (allowed[1]?.label ?? 'identifier') +
543
- '.'
544
- );
545
- }
546
- return (
547
- 'Use a valid ' +
548
- (allowed[0]?.label ?? 'identifier') +
549
- ', ' +
550
- (allowed[1]?.label ?? 'identifier') +
551
- ', or ' +
552
- (allowed[2]?.label ?? 'identifier') +
553
- '.'
554
- );
335
+ `;
555
336
  }
556
-
557
- function validateSignUpInput(args: {
558
- identifier: string;
559
- password: string;
560
- requiredFields: string[];
561
- firstName: string;
562
- lastName: string;
563
- displayName: string;
564
- }): string | null {
565
- const { identifier, password, requiredFields, firstName, lastName, displayName } = args;
566
- const missing: string[] = [];
567
- for (const field of requiredFields) {
568
- const normalized = field.trim().toLowerCase();
569
- switch (normalized) {
570
- case 'email':
571
- case 'username':
572
- case 'phone':
573
- if (!identifier.trim()) missing.push(fieldLabel(normalized));
574
- break;
575
- case 'password':
576
- if (password.length === 0) missing.push('password');
577
- break;
578
- case 'firstname':
579
- if (!firstName.trim()) missing.push('first name');
580
- break;
581
- case 'lastname':
582
- if (!lastName.trim()) missing.push('last name');
583
- break;
584
- case 'displayname':
585
- if (!displayName.trim()) missing.push('display name');
586
- break;
587
- default:
588
- break;
337
+ function getOAuthControllerSource() {
338
+ return `
339
+ async function handleOAuthProviderPress(providerId: string) {
340
+ if (loading || oauthLoadingProvider !== null) return;
341
+ setError(null);
342
+ setInfo(null);
343
+ setOAuthLoadingProvider(providerId);
344
+ try {
345
+ const outcome = await startOAuthAuthorization(providerId);
346
+ if (outcome.status === 'authenticated') router.replace(POST_SIGN_IN_ROUTE);
347
+ else if (outcome.status === 'cancelled') setInfo(outcome.message);
348
+ else setError(outcome.message);
349
+ } catch (caught) {
350
+ setError(getErrorMessage(caught));
351
+ } finally {
352
+ setOAuthLoadingProvider(null);
589
353
  }
590
- }
591
- return missing.length > 0 ? 'Complete required fields: ' + missing.join(', ') + '.' : null;
354
+ }`;
592
355
  }
593
-
594
- function fieldLabel(normalized: string): string {
595
- switch (normalized) {
596
- case 'email':
597
- return 'email';
598
- case 'phone':
599
- return 'phone';
600
- case 'username':
601
- return 'username';
602
- default:
603
- return normalized;
604
- }
605
- }
606
-
607
- function unique(values: string[]): string[] {
608
- return [...new Set(values)];
609
- }
610
- `;
356
+ function getOAuthViewSource() {
357
+ return `
358
+ <OAuthProviderList
359
+ disabled={controller.loading}
360
+ onProviderPress={controller.handleOAuthProviderPress}
361
+ providers={generatedOAuthProviderItems.map((provider) => ({
362
+ ...provider,
363
+ disabled:
364
+ controller.oauthLoadingProvider !== null &&
365
+ controller.oauthLoadingProvider !== provider.id,
366
+ loading: controller.oauthLoadingProvider === provider.id,
367
+ }))}
368
+ />
369
+ <View style={styles.separatorRow}>
370
+ <View style={[styles.separatorLine, { backgroundColor: borderColor }]} />
371
+ <Text emphasis="muted" variant="bodySmall">
372
+ or continue with password
373
+ </Text>
374
+ <View style={[styles.separatorLine, { backgroundColor: borderColor }]} />
375
+ </View>`;
611
376
  }
612
377
  //# sourceMappingURL=screen.js.map