@onairos/react-native 3.1.15 → 3.1.17

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 (191) hide show
  1. package/README.md +404 -0
  2. package/lib/commonjs/assets/images/Checkbox.svg +3 -3
  3. package/lib/commonjs/assets/images/EnochE.svg +19 -19
  4. package/lib/commonjs/assets/images/Personalityprofile.svg +3 -3
  5. package/lib/commonjs/assets/images/Personalitytraits.svg +3 -3
  6. package/lib/commonjs/assets/images/Userpreferences.svg +3 -3
  7. package/lib/commonjs/assets/images/arrow.svg +20 -20
  8. package/lib/commonjs/assets/images/basicproficon.svg +43 -43
  9. package/lib/commonjs/assets/images/basicprofile.svg +3 -3
  10. package/lib/commonjs/assets/images/checkmark.svg +4 -4
  11. package/lib/commonjs/assets/images/contentanalysis.svg +3 -3
  12. package/lib/commonjs/assets/images/contenticon.svg +23 -23
  13. package/lib/commonjs/assets/images/personalityicon.svg +18 -18
  14. package/lib/commonjs/assets/images/x-close.svg +3 -3
  15. package/lib/commonjs/components/OnairosSignInButton.js +32 -74
  16. package/lib/commonjs/components/OnairosSignInButton.js.map +1 -1
  17. package/lib/commonjs/components/UniversalOnboarding.js +4 -4
  18. package/lib/commonjs/config/api.js +2 -2
  19. package/lib/commonjs/hooks/useConnections.js +6 -6
  20. package/lib/commonjs/hooks/useUserConnections.js +10 -10
  21. package/lib/commonjs/index.js +5 -12
  22. package/lib/commonjs/index.js.map +1 -1
  23. package/lib/commonjs/services/apiClient.js +35 -35
  24. package/lib/commonjs/services/apiKeyService.js +99 -99
  25. package/lib/commonjs/services/authService.js +82 -82
  26. package/lib/commonjs/services/biometricPinService.js +10 -10
  27. package/lib/commonjs/services/connectedAccountsService.js +32 -32
  28. package/lib/commonjs/services/googleAuthService.js +15 -15
  29. package/lib/commonjs/services/imageCompressionService.js +15 -15
  30. package/lib/commonjs/services/jwtStorageService.js +59 -59
  31. package/lib/commonjs/services/mobileTrainingService.js +14 -14
  32. package/lib/commonjs/services/pinEncryptionService.js +10 -10
  33. package/lib/commonjs/services/pinStorageUtils.js +15 -15
  34. package/lib/commonjs/services/platformAuthService.js +47 -47
  35. package/lib/commonjs/services/storageService.js +31 -31
  36. package/lib/commonjs/services/trainingApiHelpers.js +33 -33
  37. package/lib/commonjs/services/userConnectionsService.js +24 -24
  38. package/lib/commonjs/utils/Portal.js +4 -4
  39. package/lib/commonjs/utils/api.js +24 -24
  40. package/lib/commonjs/utils/auth.js +18 -18
  41. package/lib/commonjs/utils/crypto.js +13 -13
  42. package/lib/commonjs/utils/encryption.js +12 -12
  43. package/lib/commonjs/utils/eventUtils.js +52 -52
  44. package/lib/commonjs/utils/programmaticFlow.js +16 -16
  45. package/lib/commonjs/utils/retryHelper.js +27 -27
  46. package/lib/module/assets/images/Checkbox.svg +3 -3
  47. package/lib/module/assets/images/EnochE.svg +19 -19
  48. package/lib/module/assets/images/Personalityprofile.svg +3 -3
  49. package/lib/module/assets/images/Personalitytraits.svg +3 -3
  50. package/lib/module/assets/images/Userpreferences.svg +3 -3
  51. package/lib/module/assets/images/arrow.svg +20 -20
  52. package/lib/module/assets/images/basicproficon.svg +43 -43
  53. package/lib/module/assets/images/basicprofile.svg +3 -3
  54. package/lib/module/assets/images/checkmark.svg +4 -4
  55. package/lib/module/assets/images/contentanalysis.svg +3 -3
  56. package/lib/module/assets/images/contenticon.svg +23 -23
  57. package/lib/module/assets/images/personalityicon.svg +18 -18
  58. package/lib/module/assets/images/x-close.svg +3 -3
  59. package/lib/module/components/OnairosSignInButton.js +32 -74
  60. package/lib/module/components/OnairosSignInButton.js.map +1 -1
  61. package/lib/module/components/UniversalOnboarding.js +4 -4
  62. package/lib/module/config/api.js +2 -2
  63. package/lib/module/hooks/useConnections.js +6 -6
  64. package/lib/module/hooks/useUserConnections.js +10 -10
  65. package/lib/module/index.js +5 -6
  66. package/lib/module/index.js.map +1 -1
  67. package/lib/module/services/apiClient.js +35 -35
  68. package/lib/module/services/apiKeyService.js +99 -99
  69. package/lib/module/services/authService.js +82 -82
  70. package/lib/module/services/biometricPinService.js +10 -10
  71. package/lib/module/services/connectedAccountsService.js +32 -32
  72. package/lib/module/services/googleAuthService.js +15 -15
  73. package/lib/module/services/imageCompressionService.js +15 -15
  74. package/lib/module/services/jwtStorageService.js +59 -59
  75. package/lib/module/services/mobileTrainingService.js +14 -14
  76. package/lib/module/services/pinEncryptionService.js +10 -10
  77. package/lib/module/services/pinStorageUtils.js +15 -15
  78. package/lib/module/services/platformAuthService.js +47 -47
  79. package/lib/module/services/storageService.js +31 -31
  80. package/lib/module/services/trainingApiHelpers.js +33 -33
  81. package/lib/module/services/userConnectionsService.js +24 -24
  82. package/lib/module/utils/Portal.js +4 -4
  83. package/lib/module/utils/api.js +24 -24
  84. package/lib/module/utils/auth.js +18 -18
  85. package/lib/module/utils/crypto.js +13 -13
  86. package/lib/module/utils/encryption.js +12 -12
  87. package/lib/module/utils/eventUtils.js +52 -52
  88. package/lib/module/utils/programmaticFlow.js +16 -16
  89. package/lib/module/utils/retryHelper.js +27 -27
  90. package/lib/typescript/components/OnairosSignInButton.d.ts.map +1 -1
  91. package/lib/typescript/index.d.ts +0 -1
  92. package/lib/typescript/index.d.ts.map +1 -1
  93. package/package.json +163 -163
  94. package/src/api/index.ts +151 -151
  95. package/src/assets/images/Checkbox.svg +3 -3
  96. package/src/assets/images/EnochE.svg +19 -19
  97. package/src/assets/images/Personalityprofile.svg +3 -3
  98. package/src/assets/images/Personalitytraits.svg +3 -3
  99. package/src/assets/images/Userpreferences.svg +3 -3
  100. package/src/assets/images/arrow.svg +20 -20
  101. package/src/assets/images/basicproficon.svg +43 -43
  102. package/src/assets/images/basicprofile.svg +3 -3
  103. package/src/assets/images/checkmark.svg +4 -4
  104. package/src/assets/images/contentanalysis.svg +3 -3
  105. package/src/assets/images/contenticon.svg +23 -23
  106. package/src/assets/images/personalityicon.svg +18 -18
  107. package/src/assets/images/x-close.svg +3 -3
  108. package/src/components/BodyText.tsx +33 -33
  109. package/src/components/BrandMark.tsx +62 -62
  110. package/src/components/CodeInput.tsx +32 -32
  111. package/src/components/DataRequestScreen.tsx +355 -355
  112. package/src/components/EmailInput.tsx +31 -31
  113. package/src/components/EmailVerificationModal.tsx +363 -363
  114. package/src/components/ExistingUserDataConfirmation.tsx +506 -506
  115. package/src/components/GoogleButton.tsx +55 -55
  116. package/src/components/HeadingGroup.tsx +49 -49
  117. package/src/components/ModalHeader.tsx +125 -125
  118. package/src/components/ModalSheet.tsx +57 -57
  119. package/src/components/Onairos.tsx +422 -422
  120. package/src/components/OnairosButton.tsx +339 -339
  121. package/src/components/OnairosSignInButton.tsx +130 -166
  122. package/src/components/Overlay.tsx +506 -506
  123. package/src/components/PersonaImage.tsx +79 -79
  124. package/src/components/PersonaLoadingScreen.tsx +201 -201
  125. package/src/components/PersonalizationConsentScreen.tsx +410 -410
  126. package/src/components/PinCreationScreen.tsx +492 -492
  127. package/src/components/PinInput.tsx +555 -555
  128. package/src/components/PlatformConnectorsStep.tsx +891 -891
  129. package/src/components/PlatformList.tsx +144 -144
  130. package/src/components/PlatformToggle.tsx +226 -226
  131. package/src/components/PrimaryButton.tsx +213 -213
  132. package/src/components/SignInMatchAnimation.tsx +225 -225
  133. package/src/components/SignInStep.tsx +217 -217
  134. package/src/components/TrainingModal.tsx +1047 -1047
  135. package/src/components/UniversalOnboarding.tsx +2887 -2887
  136. package/src/components/VerificationStep.tsx +198 -198
  137. package/src/components/WelcomeScreen.tsx +473 -473
  138. package/src/components/icons/Basicproficon.tsx +30 -30
  139. package/src/components/icons/Basicprofile.tsx +17 -17
  140. package/src/components/icons/Checkbox.tsx +17 -17
  141. package/src/components/icons/Checkmark.tsx +24 -24
  142. package/src/components/icons/Contentanalysis.tsx +17 -17
  143. package/src/components/icons/Contenticon.tsx +30 -30
  144. package/src/components/icons/EnochE.tsx +39 -39
  145. package/src/components/icons/Personalityicon.tsx +22 -22
  146. package/src/components/icons/Personalityprofile.tsx +17 -17
  147. package/src/components/icons/Personalitytraits.tsx +17 -17
  148. package/src/components/icons/Userpreferences.tsx +17 -17
  149. package/src/components/icons/index.ts +12 -12
  150. package/src/components/onboarding/OAuthWebView.tsx +232 -232
  151. package/src/config/api.ts +25 -25
  152. package/src/context/AuthContext.tsx +393 -393
  153. package/src/hooks/useConnectedAccounts.ts +138 -138
  154. package/src/hooks/useConnections.ts +161 -161
  155. package/src/hooks/useCredentials.ts +174 -174
  156. package/src/hooks/useUserConnections.ts +165 -165
  157. package/src/index.js +14 -14
  158. package/src/index.ts +94 -95
  159. package/src/services/apiClient.ts +336 -336
  160. package/src/services/apiKeyService.ts +919 -919
  161. package/src/services/authService.ts +1008 -1008
  162. package/src/services/biometricPinService.ts +192 -192
  163. package/src/services/connectedAccountsService.ts +289 -289
  164. package/src/services/googleAuthService.ts +279 -279
  165. package/src/services/imageCompressionService.ts +302 -302
  166. package/src/services/jwtStorageService.ts +256 -256
  167. package/src/services/mobileTrainingService.ts +203 -203
  168. package/src/services/pinEncryptionService.ts +75 -75
  169. package/src/services/pinStorageUtils.ts +96 -96
  170. package/src/services/platformAuthService.ts +1346 -1346
  171. package/src/services/storageService.ts +451 -451
  172. package/src/services/trainingApiHelpers.ts +66 -66
  173. package/src/services/userConnectionsService.ts +556 -556
  174. package/src/services/youtubeMigrationService.ts +453 -453
  175. package/src/theme/index.ts +239 -239
  176. package/src/types/ambient.d.ts +28 -28
  177. package/src/types/index.ts +265 -265
  178. package/src/types/node-fix.d.ts +18 -18
  179. package/src/types/node-override.d.ts +23 -23
  180. package/src/types/opacity.d.ts +15 -15
  181. package/src/types/types.d.ts +17 -17
  182. package/src/utils/Portal.tsx +82 -82
  183. package/src/utils/api.js +111 -111
  184. package/src/utils/auth.js +103 -103
  185. package/src/utils/crypto.js +59 -59
  186. package/src/utils/encryption.ts +68 -68
  187. package/src/utils/eventUtils.ts +302 -302
  188. package/src/utils/haptics.ts +58 -58
  189. package/src/utils/imagePreloader.ts +2 -2
  190. package/src/utils/programmaticFlow.ts +112 -112
  191. package/src/utils/retryHelper.ts +274 -274
@@ -1,555 +1,555 @@
1
- import React, { useState, useCallback } from 'react';
2
- import {
3
- View,
4
- Text,
5
- StyleSheet,
6
- TextInput,
7
- TouchableOpacity,
8
- Dimensions,
9
- Keyboard,
10
- ActivityIndicator,
11
- } from 'react-native';
12
- import { biometricPinService } from '../services/biometricPinService';
13
-
14
- const { width } = Dimensions.get('window');
15
-
16
- export interface PinInputProps {
17
- onSubmit: (pin: string) => void;
18
- minLength?: number;
19
- requireSpecialChar?: boolean;
20
- requireNumber?: boolean;
21
- onBack?: () => void;
22
- // New prop to enable biometric storage
23
- enableBiometricStorage?: boolean;
24
- // ✅ NEW: Background training optimization props
25
- onBackgroundTrainingStart?: () => Promise<void>;
26
- showBackgroundProgress?: boolean;
27
- backgroundProgressText?: string;
28
- }
29
-
30
- type InfoStatus = 'default' | 'biometric-unavailable' | 'biometric-failed' | 'storage-error' | 'authenticating';
31
-
32
- export const PinInput: React.FC<PinInputProps> = ({
33
- onSubmit,
34
- minLength = 8,
35
- requireSpecialChar = true,
36
- requireNumber = true,
37
- onBack,
38
- enableBiometricStorage = true,
39
- // ✅ NEW: Background training optimization props
40
- onBackgroundTrainingStart,
41
- showBackgroundProgress = false,
42
- backgroundProgressText = "Training is starting in the background...",
43
- }) => {
44
- const [pin, setPin] = useState('');
45
- const [error, setError] = useState<string | null>(null);
46
- const [showPin, setShowPin] = useState(false);
47
- const [isStoringPin, setIsStoringPin] = useState(false);
48
- const [infoStatus, setInfoStatus] = useState<InfoStatus>('default');
49
- // ✅ NEW: Background training state
50
- const [isBackgroundTrainingStarted, setIsBackgroundTrainingStarted] = useState(false);
51
-
52
- // ✅ UPDATED: Make background training conditional, not automatic on mount
53
- React.useEffect(() => {
54
- // Only start background training if explicitly requested and not already started
55
- // This prevents automatic training on component mount
56
- if (onBackgroundTrainingStart && !isBackgroundTrainingStarted && showBackgroundProgress) {
57
- console.log('🚀 [PIN INPUT] Background training explicitly requested...');
58
- setIsBackgroundTrainingStarted(true);
59
-
60
- onBackgroundTrainingStart().catch(error => {
61
- console.error('❌ [PIN INPUT] Background training failed:', error);
62
- setIsBackgroundTrainingStarted(false);
63
- });
64
- }
65
- }, [onBackgroundTrainingStart, isBackgroundTrainingStarted, showBackgroundProgress]);
66
-
67
- const validatePin = useCallback((value: string) => {
68
- if (value.length < minLength) {
69
- return `PIN must be at least ${minLength} characters`;
70
- }
71
- if (requireSpecialChar && !/[!@#$%^&*(),.?":{}|<>]/.test(value)) {
72
- return 'PIN must include a special character';
73
- }
74
- if (requireNumber && !/\d/.test(value)) {
75
- return 'PIN must include a number';
76
- }
77
- return null;
78
- }, [minLength, requireSpecialChar, requireNumber]);
79
-
80
- const handleSubmit = useCallback(async () => {
81
- console.log('🔍 [PIN INPUT] handleSubmit called with PIN length:', pin.length);
82
-
83
- const validationError = validatePin(pin);
84
- if (validationError) {
85
- console.log('❌ [PIN INPUT] Validation failed:', validationError);
86
- setError(validationError);
87
- return;
88
- }
89
-
90
- console.log('✅ [PIN INPUT] PIN validation passed');
91
-
92
- // If biometric storage is enabled, store PIN securely
93
- if (enableBiometricStorage) {
94
- console.log('🔐 [PIN INPUT] Biometric storage enabled, starting biometric flow...');
95
- setIsStoringPin(true);
96
- setInfoStatus('authenticating');
97
-
98
- try {
99
- // Check if biometric is available
100
- const biometricAvailable = await biometricPinService.isBiometricAvailable();
101
- console.log('📱 [PIN INPUT] Biometric available:', biometricAvailable);
102
-
103
- if (!biometricAvailable) {
104
- console.error('❌ [PIN INPUT] Face ID not available on this device');
105
- setInfoStatus('biometric-unavailable');
106
- setIsStoringPin(false);
107
- return;
108
- }
109
-
110
- // Immediately attempt to store PIN with biometric authentication
111
- console.log('🔐 [PIN INPUT] Triggering biometric authentication for PIN storage...');
112
-
113
- const stored = await biometricPinService.storePinWithBiometric(pin);
114
-
115
- if (stored) {
116
- console.log('✅ [PIN INPUT] PIN stored successfully with biometric protection');
117
-
118
- // Verify the PIN was actually stored by checking if it can be retrieved
119
- const isStored = await biometricPinService.isPinStored();
120
- if (isStored) {
121
- console.log('✅ [PIN INPUT] PIN storage verified - proceeding with onboarding');
122
- onSubmit(pin);
123
- } else {
124
- console.error('❌ [PIN INPUT] PIN storage verification failed');
125
- setInfoStatus('storage-error');
126
- setIsStoringPin(false);
127
- }
128
- } else {
129
- // Biometric authentication was cancelled or failed
130
- console.warn('⚠️ [PIN INPUT] Biometric authentication failed or was cancelled');
131
- setInfoStatus('biometric-failed');
132
- setIsStoringPin(false);
133
- }
134
- } catch (error) {
135
- console.error('❌ [PIN INPUT] Error handling biometric PIN storage:', error);
136
- setInfoStatus('storage-error');
137
- setIsStoringPin(false);
138
- }
139
- } else {
140
- // Biometric storage disabled, proceed normally
141
- console.log('🔐 [PIN INPUT] Biometric storage disabled, proceeding normally');
142
- onSubmit(pin);
143
- }
144
- }, [pin, validatePin, onSubmit, enableBiometricStorage]);
145
-
146
- const handlePinChange = useCallback((value: string) => {
147
- setPin(value);
148
- setError(null);
149
- setInfoStatus('default');
150
-
151
- // Debug logging to help troubleshoot
152
- console.log('🔍 [PIN INPUT] PIN changed:', {
153
- length: value.length,
154
- hasSpecialChar: /[!@#$%^&*(),.?":{}|<>]/.test(value),
155
- hasNumber: /\d/.test(value),
156
- isValid: !validatePin(value)
157
- });
158
- }, [validatePin]);
159
-
160
- const handleContinueWithoutBiometric = useCallback(() => {
161
- console.warn('⚠️ [PIN INPUT] User chose to continue without biometric security');
162
- onSubmit(pin);
163
- }, [pin, onSubmit]);
164
-
165
- const handleRetry = useCallback(() => {
166
- setInfoStatus('default');
167
- handleSubmit();
168
- }, [handleSubmit]);
169
-
170
- const getInfoMessage = () => {
171
- switch (infoStatus) {
172
- case 'biometric-unavailable':
173
- return {
174
- title: 'Biometric Authentication Not Available',
175
- message: 'This device does not support Face ID or Touch ID. Please enable biometric authentication in your device settings to secure your PIN.',
176
- type: 'warning' as const,
177
- showActions: true,
178
- };
179
- case 'biometric-failed':
180
- return {
181
- title: 'Biometric Authentication Failed',
182
- message: 'Biometric authentication was cancelled or failed. Your PIN needs to be secured with biometric authentication to proceed.',
183
- type: 'warning' as const,
184
- showActions: true,
185
- };
186
- case 'storage-error':
187
- return {
188
- title: 'Storage Error',
189
- message: 'An error occurred while securing your PIN. Would you like to try again?',
190
- type: 'error' as const,
191
- showActions: true,
192
- };
193
- case 'authenticating':
194
- return {
195
- title: 'Authenticating...',
196
- message: 'Please complete biometric authentication to secure your PIN.',
197
- type: 'info' as const,
198
- showActions: false,
199
- };
200
- default:
201
- return {
202
- title: 'Secure PIN Storage',
203
- message: 'Your PIN will be securely stored locally on your device using biometric authentication (Face ID or Touch ID) for enhanced security.',
204
- type: 'info' as const,
205
- showActions: false,
206
- };
207
- }
208
- };
209
-
210
- const infoMessage = getInfoMessage();
211
-
212
- return (
213
- <View style={styles.container}>
214
- <View style={styles.inputSection}>
215
- <View style={styles.header}>
216
- {onBack && (
217
- <TouchableOpacity style={styles.backButton} onPress={onBack}>
218
- <Text style={{ fontSize: 24 }}>←</Text>
219
- </TouchableOpacity>
220
- )}
221
- <Text style={styles.title}>Create a PIN</Text>
222
- </View>
223
-
224
- <Text style={styles.subtitle}>
225
- A PIN so only you have access to your data
226
- </Text>
227
-
228
- {/* ✅ NEW: Background training progress indicator */}
229
- {showBackgroundProgress && isBackgroundTrainingStarted && (
230
- <View style={styles.backgroundProgressContainer}>
231
- <Text style={styles.backgroundProgressText}>
232
- {backgroundProgressText}
233
- </Text>
234
- <View style={styles.backgroundProgressIndicator}>
235
- <ActivityIndicator size="small" color="#007AFF" />
236
- </View>
237
- </View>
238
- )}
239
-
240
- <View style={styles.inputContainer}>
241
- <TextInput
242
- style={styles.input}
243
- value={pin}
244
- onChangeText={handlePinChange}
245
- secureTextEntry={!showPin}
246
- placeholder="e.g. MyPin123!"
247
- keyboardType="default"
248
- maxLength={20}
249
- autoCapitalize="none"
250
- autoCorrect={false}
251
- returnKeyType="done"
252
- onSubmitEditing={() => Keyboard.dismiss()}
253
- />
254
- <TouchableOpacity
255
- style={styles.visibilityButton}
256
- onPress={() => setShowPin(!showPin)}
257
- >
258
- <Text>{showPin ? '👁️' : '👁️‍🗨️'}</Text>
259
- </TouchableOpacity>
260
- </View>
261
- </View>
262
-
263
- {error && <Text style={styles.error}>{error}</Text>}
264
-
265
- <View style={styles.requirements}>
266
- <Text style={styles.requirementTitle}>PIN Requirements:</Text>
267
- <Text style={[styles.requirement, pin.length >= minLength && styles.requirementMet]}>
268
- • At least {minLength} characters
269
- </Text>
270
- {requireSpecialChar && (
271
- <Text
272
- style={[
273
- styles.requirement,
274
- /[!@#$%^&*(),.?":{}|<>]/.test(pin) && styles.requirementMet,
275
- ]}
276
- >
277
- • Include a special character
278
- </Text>
279
- )}
280
- {requireNumber && (
281
- <Text
282
- style={[
283
- styles.requirement,
284
- /\d/.test(pin) && styles.requirementMet,
285
- ]}
286
- >
287
- • Include a number
288
- </Text>
289
- )}
290
- </View>
291
-
292
- {/* Inline Information Section */}
293
- <View style={[
294
- styles.infoContainer,
295
- infoMessage.type === 'info' && styles.infoContainerInfo,
296
- infoMessage.type === 'warning' && styles.infoContainerWarning,
297
- infoMessage.type === 'error' && styles.infoContainerError,
298
- ]}>
299
- <Text style={[
300
- styles.infoTitle,
301
- infoMessage.type === 'info' && styles.infoTitleInfo,
302
- infoMessage.type === 'warning' && styles.infoTitleWarning,
303
- infoMessage.type === 'error' && styles.infoTitleError,
304
- ]}>
305
- {infoMessage.title}
306
- </Text>
307
- <Text style={[
308
- styles.infoMessage,
309
- infoMessage.type === 'info' && styles.infoMessageInfo,
310
- infoMessage.type === 'warning' && styles.infoMessageWarning,
311
- infoMessage.type === 'error' && styles.infoMessageError,
312
- ]}>
313
- {infoMessage.message}
314
- </Text>
315
-
316
- {infoMessage.showActions && (
317
- <View style={styles.infoActions}>
318
- <TouchableOpacity
319
- style={styles.infoActionButton}
320
- onPress={handleRetry}
321
- >
322
- <Text style={styles.infoActionButtonText}>Try Again</Text>
323
- </TouchableOpacity>
324
- <TouchableOpacity
325
- style={[styles.infoActionButton, styles.infoActionButtonSecondary]}
326
- onPress={handleContinueWithoutBiometric}
327
- >
328
- <Text style={[styles.infoActionButtonText, styles.infoActionButtonSecondaryText]}>
329
- Continue Without Biometric
330
- </Text>
331
- </TouchableOpacity>
332
- </View>
333
- )}
334
- </View>
335
-
336
- <View style={styles.footer}>
337
- {onBack && (
338
- <TouchableOpacity
339
- style={styles.cancelButton}
340
- onPress={onBack}
341
- >
342
- <Text style={styles.cancelButtonText}>Back</Text>
343
- </TouchableOpacity>
344
- )}
345
-
346
- <TouchableOpacity
347
- style={[styles.submitButton, (!pin || isStoringPin || validatePin(pin)) && styles.submitButtonDisabled]}
348
- onPress={handleSubmit}
349
- disabled={!pin || isStoringPin || !!validatePin(pin)}
350
- >
351
- <Text style={styles.submitButtonText}>
352
- {isStoringPin ? 'Authenticating...' : 'Continue'}
353
- </Text>
354
- </TouchableOpacity>
355
- </View>
356
- </View>
357
- );
358
- };
359
-
360
- const styles = StyleSheet.create({
361
- container: {
362
- flex: 1,
363
- padding: 16,
364
- width: width,
365
- backgroundColor: '#fff',
366
- justifyContent: 'flex-start',
367
- },
368
- inputSection: {
369
- width: '100%',
370
- marginTop: 0,
371
- paddingTop: 0,
372
- },
373
- header: {
374
- flexDirection: 'row',
375
- alignItems: 'center',
376
- justifyContent: 'space-between',
377
- marginBottom: 16,
378
- paddingVertical: 8,
379
- },
380
- backButton: {
381
- padding: 8,
382
- },
383
- title: {
384
- fontSize: 20,
385
- fontWeight: '600',
386
- color: '#333',
387
- textAlign: 'center',
388
- flex: 1,
389
- marginBottom: 8,
390
- },
391
- subtitle: {
392
- fontSize: 14,
393
- color: '#666',
394
- marginBottom: 24,
395
- textAlign: 'center',
396
- },
397
- inputContainer: {
398
- flexDirection: 'row',
399
- alignItems: 'center',
400
- borderWidth: 1,
401
- borderColor: '#CCCCCC',
402
- borderRadius: 8,
403
- marginBottom: 16,
404
- },
405
- input: {
406
- flex: 1,
407
- padding: 12,
408
- fontSize: 16,
409
- },
410
- visibilityButton: {
411
- padding: 12,
412
- },
413
- // Info Container Styles
414
- infoContainer: {
415
- padding: 16,
416
- borderRadius: 12,
417
- marginBottom: 16,
418
- borderWidth: 1,
419
- },
420
- infoContainerInfo: {
421
- backgroundColor: '#E3F2FD',
422
- borderColor: '#2196F3',
423
- },
424
- infoContainerWarning: {
425
- backgroundColor: '#FFF3E0',
426
- borderColor: '#FF9800',
427
- },
428
- infoContainerError: {
429
- backgroundColor: '#FFEBEE',
430
- borderColor: '#F44336',
431
- },
432
- infoTitle: {
433
- fontSize: 16,
434
- fontWeight: '600',
435
- marginBottom: 8,
436
- },
437
- infoTitleInfo: {
438
- color: '#1976D2',
439
- },
440
- infoTitleWarning: {
441
- color: '#F57C00',
442
- },
443
- infoTitleError: {
444
- color: '#D32F2F',
445
- },
446
- infoMessage: {
447
- fontSize: 14,
448
- lineHeight: 20,
449
- marginBottom: 12,
450
- },
451
- infoMessageInfo: {
452
- color: '#1565C0',
453
- },
454
- infoMessageWarning: {
455
- color: '#E65100',
456
- },
457
- infoMessageError: {
458
- color: '#C62828',
459
- },
460
- infoActions: {
461
- flexDirection: 'row',
462
- justifyContent: 'space-between',
463
- marginTop: 8,
464
- },
465
- infoActionButton: {
466
- flex: 1,
467
- paddingVertical: 10,
468
- paddingHorizontal: 16,
469
- borderRadius: 8,
470
- backgroundColor: '#007AFF',
471
- marginHorizontal: 4,
472
- alignItems: 'center',
473
- },
474
- infoActionButtonSecondary: {
475
- backgroundColor: 'transparent',
476
- borderWidth: 1,
477
- borderColor: '#007AFF',
478
- },
479
- infoActionButtonText: {
480
- color: '#fff',
481
- fontSize: 14,
482
- fontWeight: '600',
483
- },
484
- infoActionButtonSecondaryText: {
485
- color: '#007AFF',
486
- },
487
- error: {
488
- color: '#FF3B30',
489
- marginBottom: 16,
490
- },
491
- requirements: {
492
- marginBottom: 24,
493
- },
494
- requirementTitle: {
495
- fontSize: 14,
496
- fontWeight: '600',
497
- marginBottom: 8,
498
- color: '#333',
499
- },
500
- requirement: {
501
- fontSize: 14,
502
- color: '#666',
503
- marginBottom: 4,
504
- },
505
- requirementMet: {
506
- color: '#34C759',
507
- },
508
- footer: {
509
- flexDirection: 'row',
510
- alignItems: 'center',
511
- justifyContent: 'space-between',
512
- marginTop: 24,
513
- borderTopWidth: 1,
514
- borderTopColor: '#eee',
515
- paddingTop: 16,
516
- },
517
- cancelButton: {
518
- paddingVertical: 8,
519
- paddingHorizontal: 16,
520
- },
521
- cancelButtonText: {
522
- color: '#666',
523
- fontSize: 16,
524
- },
525
- submitButton: {
526
- paddingVertical: 16,
527
- paddingHorizontal: 32,
528
- borderRadius: 16,
529
- backgroundColor: '#000000',
530
- alignItems: 'center',
531
- },
532
- submitButtonDisabled: {
533
- opacity: 0.5,
534
- },
535
- submitButtonText: {
536
- color: '#fff',
537
- fontSize: 16,
538
- fontWeight: '600',
539
- },
540
- // ✅ NEW: Background training progress styles
541
- backgroundProgressContainer: {
542
- flexDirection: 'row',
543
- alignItems: 'center',
544
- justifyContent: 'center',
545
- marginBottom: 16,
546
- },
547
- backgroundProgressText: {
548
- fontSize: 14,
549
- color: '#666',
550
- marginRight: 10,
551
- },
552
- backgroundProgressIndicator: {
553
- padding: 5,
554
- },
555
- });
1
+ import React, { useState, useCallback } from 'react';
2
+ import {
3
+ View,
4
+ Text,
5
+ StyleSheet,
6
+ TextInput,
7
+ TouchableOpacity,
8
+ Dimensions,
9
+ Keyboard,
10
+ ActivityIndicator,
11
+ } from 'react-native';
12
+ import { biometricPinService } from '../services/biometricPinService';
13
+
14
+ const { width } = Dimensions.get('window');
15
+
16
+ export interface PinInputProps {
17
+ onSubmit: (pin: string) => void;
18
+ minLength?: number;
19
+ requireSpecialChar?: boolean;
20
+ requireNumber?: boolean;
21
+ onBack?: () => void;
22
+ // New prop to enable biometric storage
23
+ enableBiometricStorage?: boolean;
24
+ // ✅ NEW: Background training optimization props
25
+ onBackgroundTrainingStart?: () => Promise<void>;
26
+ showBackgroundProgress?: boolean;
27
+ backgroundProgressText?: string;
28
+ }
29
+
30
+ type InfoStatus = 'default' | 'biometric-unavailable' | 'biometric-failed' | 'storage-error' | 'authenticating';
31
+
32
+ export const PinInput: React.FC<PinInputProps> = ({
33
+ onSubmit,
34
+ minLength = 8,
35
+ requireSpecialChar = true,
36
+ requireNumber = true,
37
+ onBack,
38
+ enableBiometricStorage = true,
39
+ // ✅ NEW: Background training optimization props
40
+ onBackgroundTrainingStart,
41
+ showBackgroundProgress = false,
42
+ backgroundProgressText = "Training is starting in the background...",
43
+ }) => {
44
+ const [pin, setPin] = useState('');
45
+ const [error, setError] = useState<string | null>(null);
46
+ const [showPin, setShowPin] = useState(false);
47
+ const [isStoringPin, setIsStoringPin] = useState(false);
48
+ const [infoStatus, setInfoStatus] = useState<InfoStatus>('default');
49
+ // ✅ NEW: Background training state
50
+ const [isBackgroundTrainingStarted, setIsBackgroundTrainingStarted] = useState(false);
51
+
52
+ // ✅ UPDATED: Make background training conditional, not automatic on mount
53
+ React.useEffect(() => {
54
+ // Only start background training if explicitly requested and not already started
55
+ // This prevents automatic training on component mount
56
+ if (onBackgroundTrainingStart && !isBackgroundTrainingStarted && showBackgroundProgress) {
57
+ console.log('🚀 [PIN INPUT] Background training explicitly requested...');
58
+ setIsBackgroundTrainingStarted(true);
59
+
60
+ onBackgroundTrainingStart().catch(error => {
61
+ console.error('❌ [PIN INPUT] Background training failed:', error);
62
+ setIsBackgroundTrainingStarted(false);
63
+ });
64
+ }
65
+ }, [onBackgroundTrainingStart, isBackgroundTrainingStarted, showBackgroundProgress]);
66
+
67
+ const validatePin = useCallback((value: string) => {
68
+ if (value.length < minLength) {
69
+ return `PIN must be at least ${minLength} characters`;
70
+ }
71
+ if (requireSpecialChar && !/[!@#$%^&*(),.?":{}|<>]/.test(value)) {
72
+ return 'PIN must include a special character';
73
+ }
74
+ if (requireNumber && !/\d/.test(value)) {
75
+ return 'PIN must include a number';
76
+ }
77
+ return null;
78
+ }, [minLength, requireSpecialChar, requireNumber]);
79
+
80
+ const handleSubmit = useCallback(async () => {
81
+ console.log('🔍 [PIN INPUT] handleSubmit called with PIN length:', pin.length);
82
+
83
+ const validationError = validatePin(pin);
84
+ if (validationError) {
85
+ console.log('❌ [PIN INPUT] Validation failed:', validationError);
86
+ setError(validationError);
87
+ return;
88
+ }
89
+
90
+ console.log('✅ [PIN INPUT] PIN validation passed');
91
+
92
+ // If biometric storage is enabled, store PIN securely
93
+ if (enableBiometricStorage) {
94
+ console.log('🔐 [PIN INPUT] Biometric storage enabled, starting biometric flow...');
95
+ setIsStoringPin(true);
96
+ setInfoStatus('authenticating');
97
+
98
+ try {
99
+ // Check if biometric is available
100
+ const biometricAvailable = await biometricPinService.isBiometricAvailable();
101
+ console.log('📱 [PIN INPUT] Biometric available:', biometricAvailable);
102
+
103
+ if (!biometricAvailable) {
104
+ console.error('❌ [PIN INPUT] Face ID not available on this device');
105
+ setInfoStatus('biometric-unavailable');
106
+ setIsStoringPin(false);
107
+ return;
108
+ }
109
+
110
+ // Immediately attempt to store PIN with biometric authentication
111
+ console.log('🔐 [PIN INPUT] Triggering biometric authentication for PIN storage...');
112
+
113
+ const stored = await biometricPinService.storePinWithBiometric(pin);
114
+
115
+ if (stored) {
116
+ console.log('✅ [PIN INPUT] PIN stored successfully with biometric protection');
117
+
118
+ // Verify the PIN was actually stored by checking if it can be retrieved
119
+ const isStored = await biometricPinService.isPinStored();
120
+ if (isStored) {
121
+ console.log('✅ [PIN INPUT] PIN storage verified - proceeding with onboarding');
122
+ onSubmit(pin);
123
+ } else {
124
+ console.error('❌ [PIN INPUT] PIN storage verification failed');
125
+ setInfoStatus('storage-error');
126
+ setIsStoringPin(false);
127
+ }
128
+ } else {
129
+ // Biometric authentication was cancelled or failed
130
+ console.warn('⚠️ [PIN INPUT] Biometric authentication failed or was cancelled');
131
+ setInfoStatus('biometric-failed');
132
+ setIsStoringPin(false);
133
+ }
134
+ } catch (error) {
135
+ console.error('❌ [PIN INPUT] Error handling biometric PIN storage:', error);
136
+ setInfoStatus('storage-error');
137
+ setIsStoringPin(false);
138
+ }
139
+ } else {
140
+ // Biometric storage disabled, proceed normally
141
+ console.log('🔐 [PIN INPUT] Biometric storage disabled, proceeding normally');
142
+ onSubmit(pin);
143
+ }
144
+ }, [pin, validatePin, onSubmit, enableBiometricStorage]);
145
+
146
+ const handlePinChange = useCallback((value: string) => {
147
+ setPin(value);
148
+ setError(null);
149
+ setInfoStatus('default');
150
+
151
+ // Debug logging to help troubleshoot
152
+ console.log('🔍 [PIN INPUT] PIN changed:', {
153
+ length: value.length,
154
+ hasSpecialChar: /[!@#$%^&*(),.?":{}|<>]/.test(value),
155
+ hasNumber: /\d/.test(value),
156
+ isValid: !validatePin(value)
157
+ });
158
+ }, [validatePin]);
159
+
160
+ const handleContinueWithoutBiometric = useCallback(() => {
161
+ console.warn('⚠️ [PIN INPUT] User chose to continue without biometric security');
162
+ onSubmit(pin);
163
+ }, [pin, onSubmit]);
164
+
165
+ const handleRetry = useCallback(() => {
166
+ setInfoStatus('default');
167
+ handleSubmit();
168
+ }, [handleSubmit]);
169
+
170
+ const getInfoMessage = () => {
171
+ switch (infoStatus) {
172
+ case 'biometric-unavailable':
173
+ return {
174
+ title: 'Biometric Authentication Not Available',
175
+ message: 'This device does not support Face ID or Touch ID. Please enable biometric authentication in your device settings to secure your PIN.',
176
+ type: 'warning' as const,
177
+ showActions: true,
178
+ };
179
+ case 'biometric-failed':
180
+ return {
181
+ title: 'Biometric Authentication Failed',
182
+ message: 'Biometric authentication was cancelled or failed. Your PIN needs to be secured with biometric authentication to proceed.',
183
+ type: 'warning' as const,
184
+ showActions: true,
185
+ };
186
+ case 'storage-error':
187
+ return {
188
+ title: 'Storage Error',
189
+ message: 'An error occurred while securing your PIN. Would you like to try again?',
190
+ type: 'error' as const,
191
+ showActions: true,
192
+ };
193
+ case 'authenticating':
194
+ return {
195
+ title: 'Authenticating...',
196
+ message: 'Please complete biometric authentication to secure your PIN.',
197
+ type: 'info' as const,
198
+ showActions: false,
199
+ };
200
+ default:
201
+ return {
202
+ title: 'Secure PIN Storage',
203
+ message: 'Your PIN will be securely stored locally on your device using biometric authentication (Face ID or Touch ID) for enhanced security.',
204
+ type: 'info' as const,
205
+ showActions: false,
206
+ };
207
+ }
208
+ };
209
+
210
+ const infoMessage = getInfoMessage();
211
+
212
+ return (
213
+ <View style={styles.container}>
214
+ <View style={styles.inputSection}>
215
+ <View style={styles.header}>
216
+ {onBack && (
217
+ <TouchableOpacity style={styles.backButton} onPress={onBack}>
218
+ <Text style={{ fontSize: 24 }}>←</Text>
219
+ </TouchableOpacity>
220
+ )}
221
+ <Text style={styles.title}>Create a PIN</Text>
222
+ </View>
223
+
224
+ <Text style={styles.subtitle}>
225
+ A PIN so only you have access to your data
226
+ </Text>
227
+
228
+ {/* ✅ NEW: Background training progress indicator */}
229
+ {showBackgroundProgress && isBackgroundTrainingStarted && (
230
+ <View style={styles.backgroundProgressContainer}>
231
+ <Text style={styles.backgroundProgressText}>
232
+ {backgroundProgressText}
233
+ </Text>
234
+ <View style={styles.backgroundProgressIndicator}>
235
+ <ActivityIndicator size="small" color="#007AFF" />
236
+ </View>
237
+ </View>
238
+ )}
239
+
240
+ <View style={styles.inputContainer}>
241
+ <TextInput
242
+ style={styles.input}
243
+ value={pin}
244
+ onChangeText={handlePinChange}
245
+ secureTextEntry={!showPin}
246
+ placeholder="e.g. MyPin123!"
247
+ keyboardType="default"
248
+ maxLength={20}
249
+ autoCapitalize="none"
250
+ autoCorrect={false}
251
+ returnKeyType="done"
252
+ onSubmitEditing={() => Keyboard.dismiss()}
253
+ />
254
+ <TouchableOpacity
255
+ style={styles.visibilityButton}
256
+ onPress={() => setShowPin(!showPin)}
257
+ >
258
+ <Text>{showPin ? '👁️' : '👁️‍🗨️'}</Text>
259
+ </TouchableOpacity>
260
+ </View>
261
+ </View>
262
+
263
+ {error && <Text style={styles.error}>{error}</Text>}
264
+
265
+ <View style={styles.requirements}>
266
+ <Text style={styles.requirementTitle}>PIN Requirements:</Text>
267
+ <Text style={[styles.requirement, pin.length >= minLength && styles.requirementMet]}>
268
+ • At least {minLength} characters
269
+ </Text>
270
+ {requireSpecialChar && (
271
+ <Text
272
+ style={[
273
+ styles.requirement,
274
+ /[!@#$%^&*(),.?":{}|<>]/.test(pin) && styles.requirementMet,
275
+ ]}
276
+ >
277
+ • Include a special character
278
+ </Text>
279
+ )}
280
+ {requireNumber && (
281
+ <Text
282
+ style={[
283
+ styles.requirement,
284
+ /\d/.test(pin) && styles.requirementMet,
285
+ ]}
286
+ >
287
+ • Include a number
288
+ </Text>
289
+ )}
290
+ </View>
291
+
292
+ {/* Inline Information Section */}
293
+ <View style={[
294
+ styles.infoContainer,
295
+ infoMessage.type === 'info' && styles.infoContainerInfo,
296
+ infoMessage.type === 'warning' && styles.infoContainerWarning,
297
+ infoMessage.type === 'error' && styles.infoContainerError,
298
+ ]}>
299
+ <Text style={[
300
+ styles.infoTitle,
301
+ infoMessage.type === 'info' && styles.infoTitleInfo,
302
+ infoMessage.type === 'warning' && styles.infoTitleWarning,
303
+ infoMessage.type === 'error' && styles.infoTitleError,
304
+ ]}>
305
+ {infoMessage.title}
306
+ </Text>
307
+ <Text style={[
308
+ styles.infoMessage,
309
+ infoMessage.type === 'info' && styles.infoMessageInfo,
310
+ infoMessage.type === 'warning' && styles.infoMessageWarning,
311
+ infoMessage.type === 'error' && styles.infoMessageError,
312
+ ]}>
313
+ {infoMessage.message}
314
+ </Text>
315
+
316
+ {infoMessage.showActions && (
317
+ <View style={styles.infoActions}>
318
+ <TouchableOpacity
319
+ style={styles.infoActionButton}
320
+ onPress={handleRetry}
321
+ >
322
+ <Text style={styles.infoActionButtonText}>Try Again</Text>
323
+ </TouchableOpacity>
324
+ <TouchableOpacity
325
+ style={[styles.infoActionButton, styles.infoActionButtonSecondary]}
326
+ onPress={handleContinueWithoutBiometric}
327
+ >
328
+ <Text style={[styles.infoActionButtonText, styles.infoActionButtonSecondaryText]}>
329
+ Continue Without Biometric
330
+ </Text>
331
+ </TouchableOpacity>
332
+ </View>
333
+ )}
334
+ </View>
335
+
336
+ <View style={styles.footer}>
337
+ {onBack && (
338
+ <TouchableOpacity
339
+ style={styles.cancelButton}
340
+ onPress={onBack}
341
+ >
342
+ <Text style={styles.cancelButtonText}>Back</Text>
343
+ </TouchableOpacity>
344
+ )}
345
+
346
+ <TouchableOpacity
347
+ style={[styles.submitButton, (!pin || isStoringPin || validatePin(pin)) && styles.submitButtonDisabled]}
348
+ onPress={handleSubmit}
349
+ disabled={!pin || isStoringPin || !!validatePin(pin)}
350
+ >
351
+ <Text style={styles.submitButtonText}>
352
+ {isStoringPin ? 'Authenticating...' : 'Continue'}
353
+ </Text>
354
+ </TouchableOpacity>
355
+ </View>
356
+ </View>
357
+ );
358
+ };
359
+
360
+ const styles = StyleSheet.create({
361
+ container: {
362
+ flex: 1,
363
+ padding: 16,
364
+ width: width,
365
+ backgroundColor: '#fff',
366
+ justifyContent: 'flex-start',
367
+ },
368
+ inputSection: {
369
+ width: '100%',
370
+ marginTop: 0,
371
+ paddingTop: 0,
372
+ },
373
+ header: {
374
+ flexDirection: 'row',
375
+ alignItems: 'center',
376
+ justifyContent: 'space-between',
377
+ marginBottom: 16,
378
+ paddingVertical: 8,
379
+ },
380
+ backButton: {
381
+ padding: 8,
382
+ },
383
+ title: {
384
+ fontSize: 20,
385
+ fontWeight: '600',
386
+ color: '#333',
387
+ textAlign: 'center',
388
+ flex: 1,
389
+ marginBottom: 8,
390
+ },
391
+ subtitle: {
392
+ fontSize: 14,
393
+ color: '#666',
394
+ marginBottom: 24,
395
+ textAlign: 'center',
396
+ },
397
+ inputContainer: {
398
+ flexDirection: 'row',
399
+ alignItems: 'center',
400
+ borderWidth: 1,
401
+ borderColor: '#CCCCCC',
402
+ borderRadius: 8,
403
+ marginBottom: 16,
404
+ },
405
+ input: {
406
+ flex: 1,
407
+ padding: 12,
408
+ fontSize: 16,
409
+ },
410
+ visibilityButton: {
411
+ padding: 12,
412
+ },
413
+ // Info Container Styles
414
+ infoContainer: {
415
+ padding: 16,
416
+ borderRadius: 12,
417
+ marginBottom: 16,
418
+ borderWidth: 1,
419
+ },
420
+ infoContainerInfo: {
421
+ backgroundColor: '#E3F2FD',
422
+ borderColor: '#2196F3',
423
+ },
424
+ infoContainerWarning: {
425
+ backgroundColor: '#FFF3E0',
426
+ borderColor: '#FF9800',
427
+ },
428
+ infoContainerError: {
429
+ backgroundColor: '#FFEBEE',
430
+ borderColor: '#F44336',
431
+ },
432
+ infoTitle: {
433
+ fontSize: 16,
434
+ fontWeight: '600',
435
+ marginBottom: 8,
436
+ },
437
+ infoTitleInfo: {
438
+ color: '#1976D2',
439
+ },
440
+ infoTitleWarning: {
441
+ color: '#F57C00',
442
+ },
443
+ infoTitleError: {
444
+ color: '#D32F2F',
445
+ },
446
+ infoMessage: {
447
+ fontSize: 14,
448
+ lineHeight: 20,
449
+ marginBottom: 12,
450
+ },
451
+ infoMessageInfo: {
452
+ color: '#1565C0',
453
+ },
454
+ infoMessageWarning: {
455
+ color: '#E65100',
456
+ },
457
+ infoMessageError: {
458
+ color: '#C62828',
459
+ },
460
+ infoActions: {
461
+ flexDirection: 'row',
462
+ justifyContent: 'space-between',
463
+ marginTop: 8,
464
+ },
465
+ infoActionButton: {
466
+ flex: 1,
467
+ paddingVertical: 10,
468
+ paddingHorizontal: 16,
469
+ borderRadius: 8,
470
+ backgroundColor: '#007AFF',
471
+ marginHorizontal: 4,
472
+ alignItems: 'center',
473
+ },
474
+ infoActionButtonSecondary: {
475
+ backgroundColor: 'transparent',
476
+ borderWidth: 1,
477
+ borderColor: '#007AFF',
478
+ },
479
+ infoActionButtonText: {
480
+ color: '#fff',
481
+ fontSize: 14,
482
+ fontWeight: '600',
483
+ },
484
+ infoActionButtonSecondaryText: {
485
+ color: '#007AFF',
486
+ },
487
+ error: {
488
+ color: '#FF3B30',
489
+ marginBottom: 16,
490
+ },
491
+ requirements: {
492
+ marginBottom: 24,
493
+ },
494
+ requirementTitle: {
495
+ fontSize: 14,
496
+ fontWeight: '600',
497
+ marginBottom: 8,
498
+ color: '#333',
499
+ },
500
+ requirement: {
501
+ fontSize: 14,
502
+ color: '#666',
503
+ marginBottom: 4,
504
+ },
505
+ requirementMet: {
506
+ color: '#34C759',
507
+ },
508
+ footer: {
509
+ flexDirection: 'row',
510
+ alignItems: 'center',
511
+ justifyContent: 'space-between',
512
+ marginTop: 24,
513
+ borderTopWidth: 1,
514
+ borderTopColor: '#eee',
515
+ paddingTop: 16,
516
+ },
517
+ cancelButton: {
518
+ paddingVertical: 8,
519
+ paddingHorizontal: 16,
520
+ },
521
+ cancelButtonText: {
522
+ color: '#666',
523
+ fontSize: 16,
524
+ },
525
+ submitButton: {
526
+ paddingVertical: 16,
527
+ paddingHorizontal: 32,
528
+ borderRadius: 16,
529
+ backgroundColor: '#000000',
530
+ alignItems: 'center',
531
+ },
532
+ submitButtonDisabled: {
533
+ opacity: 0.5,
534
+ },
535
+ submitButtonText: {
536
+ color: '#fff',
537
+ fontSize: 16,
538
+ fontWeight: '600',
539
+ },
540
+ // ✅ NEW: Background training progress styles
541
+ backgroundProgressContainer: {
542
+ flexDirection: 'row',
543
+ alignItems: 'center',
544
+ justifyContent: 'center',
545
+ marginBottom: 16,
546
+ },
547
+ backgroundProgressText: {
548
+ fontSize: 14,
549
+ color: '#666',
550
+ marginRight: 10,
551
+ },
552
+ backgroundProgressIndicator: {
553
+ padding: 5,
554
+ },
555
+ });