@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.
- package/dist/host/layout/auth/resolveAuthLayoutPlan.d.ts +1 -1
- package/dist/host/layout/auth/resolveAuthLayoutPlan.d.ts.map +1 -1
- package/dist/host/layout/auth/resolveAuthLayoutPlan.js +24 -1
- package/dist/host/layout/auth/resolveAuthLayoutPlan.js.map +1 -1
- package/dist/host/layout/layoutGenerator.d.ts.map +1 -1
- package/dist/host/layout/layoutGenerator.js +38 -13
- package/dist/host/layout/layoutGenerator.js.map +1 -1
- package/dist/host/layout/templates/auth/adapter.d.ts.map +1 -1
- package/dist/host/layout/templates/auth/adapter.js +4 -1
- package/dist/host/layout/templates/auth/adapter.js.map +1 -1
- package/dist/host/layout/templates/auth/callback.d.ts.map +1 -1
- package/dist/host/layout/templates/auth/callback.js +78 -37
- package/dist/host/layout/templates/auth/callback.js.map +1 -1
- package/dist/host/layout/templates/auth/form.d.ts +2 -0
- package/dist/host/layout/templates/auth/form.d.ts.map +1 -0
- package/dist/host/layout/templates/auth/form.js +238 -0
- package/dist/host/layout/templates/auth/form.js.map +1 -0
- package/dist/host/layout/templates/auth/navigation.d.ts +10 -0
- package/dist/host/layout/templates/auth/navigation.d.ts.map +1 -0
- package/dist/host/layout/templates/auth/navigation.js +183 -0
- package/dist/host/layout/templates/auth/navigation.js.map +1 -0
- package/dist/host/layout/templates/auth/oauth.d.ts.map +1 -1
- package/dist/host/layout/templates/auth/oauth.js +112 -196
- package/dist/host/layout/templates/auth/oauth.js.map +1 -1
- package/dist/host/layout/templates/auth/oauthCompletion.d.ts +6 -0
- package/dist/host/layout/templates/auth/oauthCompletion.d.ts.map +1 -0
- package/dist/host/layout/templates/auth/oauthCompletion.js +147 -0
- package/dist/host/layout/templates/auth/oauthCompletion.js.map +1 -0
- package/dist/host/layout/templates/auth/oauthState.d.ts +2 -0
- package/dist/host/layout/templates/auth/oauthState.d.ts.map +1 -0
- package/dist/host/layout/templates/auth/oauthState.js +45 -0
- package/dist/host/layout/templates/auth/oauthState.js.map +1 -0
- package/dist/host/layout/templates/auth/screen.d.ts +5 -2
- package/dist/host/layout/templates/auth/screen.d.ts.map +1 -1
- package/dist/host/layout/templates/auth/screen.js +295 -529
- package/dist/host/layout/templates/auth/screen.js.map +1 -1
- package/dist/host/layout/templates/auth/session.js +5 -5
- package/dist/host/layout/templates/auth/signOut.d.ts.map +1 -1
- package/dist/host/layout/templates/auth/signOut.js +10 -9
- package/dist/host/layout/templates/auth/signOut.js.map +1 -1
- package/dist/host/layout/templates/index.d.ts +5 -1
- package/dist/host/layout/templates/index.d.ts.map +1 -1
- package/dist/host/layout/templates/index.js +5 -1
- package/dist/host/layout/templates/index.js.map +1 -1
- package/dist/host/layout/templates/rootLayout.d.ts.map +1 -1
- package/dist/host/layout/templates/rootLayout.js +8 -147
- package/dist/host/layout/templates/rootLayout.js.map +1 -1
- package/dist/host/orchestrator/templates.d.ts.map +1 -1
- package/dist/host/orchestrator/templates.js +11 -13
- package/dist/host/orchestrator/templates.js.map +1 -1
- 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
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
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
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
34
|
+
import { ManifestProvider } from '@ankhorage/runtime';
|
|
75
35
|
import {
|
|
76
|
-
|
|
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
|
|
45
|
+
import { useMemo, useState } from 'react';
|
|
90
46
|
import { StyleSheet, View } from 'react-native';
|
|
91
47
|
|
|
92
48
|
import { authAdapter } from '@/auth/adapter';
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
displayName: string;
|
|
83
|
+
oauthLoadingProvider: string | null;
|
|
84
|
+
showSignIn: () => void;
|
|
85
|
+
showSignUp: () => void;
|
|
86
|
+
signUpFields: ReturnType<typeof buildSignUpFields>;
|
|
115
87
|
}
|
|
116
88
|
|
|
117
|
-
function
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
115
|
+
function useAuthScreenController(initialMode: AuthMode): AuthScreenController {
|
|
124
116
|
const router = useRouter();
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
const [mode, setMode] = useState<AuthMode>('${initialMode}');
|
|
117
|
+
const [mode, setMode] = useState<AuthMode>(initialMode);
|
|
128
118
|
const [loading, setLoading] = useState(false);
|
|
129
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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:
|
|
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
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|