@rownd/react-native 0.1.1

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 (175) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +213 -0
  3. package/android/build.gradle +59 -0
  4. package/android/src/main/AndroidManifest.xml +4 -0
  5. package/android/src/main/java/com/reactnative/ReactNativePackage.java +22 -0
  6. package/android/src/main/java/com/reactnative/ReactNativeViewManager.java +31 -0
  7. package/ios/ReactNative.xcodeproj/project.pbxproj +282 -0
  8. package/ios/ReactNative.xcodeproj/project.xcworkspace/contents.xcworkspacedata +4 -0
  9. package/ios/ReactNativeViewManager.m +34 -0
  10. package/lib/commonjs/assets/images/checkmark--filled.svg +12 -0
  11. package/lib/commonjs/assets/images/email-verify-waiting.svg +36 -0
  12. package/lib/commonjs/assets/images/phone-verify-waiting.svg +26 -0
  13. package/lib/commonjs/components/AuthenticatedComponent.js +35 -0
  14. package/lib/commonjs/components/AuthenticatedComponent.js.map +1 -0
  15. package/lib/commonjs/components/AutoSigninDialog.js +119 -0
  16. package/lib/commonjs/components/AutoSigninDialog.js.map +1 -0
  17. package/lib/commonjs/components/DefaultContext.js +269 -0
  18. package/lib/commonjs/components/DefaultContext.js.map +1 -0
  19. package/lib/commonjs/components/GlobalContext.js +340 -0
  20. package/lib/commonjs/components/GlobalContext.js.map +1 -0
  21. package/lib/commonjs/components/RowndComponents.js +29 -0
  22. package/lib/commonjs/components/RowndComponents.js.map +1 -0
  23. package/lib/commonjs/components/RowndProvider.js +55 -0
  24. package/lib/commonjs/components/RowndProvider.js.map +1 -0
  25. package/lib/commonjs/components/SignIn.js +622 -0
  26. package/lib/commonjs/components/SignIn.js.map +1 -0
  27. package/lib/commonjs/data/actions.js +26 -0
  28. package/lib/commonjs/data/actions.js.map +1 -0
  29. package/lib/commonjs/hooks/api.js +157 -0
  30. package/lib/commonjs/hooks/api.js.map +1 -0
  31. package/lib/commonjs/hooks/debounce.js +38 -0
  32. package/lib/commonjs/hooks/debounce.js.map +1 -0
  33. package/lib/commonjs/hooks/fingerprint.js +176 -0
  34. package/lib/commonjs/hooks/fingerprint.js.map +1 -0
  35. package/lib/commonjs/hooks/index.js +48 -0
  36. package/lib/commonjs/hooks/index.js.map +1 -0
  37. package/lib/commonjs/hooks/interval.js +31 -0
  38. package/lib/commonjs/hooks/interval.js.map +1 -0
  39. package/lib/commonjs/hooks/nav.js +39 -0
  40. package/lib/commonjs/hooks/nav.js.map +1 -0
  41. package/lib/commonjs/hooks/rownd.js +163 -0
  42. package/lib/commonjs/hooks/rownd.js.map +1 -0
  43. package/lib/commonjs/index.js +32 -0
  44. package/lib/commonjs/index.js.map +1 -0
  45. package/lib/commonjs/index.tsx.bak +26 -0
  46. package/lib/commonjs/types.js +2 -0
  47. package/lib/commonjs/types.js.map +1 -0
  48. package/lib/commonjs/utils/config.js +28 -0
  49. package/lib/commonjs/utils/config.js.map +1 -0
  50. package/lib/commonjs/utils/events.js +57 -0
  51. package/lib/commonjs/utils/events.js.map +1 -0
  52. package/lib/commonjs/utils/form.js +46 -0
  53. package/lib/commonjs/utils/form.js.map +1 -0
  54. package/lib/commonjs/utils/queue.js +117 -0
  55. package/lib/commonjs/utils/queue.js.map +1 -0
  56. package/lib/commonjs/utils/storage.js +15 -0
  57. package/lib/commonjs/utils/storage.js.map +1 -0
  58. package/lib/commonjs/utils/tailwind.js +17 -0
  59. package/lib/commonjs/utils/tailwind.js.map +1 -0
  60. package/lib/commonjs/utils/tokens.js +35 -0
  61. package/lib/commonjs/utils/tokens.js.map +1 -0
  62. package/lib/commonjs/utils/user-data.js +21 -0
  63. package/lib/commonjs/utils/user-data.js.map +1 -0
  64. package/lib/module/assets/images/checkmark--filled.svg +12 -0
  65. package/lib/module/assets/images/email-verify-waiting.svg +36 -0
  66. package/lib/module/assets/images/phone-verify-waiting.svg +26 -0
  67. package/lib/module/components/AuthenticatedComponent.js +24 -0
  68. package/lib/module/components/AuthenticatedComponent.js.map +1 -0
  69. package/lib/module/components/AutoSigninDialog.js +100 -0
  70. package/lib/module/components/AutoSigninDialog.js.map +1 -0
  71. package/lib/module/components/DefaultContext.js +244 -0
  72. package/lib/module/components/DefaultContext.js.map +1 -0
  73. package/lib/module/components/GlobalContext.js +318 -0
  74. package/lib/module/components/GlobalContext.js.map +1 -0
  75. package/lib/module/components/RowndComponents.js +14 -0
  76. package/lib/module/components/RowndComponents.js.map +1 -0
  77. package/lib/module/components/RowndProvider.js +39 -0
  78. package/lib/module/components/RowndProvider.js.map +1 -0
  79. package/lib/module/components/SignIn.js +593 -0
  80. package/lib/module/components/SignIn.js.map +1 -0
  81. package/lib/module/data/actions.js +19 -0
  82. package/lib/module/data/actions.js.map +1 -0
  83. package/lib/module/hooks/api.js +138 -0
  84. package/lib/module/hooks/api.js.map +1 -0
  85. package/lib/module/hooks/debounce.js +29 -0
  86. package/lib/module/hooks/debounce.js.map +1 -0
  87. package/lib/module/hooks/fingerprint.js +157 -0
  88. package/lib/module/hooks/fingerprint.js.map +1 -0
  89. package/lib/module/hooks/index.js +7 -0
  90. package/lib/module/hooks/index.js.map +1 -0
  91. package/lib/module/hooks/interval.js +23 -0
  92. package/lib/module/hooks/interval.js.map +1 -0
  93. package/lib/module/hooks/nav.js +30 -0
  94. package/lib/module/hooks/nav.js.map +1 -0
  95. package/lib/module/hooks/rownd.js +148 -0
  96. package/lib/module/hooks/rownd.js.map +1 -0
  97. package/lib/module/index.js +6 -0
  98. package/lib/module/index.js.map +1 -0
  99. package/lib/module/index.tsx.bak +26 -0
  100. package/lib/module/types.js +2 -0
  101. package/lib/module/types.js.map +1 -0
  102. package/lib/module/utils/config.js +17 -0
  103. package/lib/module/utils/config.js.map +1 -0
  104. package/lib/module/utils/events.js +45 -0
  105. package/lib/module/utils/events.js.map +1 -0
  106. package/lib/module/utils/form.js +34 -0
  107. package/lib/module/utils/form.js.map +1 -0
  108. package/lib/module/utils/queue.js +109 -0
  109. package/lib/module/utils/queue.js.map +1 -0
  110. package/lib/module/utils/storage.js +6 -0
  111. package/lib/module/utils/storage.js.map +1 -0
  112. package/lib/module/utils/tailwind.js +5 -0
  113. package/lib/module/utils/tailwind.js.map +1 -0
  114. package/lib/module/utils/tokens.js +24 -0
  115. package/lib/module/utils/tokens.js.map +1 -0
  116. package/lib/module/utils/user-data.js +14 -0
  117. package/lib/module/utils/user-data.js.map +1 -0
  118. package/lib/typescript/example2/App.d.ts +11 -0
  119. package/lib/typescript/src/components/AuthenticatedComponent.d.ts +7 -0
  120. package/lib/typescript/src/components/AutoSigninDialog.d.ts +1 -0
  121. package/lib/typescript/src/components/DefaultContext.d.ts +12 -0
  122. package/lib/typescript/src/components/GlobalContext.d.ts +111 -0
  123. package/lib/typescript/src/components/RowndComponents.d.ts +1 -0
  124. package/lib/typescript/src/components/RowndProvider.d.ts +8 -0
  125. package/lib/typescript/src/components/SignIn.d.ts +1 -0
  126. package/lib/typescript/src/data/actions.d.ts +20 -0
  127. package/lib/typescript/src/hooks/api.d.ts +12 -0
  128. package/lib/typescript/src/hooks/debounce.d.ts +5 -0
  129. package/lib/typescript/src/hooks/fingerprint.d.ts +12 -0
  130. package/lib/typescript/src/hooks/index.d.ts +6 -0
  131. package/lib/typescript/src/hooks/interval.d.ts +2 -0
  132. package/lib/typescript/src/hooks/nav.d.ts +6 -0
  133. package/lib/typescript/src/hooks/rownd.d.ts +37 -0
  134. package/lib/typescript/src/index.d.ts +4 -0
  135. package/lib/typescript/src/types.d.ts +26 -0
  136. package/lib/typescript/src/utils/config.d.ts +18 -0
  137. package/lib/typescript/src/utils/events.d.ts +22 -0
  138. package/lib/typescript/src/utils/form.d.ts +17 -0
  139. package/lib/typescript/src/utils/queue.d.ts +21 -0
  140. package/lib/typescript/src/utils/storage.d.ts +3 -0
  141. package/lib/typescript/src/utils/tailwind.d.ts +2 -0
  142. package/lib/typescript/src/utils/tokens.d.ts +4 -0
  143. package/lib/typescript/src/utils/user-data.d.ts +3 -0
  144. package/lib/typescript/tailwind.config.d.ts +10 -0
  145. package/package.json +177 -0
  146. package/react-native.podspec +19 -0
  147. package/src/assets/images/checkmark--filled.svg +12 -0
  148. package/src/assets/images/email-verify-waiting.svg +36 -0
  149. package/src/assets/images/phone-verify-waiting.svg +26 -0
  150. package/src/components/AuthenticatedComponent.tsx +30 -0
  151. package/src/components/AutoSigninDialog.tsx +125 -0
  152. package/src/components/DefaultContext.tsx +278 -0
  153. package/src/components/GlobalContext.tsx +485 -0
  154. package/src/components/RowndComponents.tsx +21 -0
  155. package/src/components/RowndProvider.tsx +56 -0
  156. package/src/components/SignIn.tsx +770 -0
  157. package/src/data/actions.ts +21 -0
  158. package/src/hooks/api.ts +163 -0
  159. package/src/hooks/debounce.ts +36 -0
  160. package/src/hooks/fingerprint.ts +217 -0
  161. package/src/hooks/index.ts +7 -0
  162. package/src/hooks/interval.ts +25 -0
  163. package/src/hooks/nav.tsx +29 -0
  164. package/src/hooks/rownd.ts +184 -0
  165. package/src/index.tsx +6 -0
  166. package/src/index.tsx.bak +26 -0
  167. package/src/types.ts +27 -0
  168. package/src/utils/config.ts +36 -0
  169. package/src/utils/events.ts +54 -0
  170. package/src/utils/form.tsx +64 -0
  171. package/src/utils/queue.ts +75 -0
  172. package/src/utils/storage.ts +7 -0
  173. package/src/utils/tailwind.ts +6 -0
  174. package/src/utils/tokens.ts +26 -0
  175. package/src/utils/user-data.ts +15 -0
@@ -0,0 +1,770 @@
1
+ import * as Linking from 'expo-linking';
2
+ import React, {
3
+ useCallback,
4
+ useEffect,
5
+ useMemo,
6
+ useReducer,
7
+ useRef,
8
+ useState,
9
+ } from 'react';
10
+ import { differenceInMinutes } from 'date-fns';
11
+ import {
12
+ View,
13
+ Text,
14
+ StyleSheet,
15
+ Pressable,
16
+ Image,
17
+ ActivityIndicator,
18
+ } from 'react-native';
19
+ import { SvgCssUri } from 'react-native-svg';
20
+ import tw from '../utils/tailwind';
21
+ import phone, { type PhoneResult } from 'phone';
22
+ import jwt_decode from 'jwt-decode';
23
+ import {
24
+ BottomSheetBackdrop,
25
+ BottomSheetBackdropProps,
26
+ BottomSheetModal,
27
+ BottomSheetTextInput,
28
+ } from '@gorhom/bottom-sheet';
29
+
30
+ import { useApi, useInterval, useNav, useDeviceFingerprint } from '../hooks';
31
+ import { useGlobalContext } from './GlobalContext';
32
+ import { ActionType } from '../data/actions';
33
+ import { renderField } from '../utils/form';
34
+
35
+ // Image imports
36
+ import ImageEmailVerifyWaiting from '../assets/images/email-verify-waiting.svg';
37
+ import ImagePhoneVerifyWaiting from '../assets/images/phone-verify-waiting.svg';
38
+ import ImageCheckmarkFilled from '../assets/images/checkmark--filled.svg';
39
+
40
+ enum LoginStep {
41
+ INIT = 'init',
42
+ WAITING = 'waiting',
43
+ SUCCESS = 'success',
44
+ FAILURE = 'failure',
45
+ ERROR = 'error',
46
+ }
47
+
48
+ type LoginInitBody = {
49
+ challenge_id: string;
50
+ message: string;
51
+ auth_tokens?: {
52
+ access_token: string;
53
+ refresh_token: string;
54
+ };
55
+ registration_status: string;
56
+ init_data?: Record<string, any>;
57
+ };
58
+
59
+ type LoginSuccessBody = {
60
+ access_token: string;
61
+ refresh_token: string;
62
+ app_user_id: string;
63
+ app_id: string;
64
+ status: string;
65
+ };
66
+
67
+ enum LoginVerificationStatus {
68
+ PENDING = 'pending',
69
+ EXPIRED = 'expired',
70
+ VERIFIED = 'verified',
71
+ }
72
+
73
+ export function SignIn() {
74
+ const navTo = useNav();
75
+ const { getFingerprint, getChallengeIfPresent, clearFingerprint } =
76
+ useDeviceFingerprint();
77
+ const { state, dispatch } = useGlobalContext();
78
+ const { config, nav, app, user } = state;
79
+
80
+ let decodedAccessToken: any;
81
+ if (state.auth.access_token) {
82
+ decodedAccessToken = jwt_decode(state.auth.access_token);
83
+ }
84
+
85
+ const [userIdentifier, setUserIdentifier] = useState('');
86
+ const [fieldError, setFieldError] = useState<string | null>(null);
87
+ const [step, setStep] = useState(
88
+ state.auth.access_token &&
89
+ decodedAccessToken?.['https://auth.rownd.io/is_verified_user'] !== false
90
+ ? LoginStep.SUCCESS
91
+ : LoginStep.INIT
92
+ );
93
+ const [error, setError] = useState('');
94
+ const allowedIdentifiers = useMemo(() => ['email', 'phone'], []);
95
+ const [requestId, setRequestId] = useState<string | null>(null);
96
+ const [loginPollStart, setLoginPollStart] = useState<number | null>(null);
97
+ const [isSubmitting, setIsSubmitting] = useState(false);
98
+ const [loginType, setLoginType] = useState<'email' | 'phone' | null>(null);
99
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
100
+ const [_phoneDetails, setPhoneDetails] = useState<PhoneResult | null>(null); // TODO: For parity with web, need to use `phoneDetails` to update the input visuals
101
+ const [isValidUserIdentifier, setIsValidUserIdentifier] = useState(false);
102
+ const [requiresAdditionalFields, setRequiresAdditionalFields] = useState(
103
+ nav?.options?.init_data
104
+ );
105
+
106
+ const bottomSheetModalRef = useRef<BottomSheetModal>(null);
107
+ useEffect(() => {
108
+ if (bottomSheetModalRef.current) {
109
+ bottomSheetModalRef.current.present();
110
+ }
111
+ }, []);
112
+
113
+ const addlFieldInit = useCallback(
114
+ (currentState: Record<string, string>) => {
115
+ const addlFields = app?.config?.hub?.auth?.additional_fields;
116
+ const addlInputs =
117
+ nav?.options?.init_data || nav?.options?.default_values || {};
118
+
119
+ const newState: Record<string, string> = {};
120
+ if (addlFields?.length) {
121
+ for (const field of addlFields) {
122
+ if (field?.options) {
123
+ newState[field.name] =
124
+ addlInputs?.[field.name] || field.options[0].value;
125
+ }
126
+ }
127
+ }
128
+
129
+ return {
130
+ ...currentState,
131
+ ...newState,
132
+ };
133
+ },
134
+ [
135
+ app?.config?.hub?.auth?.additional_fields,
136
+ nav?.options?.default_values,
137
+ nav?.options?.init_data,
138
+ ]
139
+ );
140
+
141
+ const fieldReducer = useCallback(
142
+ (
143
+ currentState: any,
144
+ action: { type: string; payload?: Record<string, string> }
145
+ ) => {
146
+ console.log('fieldReducer', action);
147
+ switch (action.type) {
148
+ case 'reset':
149
+ return addlFieldInit(currentState);
150
+
151
+ default:
152
+ return {
153
+ ...currentState,
154
+ ...action.payload,
155
+ };
156
+ }
157
+ },
158
+ [addlFieldInit]
159
+ );
160
+
161
+ const [addlFieldValues, addlFieldDispatch] = useReducer(
162
+ fieldReducer,
163
+ {},
164
+ addlFieldInit
165
+ );
166
+
167
+ const { client: api } = useApi();
168
+
169
+ function validateEmail(email: string): boolean {
170
+ const re =
171
+ /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
172
+ const isValid = re.test(String(email).toLowerCase());
173
+
174
+ if (!isValid) {
175
+ setFieldError('Invalid email address');
176
+ return false;
177
+ }
178
+
179
+ setFieldError(null);
180
+ return true;
181
+ }
182
+
183
+ const isValidPhone = useCallback((): boolean => {
184
+ const phoneResult = phone(userIdentifier);
185
+ if (!phoneResult.isValid) {
186
+ setLoginType(null);
187
+ setPhoneDetails(null);
188
+ return false;
189
+ }
190
+
191
+ setLoginType('phone');
192
+ setPhoneDetails(phoneResult);
193
+ setUserIdentifier(phoneResult.phoneNumber);
194
+ return true;
195
+ }, [userIdentifier]);
196
+
197
+ const isValidEmail = useCallback((): boolean => {
198
+ const emailAtIdx = userIdentifier?.indexOf('@');
199
+ const emailSuffixIdx = userIdentifier?.substring(emailAtIdx).indexOf('.');
200
+ if (
201
+ emailAtIdx > 0 &&
202
+ emailSuffixIdx > 0 &&
203
+ userIdentifier?.substring(emailAtIdx + emailSuffixIdx).length >= 3
204
+ ) {
205
+ return validateEmail(userIdentifier);
206
+ }
207
+
208
+ return false;
209
+ }, [userIdentifier]);
210
+
211
+ const validateInput = useCallback(() => {
212
+ const validations = [];
213
+ if (allowedIdentifiers.includes('phone')) {
214
+ validations.push(isValidPhone);
215
+ }
216
+
217
+ if (allowedIdentifiers.includes('email')) {
218
+ validations.push(isValidEmail);
219
+ }
220
+
221
+ if (!validations.some((fn) => fn())) {
222
+ setIsValidUserIdentifier(false);
223
+ } else {
224
+ setIsValidUserIdentifier(true);
225
+ }
226
+ }, [allowedIdentifiers, isValidEmail, isValidPhone]);
227
+
228
+ // Fire validation as data changes in field
229
+ useEffect(validateInput, [validateInput]);
230
+
231
+ const pollLoginStatus = useCallback(async () => {
232
+ try {
233
+ const resp: LoginSuccessBody = await api
234
+ .post(`hub/auth/challenge_status`, {
235
+ headers: {
236
+ 'x-rownd-app-key': config?.appKey,
237
+ },
238
+ json: {
239
+ challenge_id: requestId,
240
+ [loginType === 'phone' ? 'phone' : 'email']: userIdentifier,
241
+ },
242
+ })
243
+ .json();
244
+
245
+ let err: any;
246
+ switch (resp.status) {
247
+ case 'pending':
248
+ err = new Error('Login challenge is still pending');
249
+ err.code = LoginVerificationStatus.PENDING;
250
+ throw err;
251
+
252
+ case 'expired':
253
+ err = new Error('Login challenge is still pending');
254
+ err.code = LoginVerificationStatus.PENDING;
255
+ throw err;
256
+
257
+ case 'verified':
258
+ break;
259
+
260
+ default:
261
+ err = new Error('Unknown login challenge status');
262
+ throw err;
263
+ }
264
+
265
+ dispatch({
266
+ type: ActionType.LOGIN_SUCCESS,
267
+ payload: resp,
268
+ });
269
+
270
+ setStep(LoginStep.SUCCESS);
271
+ } catch (err: any) {
272
+ // logger.log('login poll error', err);
273
+
274
+ // If network error, try again up to 1 minute, else fail
275
+ if (!err.code && differenceInMinutes(Date.now(), loginPollStart!) > 0) {
276
+ setStep(LoginStep.ERROR);
277
+ setError('Network error. Please try again later.');
278
+ return;
279
+ }
280
+
281
+ // If request expires, then fail (assume > 6 mins is a failure/expiration)
282
+ if (
283
+ (err.status || err.code) &&
284
+ differenceInMinutes(Date.now(), loginPollStart!) > 6
285
+ ) {
286
+ setStep(LoginStep.ERROR);
287
+ setError('The sign in request expired.');
288
+ return;
289
+ }
290
+
291
+ if (err.status && err.status >= 400) {
292
+ setStep(LoginStep.FAILURE);
293
+ setError('Sign in unsuccessful.');
294
+ }
295
+ }
296
+ }, [
297
+ api,
298
+ config?.appKey,
299
+ dispatch,
300
+ loginPollStart,
301
+ loginType,
302
+ requestId,
303
+ userIdentifier,
304
+ ]);
305
+
306
+ // Polling when a login flow is in progress
307
+ useInterval(pollLoginStatus, step === LoginStep.WAITING ? 5000 : null);
308
+
309
+ // login polling manager
310
+ useEffect(() => {
311
+ if (step === LoginStep.SUCCESS) {
312
+ if (nav?.options?.post_login_redirect || state.config?.postLoginUrl) {
313
+ Linking.openURL(
314
+ nav?.options?.post_login_redirect || state.config?.postLoginUrl
315
+ );
316
+ }
317
+
318
+ dispatch({
319
+ type: ActionType.CHANGE_ROUTE,
320
+ payload: {
321
+ route: '/',
322
+ },
323
+ });
324
+
325
+ // Reset modal state if this user is unverified, since we'll need to re-submit at some point
326
+ if (!state.auth.is_verified_user) {
327
+ setStep(LoginStep.INIT);
328
+ }
329
+ }
330
+ }, [
331
+ dispatch,
332
+ nav?.options?.post_login_redirect,
333
+ pollLoginStatus,
334
+ state.auth.is_verified_user,
335
+ state.config?.postLoginUrl,
336
+ step,
337
+ ]);
338
+
339
+ const initSignIn = useCallback(async () => {
340
+ if (step === LoginStep.WAITING) {
341
+ return;
342
+ }
343
+
344
+ // Validation
345
+ if (fieldError) {
346
+ return;
347
+ }
348
+
349
+ const payload = {
350
+ [loginType === 'phone' ? 'phone' : 'email']: userIdentifier,
351
+ return_url:
352
+ nav?.options?.post_login_redirect || state.config?.postLoginUrl,
353
+ user_data: Object.values(user.data).some(
354
+ (f) => f !== null && f !== undefined
355
+ )
356
+ ? user.data
357
+ : {}, // Include user.data if at least one field is defined
358
+ };
359
+
360
+ // Set the user_id to the application's default user id format if it is defined
361
+ if (app.config?.default_user_id_format) {
362
+ payload.user_id = app.config?.default_user_id_format;
363
+ }
364
+
365
+ if (requiresAdditionalFields) {
366
+ payload.user_data = {
367
+ ...payload.user_data,
368
+ ...addlFieldValues,
369
+ };
370
+ }
371
+
372
+ // Submission
373
+ try {
374
+ setIsSubmitting(true);
375
+
376
+ // Get the browser fingerprint for future sign-ins that don't require re-verification
377
+ const fingerprint = await getFingerprint();
378
+ const challengeEntry = await getChallengeIfPresent(app.id, [
379
+ userIdentifier,
380
+ ]);
381
+ payload.fingerprint = {
382
+ hash: fingerprint.visitorId,
383
+ challenge: challengeEntry?.value,
384
+ };
385
+
386
+ const resp: LoginInitBody = await api
387
+ .post('hub/auth/init', {
388
+ headers: {
389
+ 'x-rownd-app-key': config?.appKey,
390
+ },
391
+ json: payload,
392
+ })
393
+ .json();
394
+
395
+ // This will only be true when a user is signing in for the very first time and
396
+ // the app allows unverified users OR the user was successfully fingerprinted.
397
+ if (resp.auth_tokens) {
398
+ dispatch({
399
+ type: ActionType.LOGIN_SUCCESS,
400
+ payload: {
401
+ ...resp.auth_tokens,
402
+ app_id: app.id,
403
+ },
404
+ });
405
+
406
+ setStep(LoginStep.SUCCESS);
407
+ return;
408
+ } else if (payload?.fingerprint?.challenge) {
409
+ // The fingerprint is probably expired, so delete the challenge for re-registration
410
+ try {
411
+ clearFingerprint(payload.fingerprint.challenge);
412
+ } catch (err) {
413
+ // no-op, but not likely to throw anyway
414
+ }
415
+ }
416
+
417
+ setRequestId(resp.challenge_id);
418
+ setStep(LoginStep.WAITING);
419
+ setLoginPollStart(Date.now());
420
+ } catch (err: any) {
421
+ // logger.error(err);
422
+
423
+ if (err.response.status === 400) {
424
+ setStep(LoginStep.INIT);
425
+ setRequiresAdditionalFields(true);
426
+ return;
427
+ }
428
+
429
+ setStep(LoginStep.ERROR);
430
+ setError(err.message);
431
+ } finally {
432
+ setIsSubmitting(false);
433
+ }
434
+ }, [
435
+ step,
436
+ fieldError,
437
+ loginType,
438
+ userIdentifier,
439
+ nav?.options?.post_login_redirect,
440
+ state.config?.postLoginUrl,
441
+ user.data,
442
+ app.config?.default_user_id_format,
443
+ app.id,
444
+ requiresAdditionalFields,
445
+ addlFieldValues,
446
+ getFingerprint,
447
+ getChallengeIfPresent,
448
+ api,
449
+ config?.appKey,
450
+ dispatch,
451
+ clearFingerprint,
452
+ ]);
453
+
454
+ const launchRowndWebsite = () => {
455
+ Linking.openURL('https://rownd.io');
456
+ };
457
+
458
+ const handleAddlFieldChange = (evt: Event) => {
459
+ const target = evt.target as HTMLInputElement;
460
+ const { value, name } = target;
461
+
462
+ addlFieldDispatch({
463
+ type: 'default',
464
+ payload: {
465
+ [name]: value,
466
+ },
467
+ });
468
+ };
469
+
470
+ const handleClose = useCallback(() => {
471
+ setTimeout(() => {
472
+ navTo({ hide: true });
473
+ }, 150);
474
+ }, [navTo]);
475
+
476
+ const snapPoints = useMemo(() => ['25%', '70%'], []);
477
+
478
+ const renderBackdrop = useCallback(
479
+ (cbProps: BottomSheetBackdropProps) => (
480
+ <BottomSheetBackdrop {...cbProps} pressBehavior="close" />
481
+ ),
482
+ []
483
+ );
484
+
485
+ return (
486
+ <BottomSheetModal
487
+ snapPoints={snapPoints}
488
+ index={1}
489
+ backdropComponent={renderBackdrop}
490
+ keyboardBehavior="fillParent"
491
+ android_keyboardInputMode="adjustResize"
492
+ enablePanDownToClose={true}
493
+ onDismiss={handleClose}
494
+ style={styles.bottomSheet}
495
+ ref={bottomSheetModalRef}
496
+ >
497
+ <View style={styles.innerContainer}>
498
+ {step === LoginStep.INIT && (
499
+ <>
500
+ {app?.config?.hub?.auth?.show_app_icon &&
501
+ app?.icon &&
502
+ (app.icon_content_type === 'image/svg+xml' ? (
503
+ <SvgCssUri uri={app.icon} style={styles.appLogo} />
504
+ ) : (
505
+ <Image style={styles.appLogo} source={{ uri: app.icon }} />
506
+ ))}
507
+ <Text style={styles.dialogHeading}>Sign in or sign up</Text>
508
+ <Text style={styles.inputLabel}>Email or phone number</Text>
509
+ <BottomSheetTextInput
510
+ style={styles.identifierInput}
511
+ placeholder="Enter here"
512
+ keyboardType="email-address"
513
+ textContentType="emailAddress"
514
+ returnKeyLabel="Sign in"
515
+ returnKeyType="go"
516
+ enablesReturnKeyAutomatically={true}
517
+ autoCapitalize="none"
518
+ onChangeText={(text) => setUserIdentifier(text.trim())}
519
+ onBlur={validateInput}
520
+ value={userIdentifier}
521
+ onSubmitEditing={initSignIn}
522
+ />
523
+ {requiresAdditionalFields &&
524
+ app?.config?.hub?.auth?.additional_fields.map((field) => {
525
+ return renderField({
526
+ ...field,
527
+ value: addlFieldValues[field.name] || '',
528
+ [['input', 'text', 'tel', 'email'].includes(field.type)
529
+ ? 'onInput'
530
+ : 'onChange']: handleAddlFieldChange,
531
+ });
532
+ })}
533
+ <Pressable
534
+ style={({ pressed }: { pressed: boolean }) => [
535
+ styles.button,
536
+ !isValidUserIdentifier && styles.buttonDisabled,
537
+ pressed && styles.buttonPressed,
538
+ isSubmitting && styles.buttonSubmitting,
539
+ ]}
540
+ disabled={!isValidUserIdentifier || isSubmitting}
541
+ onPress={initSignIn}
542
+ >
543
+ <Text>
544
+ {isSubmitting && (
545
+ <ActivityIndicator size="small" color="#efefef" />
546
+ )}
547
+ <View style={styles.buttonText}>
548
+ <Text
549
+ style={
550
+ isValidUserIdentifier
551
+ ? styles.buttonContent
552
+ : {
553
+ ...styles.buttonContent,
554
+ ...styles.buttonDisabledText,
555
+ }
556
+ }
557
+ >
558
+ {isSubmitting ? 'Just a sec...' : 'Continue'}
559
+ </Text>
560
+ </View>
561
+ </Text>
562
+ </Pressable>
563
+ <Text style={styles.signInNoticeText}>
564
+ By continuing, you're agreeing to the terms of service that govern
565
+ this app and to receive email or text messages for verification
566
+ purposes.
567
+ </Text>
568
+ </>
569
+ )}
570
+
571
+ {step === LoginStep.WAITING && (
572
+ <>
573
+ <Text style={styles.dialogHeading}>
574
+ Thanks! Verify your{' '}
575
+ {loginType === 'phone' ? 'phone number' : 'email'} to finish
576
+ </Text>
577
+ <Text style={tw.style('py-6')}>
578
+ Click the link in the message we just sent to{' '}
579
+ <Text style={tw.style('italic')}>{userIdentifier}</Text> to verify
580
+ and finish.
581
+ <Text
582
+ style={[styles.link]}
583
+ onPress={() => setStep(LoginStep.INIT)}
584
+ >
585
+ &nbsp;Re-send message
586
+ </Text>
587
+ </Text>
588
+
589
+ <View style={styles.signInVerificationImage}>
590
+ {loginType === 'phone' ? (
591
+ <ImagePhoneVerifyWaiting />
592
+ ) : (
593
+ <ImageEmailVerifyWaiting />
594
+ )}
595
+ </View>
596
+
597
+ <Pressable
598
+ style={({ pressed }: { pressed: boolean }) => [
599
+ styles.button,
600
+ pressed && styles.buttonPressed,
601
+ ]}
602
+ onPress={() => setStep(LoginStep.INIT)}
603
+ >
604
+ <Text style={styles.buttonContent}>
605
+ Try a different{' '}
606
+ {loginType === 'phone' ? 'phone number' : 'email'}
607
+ </Text>
608
+ </Pressable>
609
+ </>
610
+ )}
611
+
612
+ {step === LoginStep.SUCCESS && (
613
+ <>
614
+ <View style={styles.signInVerificationImage}>
615
+ <ImageCheckmarkFilled />
616
+ </View>
617
+ </>
618
+ )}
619
+
620
+ {step === LoginStep.FAILURE && (
621
+ <>
622
+ <Text style={tw.style('text-base')}>Whoops, that didn't work!</Text>
623
+ <Pressable
624
+ style={styles.button}
625
+ onPress={() => setStep(LoginStep.INIT)}
626
+ >
627
+ <Text style={styles.buttonContent}>Try again</Text>
628
+ </Pressable>
629
+ </>
630
+ )}
631
+
632
+ {step === LoginStep.ERROR && (
633
+ <>
634
+ <Text style={tw.style('text-base')}>
635
+ An error occurred while signing you in.
636
+ </Text>
637
+ {!!error && <Text style={tw.style('text-rose-800')}>{error}</Text>}
638
+ <Pressable
639
+ style={styles.button}
640
+ onPress={() => setStep(LoginStep.INIT)}
641
+ >
642
+ <Text style={styles.buttonContent}>Try again</Text>
643
+ </Pressable>
644
+ </>
645
+ )}
646
+ <Text>
647
+ Powered by{' '}
648
+ <Text style={styles.link} onPress={launchRowndWebsite}>
649
+ Rownd
650
+ </Text>
651
+ </Text>
652
+ </View>
653
+ </BottomSheetModal>
654
+ );
655
+ }
656
+
657
+ const styles = StyleSheet.create({
658
+ modal: {
659
+ // flex: 1,
660
+ },
661
+ bottomSheet: {
662
+ shadowColor: '#000',
663
+ shadowOffset: {
664
+ width: 0,
665
+ height: 12,
666
+ },
667
+ shadowOpacity: 0.58,
668
+ shadowRadius: 16.0,
669
+
670
+ elevation: 24,
671
+ },
672
+ innerContainer: {
673
+ borderRadius: 20,
674
+ borderColor: 'transparent',
675
+ borderWidth: 0,
676
+ padding: 25,
677
+ },
678
+ container: {
679
+ flex: 1,
680
+ padding: 24,
681
+ },
682
+ header: {
683
+ alignItems: 'center',
684
+ backgroundColor: 'white',
685
+ paddingVertical: 20,
686
+ borderTopLeftRadius: 20,
687
+ borderTopRightRadius: 20,
688
+ },
689
+ panelHandle: {
690
+ width: 40,
691
+ height: 2,
692
+ backgroundColor: 'rgba(0,0,0,0.3)',
693
+ borderRadius: 4,
694
+ },
695
+ appLogo: {
696
+ padding: 50,
697
+ width: 75,
698
+ height: 75,
699
+ resizeMode: 'center',
700
+ textAlign: 'center',
701
+ marginLeft: 'auto',
702
+ marginRight: 'auto',
703
+ marginBottom: 20,
704
+ },
705
+ dialogHeading: {
706
+ fontSize: 24,
707
+ },
708
+ inputLabel: {
709
+ marginTop: 20,
710
+ marginBottom: 5,
711
+ },
712
+ identifierInput: {
713
+ backgroundColor: '#eee',
714
+ borderRadius: 8,
715
+ padding: 10,
716
+ fontSize: 18,
717
+ },
718
+ button: {
719
+ borderRadius: 10,
720
+ padding: 10,
721
+ marginTop: 20,
722
+ marginBottom: 30,
723
+ elevation: 0,
724
+ backgroundColor: '#5b0ae0',
725
+ display: 'flex',
726
+ alignItems: 'center',
727
+ justifyContent: 'center',
728
+ },
729
+ buttonDisabled: {
730
+ backgroundColor: '#eee',
731
+ },
732
+ buttonPressed: {
733
+ opacity: 0.5,
734
+ },
735
+ buttonContent: {
736
+ textAlign: 'center',
737
+ fontSize: 18,
738
+ color: '#fff',
739
+ },
740
+ buttonText: {
741
+ marginLeft: 10,
742
+ paddingLeft: 10,
743
+ fontSize: 18,
744
+ },
745
+ buttonTextInner: {
746
+ fontSize: 18,
747
+ },
748
+ buttonDisabledText: {
749
+ color: '#8e8e8e',
750
+ },
751
+ buttonSubmitting: {
752
+ backgroundColor: '#2f0492',
753
+ color: '#c7c7c7',
754
+ },
755
+ loadingIndicator: {
756
+ textAlignVertical: 'center',
757
+ padding: 20,
758
+ margin: 20,
759
+ },
760
+ signInNoticeText: {
761
+ fontSize: 12,
762
+ marginBottom: 20,
763
+ },
764
+ signInVerificationImage: {
765
+ alignItems: 'center',
766
+ },
767
+ link: {
768
+ color: '#6114e1',
769
+ },
770
+ });