@ankhorage/studio 2.0.2 → 2.0.4

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