@onairos/react-native 3.1.16 → 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 (198) 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/OnairosButton.js +290 -0
  16. package/lib/commonjs/components/OnairosButton.js.map +1 -0
  17. package/lib/commonjs/components/OnairosSignInButton.js +30 -8
  18. package/lib/commonjs/components/OnairosSignInButton.js.map +1 -1
  19. package/lib/commonjs/components/UniversalOnboarding.js +4 -4
  20. package/lib/commonjs/config/api.js +2 -2
  21. package/lib/commonjs/hooks/useConnections.js +6 -6
  22. package/lib/commonjs/hooks/useUserConnections.js +10 -10
  23. package/lib/commonjs/index.js +9 -10
  24. package/lib/commonjs/index.js.map +1 -1
  25. package/lib/commonjs/services/apiClient.js +35 -35
  26. package/lib/commonjs/services/apiKeyService.js +99 -99
  27. package/lib/commonjs/services/authService.js +82 -82
  28. package/lib/commonjs/services/biometricPinService.js +10 -10
  29. package/lib/commonjs/services/connectedAccountsService.js +32 -32
  30. package/lib/commonjs/services/googleAuthService.js +15 -15
  31. package/lib/commonjs/services/imageCompressionService.js +15 -15
  32. package/lib/commonjs/services/jwtStorageService.js +59 -59
  33. package/lib/commonjs/services/mobileTrainingService.js +14 -14
  34. package/lib/commonjs/services/pinEncryptionService.js +10 -10
  35. package/lib/commonjs/services/pinStorageUtils.js +15 -15
  36. package/lib/commonjs/services/platformAuthService.js +47 -47
  37. package/lib/commonjs/services/storageService.js +31 -31
  38. package/lib/commonjs/services/trainingApiHelpers.js +33 -33
  39. package/lib/commonjs/services/userConnectionsService.js +24 -24
  40. package/lib/commonjs/utils/Portal.js +4 -4
  41. package/lib/commonjs/utils/api.js +24 -24
  42. package/lib/commonjs/utils/auth.js +18 -18
  43. package/lib/commonjs/utils/crypto.js +13 -13
  44. package/lib/commonjs/utils/encryption.js +12 -12
  45. package/lib/commonjs/utils/eventUtils.js +52 -52
  46. package/lib/commonjs/utils/programmaticFlow.js +16 -16
  47. package/lib/commonjs/utils/retryHelper.js +27 -27
  48. package/lib/module/assets/images/Checkbox.svg +3 -3
  49. package/lib/module/assets/images/EnochE.svg +19 -19
  50. package/lib/module/assets/images/Personalityprofile.svg +3 -3
  51. package/lib/module/assets/images/Personalitytraits.svg +3 -3
  52. package/lib/module/assets/images/Userpreferences.svg +3 -3
  53. package/lib/module/assets/images/arrow.svg +20 -20
  54. package/lib/module/assets/images/basicproficon.svg +43 -43
  55. package/lib/module/assets/images/basicprofile.svg +3 -3
  56. package/lib/module/assets/images/checkmark.svg +4 -4
  57. package/lib/module/assets/images/contentanalysis.svg +3 -3
  58. package/lib/module/assets/images/contenticon.svg +23 -23
  59. package/lib/module/assets/images/personalityicon.svg +18 -18
  60. package/lib/module/assets/images/x-close.svg +3 -3
  61. package/lib/module/components/OnairosButton.js +282 -0
  62. package/lib/module/components/OnairosButton.js.map +1 -0
  63. package/lib/module/components/OnairosSignInButton.js +30 -8
  64. package/lib/module/components/OnairosSignInButton.js.map +1 -1
  65. package/lib/module/components/UniversalOnboarding.js +4 -4
  66. package/lib/module/config/api.js +2 -2
  67. package/lib/module/hooks/useConnections.js +6 -6
  68. package/lib/module/hooks/useUserConnections.js +10 -10
  69. package/lib/module/index.js +8 -10
  70. package/lib/module/index.js.map +1 -1
  71. package/lib/module/services/apiClient.js +35 -35
  72. package/lib/module/services/apiKeyService.js +99 -99
  73. package/lib/module/services/authService.js +82 -82
  74. package/lib/module/services/biometricPinService.js +10 -10
  75. package/lib/module/services/connectedAccountsService.js +32 -32
  76. package/lib/module/services/googleAuthService.js +15 -15
  77. package/lib/module/services/imageCompressionService.js +15 -15
  78. package/lib/module/services/jwtStorageService.js +59 -59
  79. package/lib/module/services/mobileTrainingService.js +14 -14
  80. package/lib/module/services/pinEncryptionService.js +10 -10
  81. package/lib/module/services/pinStorageUtils.js +15 -15
  82. package/lib/module/services/platformAuthService.js +47 -47
  83. package/lib/module/services/storageService.js +31 -31
  84. package/lib/module/services/trainingApiHelpers.js +33 -33
  85. package/lib/module/services/userConnectionsService.js +24 -24
  86. package/lib/module/utils/Portal.js +4 -4
  87. package/lib/module/utils/api.js +24 -24
  88. package/lib/module/utils/auth.js +18 -18
  89. package/lib/module/utils/crypto.js +13 -13
  90. package/lib/module/utils/encryption.js +12 -12
  91. package/lib/module/utils/eventUtils.js +52 -52
  92. package/lib/module/utils/programmaticFlow.js +16 -16
  93. package/lib/module/utils/retryHelper.js +27 -27
  94. package/lib/typescript/components/OnairosButton.d.ts +37 -0
  95. package/lib/typescript/components/OnairosButton.d.ts.map +1 -0
  96. package/lib/typescript/components/OnairosSignInButton.d.ts +2 -1
  97. package/lib/typescript/components/OnairosSignInButton.d.ts.map +1 -1
  98. package/lib/typescript/index.d.ts +3 -4
  99. package/lib/typescript/index.d.ts.map +1 -1
  100. package/package.json +163 -163
  101. package/src/api/index.ts +151 -151
  102. package/src/assets/images/Checkbox.svg +3 -3
  103. package/src/assets/images/EnochE.svg +19 -19
  104. package/src/assets/images/Personalityprofile.svg +3 -3
  105. package/src/assets/images/Personalitytraits.svg +3 -3
  106. package/src/assets/images/Userpreferences.svg +3 -3
  107. package/src/assets/images/arrow.svg +20 -20
  108. package/src/assets/images/basicproficon.svg +43 -43
  109. package/src/assets/images/basicprofile.svg +3 -3
  110. package/src/assets/images/checkmark.svg +4 -4
  111. package/src/assets/images/contentanalysis.svg +3 -3
  112. package/src/assets/images/contenticon.svg +23 -23
  113. package/src/assets/images/personalityicon.svg +18 -18
  114. package/src/assets/images/x-close.svg +3 -3
  115. package/src/components/BodyText.tsx +33 -33
  116. package/src/components/BrandMark.tsx +62 -62
  117. package/src/components/CodeInput.tsx +32 -32
  118. package/src/components/DataRequestScreen.tsx +355 -355
  119. package/src/components/EmailInput.tsx +31 -31
  120. package/src/components/EmailVerificationModal.tsx +363 -363
  121. package/src/components/ExistingUserDataConfirmation.tsx +506 -506
  122. package/src/components/GoogleButton.tsx +55 -55
  123. package/src/components/HeadingGroup.tsx +49 -49
  124. package/src/components/ModalHeader.tsx +125 -125
  125. package/src/components/ModalSheet.tsx +57 -57
  126. package/src/components/Onairos.tsx +422 -422
  127. package/src/components/OnairosButton.tsx +339 -0
  128. package/src/components/OnairosSignInButton.tsx +30 -10
  129. package/src/components/Overlay.tsx +506 -506
  130. package/src/components/PersonaImage.tsx +79 -79
  131. package/src/components/PersonaLoadingScreen.tsx +201 -201
  132. package/src/components/PersonalizationConsentScreen.tsx +410 -410
  133. package/src/components/PinCreationScreen.tsx +492 -492
  134. package/src/components/PinInput.tsx +555 -555
  135. package/src/components/PlatformConnectorsStep.tsx +891 -891
  136. package/src/components/PlatformList.tsx +144 -144
  137. package/src/components/PlatformToggle.tsx +226 -226
  138. package/src/components/PrimaryButton.tsx +213 -213
  139. package/src/components/SignInMatchAnimation.tsx +225 -225
  140. package/src/components/SignInStep.tsx +217 -217
  141. package/src/components/TrainingModal.tsx +1047 -1047
  142. package/src/components/UniversalOnboarding.tsx +2887 -2887
  143. package/src/components/VerificationStep.tsx +198 -198
  144. package/src/components/WelcomeScreen.tsx +473 -473
  145. package/src/components/icons/Basicproficon.tsx +30 -30
  146. package/src/components/icons/Basicprofile.tsx +17 -17
  147. package/src/components/icons/Checkbox.tsx +17 -17
  148. package/src/components/icons/Checkmark.tsx +24 -24
  149. package/src/components/icons/Contentanalysis.tsx +17 -17
  150. package/src/components/icons/Contenticon.tsx +30 -30
  151. package/src/components/icons/EnochE.tsx +39 -39
  152. package/src/components/icons/Personalityicon.tsx +22 -22
  153. package/src/components/icons/Personalityprofile.tsx +17 -17
  154. package/src/components/icons/Personalitytraits.tsx +17 -17
  155. package/src/components/icons/Userpreferences.tsx +17 -17
  156. package/src/components/icons/index.ts +12 -12
  157. package/src/components/onboarding/OAuthWebView.tsx +232 -232
  158. package/src/config/api.ts +25 -25
  159. package/src/context/AuthContext.tsx +393 -393
  160. package/src/hooks/useConnectedAccounts.ts +138 -138
  161. package/src/hooks/useConnections.ts +161 -161
  162. package/src/hooks/useCredentials.ts +174 -174
  163. package/src/hooks/useUserConnections.ts +165 -165
  164. package/src/index.js +14 -0
  165. package/src/index.ts +94 -96
  166. package/src/services/apiClient.ts +336 -336
  167. package/src/services/apiKeyService.ts +919 -919
  168. package/src/services/authService.ts +1008 -1008
  169. package/src/services/biometricPinService.ts +192 -192
  170. package/src/services/connectedAccountsService.ts +289 -289
  171. package/src/services/googleAuthService.ts +279 -279
  172. package/src/services/imageCompressionService.ts +302 -302
  173. package/src/services/jwtStorageService.ts +256 -256
  174. package/src/services/mobileTrainingService.ts +203 -203
  175. package/src/services/pinEncryptionService.ts +75 -75
  176. package/src/services/pinStorageUtils.ts +96 -96
  177. package/src/services/platformAuthService.ts +1346 -1346
  178. package/src/services/storageService.ts +451 -451
  179. package/src/services/trainingApiHelpers.ts +66 -66
  180. package/src/services/userConnectionsService.ts +556 -556
  181. package/src/services/youtubeMigrationService.ts +453 -453
  182. package/src/theme/index.ts +239 -239
  183. package/src/types/ambient.d.ts +28 -28
  184. package/src/types/index.ts +265 -265
  185. package/src/types/node-fix.d.ts +18 -18
  186. package/src/types/node-override.d.ts +23 -23
  187. package/src/types/opacity.d.ts +15 -15
  188. package/src/types/types.d.ts +17 -17
  189. package/src/utils/Portal.tsx +82 -82
  190. package/src/utils/api.js +111 -111
  191. package/src/utils/auth.js +103 -103
  192. package/src/utils/crypto.js +59 -59
  193. package/src/utils/encryption.ts +68 -68
  194. package/src/utils/eventUtils.ts +302 -302
  195. package/src/utils/haptics.ts +58 -58
  196. package/src/utils/imagePreloader.ts +2 -2
  197. package/src/utils/programmaticFlow.ts +112 -112
  198. 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
+ });