@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.
- package/README.md +404 -0
- package/lib/commonjs/assets/images/Checkbox.svg +3 -3
- package/lib/commonjs/assets/images/EnochE.svg +19 -19
- package/lib/commonjs/assets/images/Personalityprofile.svg +3 -3
- package/lib/commonjs/assets/images/Personalitytraits.svg +3 -3
- package/lib/commonjs/assets/images/Userpreferences.svg +3 -3
- package/lib/commonjs/assets/images/arrow.svg +20 -20
- package/lib/commonjs/assets/images/basicproficon.svg +43 -43
- package/lib/commonjs/assets/images/basicprofile.svg +3 -3
- package/lib/commonjs/assets/images/checkmark.svg +4 -4
- package/lib/commonjs/assets/images/contentanalysis.svg +3 -3
- package/lib/commonjs/assets/images/contenticon.svg +23 -23
- package/lib/commonjs/assets/images/personalityicon.svg +18 -18
- package/lib/commonjs/assets/images/x-close.svg +3 -3
- package/lib/commonjs/components/OnairosSignInButton.js +32 -74
- package/lib/commonjs/components/OnairosSignInButton.js.map +1 -1
- package/lib/commonjs/components/UniversalOnboarding.js +4 -4
- package/lib/commonjs/config/api.js +2 -2
- package/lib/commonjs/hooks/useConnections.js +6 -6
- package/lib/commonjs/hooks/useUserConnections.js +10 -10
- package/lib/commonjs/index.js +5 -12
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/services/apiClient.js +35 -35
- package/lib/commonjs/services/apiKeyService.js +99 -99
- package/lib/commonjs/services/authService.js +82 -82
- package/lib/commonjs/services/biometricPinService.js +10 -10
- package/lib/commonjs/services/connectedAccountsService.js +32 -32
- package/lib/commonjs/services/googleAuthService.js +15 -15
- package/lib/commonjs/services/imageCompressionService.js +15 -15
- package/lib/commonjs/services/jwtStorageService.js +59 -59
- package/lib/commonjs/services/mobileTrainingService.js +14 -14
- package/lib/commonjs/services/pinEncryptionService.js +10 -10
- package/lib/commonjs/services/pinStorageUtils.js +15 -15
- package/lib/commonjs/services/platformAuthService.js +47 -47
- package/lib/commonjs/services/storageService.js +31 -31
- package/lib/commonjs/services/trainingApiHelpers.js +33 -33
- package/lib/commonjs/services/userConnectionsService.js +24 -24
- package/lib/commonjs/utils/Portal.js +4 -4
- package/lib/commonjs/utils/api.js +24 -24
- package/lib/commonjs/utils/auth.js +18 -18
- package/lib/commonjs/utils/crypto.js +13 -13
- package/lib/commonjs/utils/encryption.js +12 -12
- package/lib/commonjs/utils/eventUtils.js +52 -52
- package/lib/commonjs/utils/programmaticFlow.js +16 -16
- package/lib/commonjs/utils/retryHelper.js +27 -27
- package/lib/module/assets/images/Checkbox.svg +3 -3
- package/lib/module/assets/images/EnochE.svg +19 -19
- package/lib/module/assets/images/Personalityprofile.svg +3 -3
- package/lib/module/assets/images/Personalitytraits.svg +3 -3
- package/lib/module/assets/images/Userpreferences.svg +3 -3
- package/lib/module/assets/images/arrow.svg +20 -20
- package/lib/module/assets/images/basicproficon.svg +43 -43
- package/lib/module/assets/images/basicprofile.svg +3 -3
- package/lib/module/assets/images/checkmark.svg +4 -4
- package/lib/module/assets/images/contentanalysis.svg +3 -3
- package/lib/module/assets/images/contenticon.svg +23 -23
- package/lib/module/assets/images/personalityicon.svg +18 -18
- package/lib/module/assets/images/x-close.svg +3 -3
- package/lib/module/components/OnairosSignInButton.js +32 -74
- package/lib/module/components/OnairosSignInButton.js.map +1 -1
- package/lib/module/components/UniversalOnboarding.js +4 -4
- package/lib/module/config/api.js +2 -2
- package/lib/module/hooks/useConnections.js +6 -6
- package/lib/module/hooks/useUserConnections.js +10 -10
- package/lib/module/index.js +5 -6
- package/lib/module/index.js.map +1 -1
- package/lib/module/services/apiClient.js +35 -35
- package/lib/module/services/apiKeyService.js +99 -99
- package/lib/module/services/authService.js +82 -82
- package/lib/module/services/biometricPinService.js +10 -10
- package/lib/module/services/connectedAccountsService.js +32 -32
- package/lib/module/services/googleAuthService.js +15 -15
- package/lib/module/services/imageCompressionService.js +15 -15
- package/lib/module/services/jwtStorageService.js +59 -59
- package/lib/module/services/mobileTrainingService.js +14 -14
- package/lib/module/services/pinEncryptionService.js +10 -10
- package/lib/module/services/pinStorageUtils.js +15 -15
- package/lib/module/services/platformAuthService.js +47 -47
- package/lib/module/services/storageService.js +31 -31
- package/lib/module/services/trainingApiHelpers.js +33 -33
- package/lib/module/services/userConnectionsService.js +24 -24
- package/lib/module/utils/Portal.js +4 -4
- package/lib/module/utils/api.js +24 -24
- package/lib/module/utils/auth.js +18 -18
- package/lib/module/utils/crypto.js +13 -13
- package/lib/module/utils/encryption.js +12 -12
- package/lib/module/utils/eventUtils.js +52 -52
- package/lib/module/utils/programmaticFlow.js +16 -16
- package/lib/module/utils/retryHelper.js +27 -27
- package/lib/typescript/components/OnairosSignInButton.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +0 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/package.json +163 -163
- package/src/api/index.ts +151 -151
- package/src/assets/images/Checkbox.svg +3 -3
- package/src/assets/images/EnochE.svg +19 -19
- package/src/assets/images/Personalityprofile.svg +3 -3
- package/src/assets/images/Personalitytraits.svg +3 -3
- package/src/assets/images/Userpreferences.svg +3 -3
- package/src/assets/images/arrow.svg +20 -20
- package/src/assets/images/basicproficon.svg +43 -43
- package/src/assets/images/basicprofile.svg +3 -3
- package/src/assets/images/checkmark.svg +4 -4
- package/src/assets/images/contentanalysis.svg +3 -3
- package/src/assets/images/contenticon.svg +23 -23
- package/src/assets/images/personalityicon.svg +18 -18
- package/src/assets/images/x-close.svg +3 -3
- package/src/components/BodyText.tsx +33 -33
- package/src/components/BrandMark.tsx +62 -62
- package/src/components/CodeInput.tsx +32 -32
- package/src/components/DataRequestScreen.tsx +355 -355
- package/src/components/EmailInput.tsx +31 -31
- package/src/components/EmailVerificationModal.tsx +363 -363
- package/src/components/ExistingUserDataConfirmation.tsx +506 -506
- package/src/components/GoogleButton.tsx +55 -55
- package/src/components/HeadingGroup.tsx +49 -49
- package/src/components/ModalHeader.tsx +125 -125
- package/src/components/ModalSheet.tsx +57 -57
- package/src/components/Onairos.tsx +422 -422
- package/src/components/OnairosButton.tsx +339 -339
- package/src/components/OnairosSignInButton.tsx +130 -166
- package/src/components/Overlay.tsx +506 -506
- package/src/components/PersonaImage.tsx +79 -79
- package/src/components/PersonaLoadingScreen.tsx +201 -201
- package/src/components/PersonalizationConsentScreen.tsx +410 -410
- package/src/components/PinCreationScreen.tsx +492 -492
- package/src/components/PinInput.tsx +555 -555
- package/src/components/PlatformConnectorsStep.tsx +891 -891
- package/src/components/PlatformList.tsx +144 -144
- package/src/components/PlatformToggle.tsx +226 -226
- package/src/components/PrimaryButton.tsx +213 -213
- package/src/components/SignInMatchAnimation.tsx +225 -225
- package/src/components/SignInStep.tsx +217 -217
- package/src/components/TrainingModal.tsx +1047 -1047
- package/src/components/UniversalOnboarding.tsx +2887 -2887
- package/src/components/VerificationStep.tsx +198 -198
- package/src/components/WelcomeScreen.tsx +473 -473
- package/src/components/icons/Basicproficon.tsx +30 -30
- package/src/components/icons/Basicprofile.tsx +17 -17
- package/src/components/icons/Checkbox.tsx +17 -17
- package/src/components/icons/Checkmark.tsx +24 -24
- package/src/components/icons/Contentanalysis.tsx +17 -17
- package/src/components/icons/Contenticon.tsx +30 -30
- package/src/components/icons/EnochE.tsx +39 -39
- package/src/components/icons/Personalityicon.tsx +22 -22
- package/src/components/icons/Personalityprofile.tsx +17 -17
- package/src/components/icons/Personalitytraits.tsx +17 -17
- package/src/components/icons/Userpreferences.tsx +17 -17
- package/src/components/icons/index.ts +12 -12
- package/src/components/onboarding/OAuthWebView.tsx +232 -232
- package/src/config/api.ts +25 -25
- package/src/context/AuthContext.tsx +393 -393
- package/src/hooks/useConnectedAccounts.ts +138 -138
- package/src/hooks/useConnections.ts +161 -161
- package/src/hooks/useCredentials.ts +174 -174
- package/src/hooks/useUserConnections.ts +165 -165
- package/src/index.js +14 -14
- package/src/index.ts +94 -95
- package/src/services/apiClient.ts +336 -336
- package/src/services/apiKeyService.ts +919 -919
- package/src/services/authService.ts +1008 -1008
- package/src/services/biometricPinService.ts +192 -192
- package/src/services/connectedAccountsService.ts +289 -289
- package/src/services/googleAuthService.ts +279 -279
- package/src/services/imageCompressionService.ts +302 -302
- package/src/services/jwtStorageService.ts +256 -256
- package/src/services/mobileTrainingService.ts +203 -203
- package/src/services/pinEncryptionService.ts +75 -75
- package/src/services/pinStorageUtils.ts +96 -96
- package/src/services/platformAuthService.ts +1346 -1346
- package/src/services/storageService.ts +451 -451
- package/src/services/trainingApiHelpers.ts +66 -66
- package/src/services/userConnectionsService.ts +556 -556
- package/src/services/youtubeMigrationService.ts +453 -453
- package/src/theme/index.ts +239 -239
- package/src/types/ambient.d.ts +28 -28
- package/src/types/index.ts +265 -265
- package/src/types/node-fix.d.ts +18 -18
- package/src/types/node-override.d.ts +23 -23
- package/src/types/opacity.d.ts +15 -15
- package/src/types/types.d.ts +17 -17
- package/src/utils/Portal.tsx +82 -82
- package/src/utils/api.js +111 -111
- package/src/utils/auth.js +103 -103
- package/src/utils/crypto.js +59 -59
- package/src/utils/encryption.ts +68 -68
- package/src/utils/eventUtils.ts +302 -302
- package/src/utils/haptics.ts +58 -58
- package/src/utils/imagePreloader.ts +2 -2
- package/src/utils/programmaticFlow.ts +112 -112
- 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
|
+
});
|