@onairos/react-native 3.1.16 → 3.1.18

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 (207) 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/ModalSheet.js +8 -2
  16. package/lib/commonjs/components/ModalSheet.js.map +1 -1
  17. package/lib/commonjs/components/OnairosButton.js +290 -0
  18. package/lib/commonjs/components/OnairosButton.js.map +1 -0
  19. package/lib/commonjs/components/OnairosSignInButton.js +32 -8
  20. package/lib/commonjs/components/OnairosSignInButton.js.map +1 -1
  21. package/lib/commonjs/components/UniversalOnboarding.js +4 -4
  22. package/lib/commonjs/components/WelcomeScreen.js +29 -13
  23. package/lib/commonjs/components/WelcomeScreen.js.map +1 -1
  24. package/lib/commonjs/config/api.js +2 -2
  25. package/lib/commonjs/hooks/useConnections.js +6 -6
  26. package/lib/commonjs/hooks/useUserConnections.js +10 -10
  27. package/lib/commonjs/index.js +13 -6
  28. package/lib/commonjs/index.js.map +1 -1
  29. package/lib/commonjs/services/apiClient.js +35 -35
  30. package/lib/commonjs/services/apiKeyService.js +99 -99
  31. package/lib/commonjs/services/authService.js +82 -82
  32. package/lib/commonjs/services/biometricPinService.js +10 -10
  33. package/lib/commonjs/services/connectedAccountsService.js +32 -32
  34. package/lib/commonjs/services/googleAuthService.js +15 -15
  35. package/lib/commonjs/services/imageCompressionService.js +15 -15
  36. package/lib/commonjs/services/jwtStorageService.js +59 -59
  37. package/lib/commonjs/services/mobileTrainingService.js +14 -14
  38. package/lib/commonjs/services/pinEncryptionService.js +10 -10
  39. package/lib/commonjs/services/pinStorageUtils.js +15 -15
  40. package/lib/commonjs/services/platformAuthService.js +47 -47
  41. package/lib/commonjs/services/storageService.js +31 -31
  42. package/lib/commonjs/services/trainingApiHelpers.js +33 -33
  43. package/lib/commonjs/services/userConnectionsService.js +24 -24
  44. package/lib/commonjs/utils/Portal.js +4 -4
  45. package/lib/commonjs/utils/api.js +24 -24
  46. package/lib/commonjs/utils/auth.js +18 -18
  47. package/lib/commonjs/utils/crypto.js +13 -13
  48. package/lib/commonjs/utils/encryption.js +12 -12
  49. package/lib/commonjs/utils/eventUtils.js +52 -52
  50. package/lib/commonjs/utils/programmaticFlow.js +16 -16
  51. package/lib/commonjs/utils/retryHelper.js +27 -27
  52. package/lib/module/assets/images/Checkbox.svg +3 -3
  53. package/lib/module/assets/images/EnochE.svg +19 -19
  54. package/lib/module/assets/images/Personalityprofile.svg +3 -3
  55. package/lib/module/assets/images/Personalitytraits.svg +3 -3
  56. package/lib/module/assets/images/Userpreferences.svg +3 -3
  57. package/lib/module/assets/images/arrow.svg +20 -20
  58. package/lib/module/assets/images/basicproficon.svg +43 -43
  59. package/lib/module/assets/images/basicprofile.svg +3 -3
  60. package/lib/module/assets/images/checkmark.svg +4 -4
  61. package/lib/module/assets/images/contentanalysis.svg +3 -3
  62. package/lib/module/assets/images/contenticon.svg +23 -23
  63. package/lib/module/assets/images/personalityicon.svg +18 -18
  64. package/lib/module/assets/images/x-close.svg +3 -3
  65. package/lib/module/components/ModalSheet.js +7 -2
  66. package/lib/module/components/ModalSheet.js.map +1 -1
  67. package/lib/module/components/OnairosButton.js +282 -0
  68. package/lib/module/components/OnairosButton.js.map +1 -0
  69. package/lib/module/components/OnairosSignInButton.js +32 -8
  70. package/lib/module/components/OnairosSignInButton.js.map +1 -1
  71. package/lib/module/components/UniversalOnboarding.js +4 -4
  72. package/lib/module/components/WelcomeScreen.js +25 -10
  73. package/lib/module/components/WelcomeScreen.js.map +1 -1
  74. package/lib/module/config/api.js +2 -2
  75. package/lib/module/hooks/useConnections.js +6 -6
  76. package/lib/module/hooks/useUserConnections.js +10 -10
  77. package/lib/module/index.js +11 -11
  78. package/lib/module/index.js.map +1 -1
  79. package/lib/module/services/apiClient.js +35 -35
  80. package/lib/module/services/apiKeyService.js +99 -99
  81. package/lib/module/services/authService.js +82 -82
  82. package/lib/module/services/biometricPinService.js +10 -10
  83. package/lib/module/services/connectedAccountsService.js +32 -32
  84. package/lib/module/services/googleAuthService.js +15 -15
  85. package/lib/module/services/imageCompressionService.js +15 -15
  86. package/lib/module/services/jwtStorageService.js +59 -59
  87. package/lib/module/services/mobileTrainingService.js +14 -14
  88. package/lib/module/services/pinEncryptionService.js +10 -10
  89. package/lib/module/services/pinStorageUtils.js +15 -15
  90. package/lib/module/services/platformAuthService.js +47 -47
  91. package/lib/module/services/storageService.js +31 -31
  92. package/lib/module/services/trainingApiHelpers.js +33 -33
  93. package/lib/module/services/userConnectionsService.js +24 -24
  94. package/lib/module/utils/Portal.js +4 -4
  95. package/lib/module/utils/api.js +24 -24
  96. package/lib/module/utils/auth.js +18 -18
  97. package/lib/module/utils/crypto.js +13 -13
  98. package/lib/module/utils/encryption.js +12 -12
  99. package/lib/module/utils/eventUtils.js +52 -52
  100. package/lib/module/utils/programmaticFlow.js +16 -16
  101. package/lib/module/utils/retryHelper.js +27 -27
  102. package/lib/typescript/components/ModalSheet.d.ts.map +1 -1
  103. package/lib/typescript/components/OnairosButton.d.ts +37 -0
  104. package/lib/typescript/components/OnairosButton.d.ts.map +1 -0
  105. package/lib/typescript/components/OnairosSignInButton.d.ts.map +1 -1
  106. package/lib/typescript/components/WelcomeScreen.d.ts.map +1 -1
  107. package/lib/typescript/index.d.ts +3 -1
  108. package/lib/typescript/index.d.ts.map +1 -1
  109. package/package.json +145 -163
  110. package/src/api/index.ts +151 -151
  111. package/src/assets/images/Checkbox.svg +3 -3
  112. package/src/assets/images/EnochE.svg +19 -19
  113. package/src/assets/images/Personalityprofile.svg +3 -3
  114. package/src/assets/images/Personalitytraits.svg +3 -3
  115. package/src/assets/images/Userpreferences.svg +3 -3
  116. package/src/assets/images/arrow.svg +20 -20
  117. package/src/assets/images/basicproficon.svg +43 -43
  118. package/src/assets/images/basicprofile.svg +3 -3
  119. package/src/assets/images/checkmark.svg +4 -4
  120. package/src/assets/images/contentanalysis.svg +3 -3
  121. package/src/assets/images/contenticon.svg +23 -23
  122. package/src/assets/images/personalityicon.svg +18 -18
  123. package/src/assets/images/x-close.svg +3 -3
  124. package/src/components/BodyText.tsx +33 -33
  125. package/src/components/BrandMark.tsx +62 -62
  126. package/src/components/CodeInput.tsx +32 -32
  127. package/src/components/DataRequestScreen.tsx +355 -355
  128. package/src/components/EmailInput.tsx +31 -31
  129. package/src/components/EmailVerificationModal.tsx +363 -363
  130. package/src/components/ExistingUserDataConfirmation.tsx +506 -506
  131. package/src/components/GoogleButton.tsx +55 -55
  132. package/src/components/HeadingGroup.tsx +49 -49
  133. package/src/components/ModalHeader.tsx +125 -125
  134. package/src/components/ModalSheet.tsx +59 -57
  135. package/src/components/Onairos.tsx +422 -422
  136. package/src/components/OnairosButton.tsx +339 -0
  137. package/src/components/OnairosSignInButton.tsx +31 -9
  138. package/src/components/Overlay.tsx +506 -506
  139. package/src/components/PersonaImage.tsx +79 -79
  140. package/src/components/PersonaLoadingScreen.tsx +201 -201
  141. package/src/components/PersonalizationConsentScreen.tsx +410 -410
  142. package/src/components/PinCreationScreen.tsx +492 -492
  143. package/src/components/PinInput.tsx +555 -555
  144. package/src/components/PlatformConnectorsStep.tsx +891 -891
  145. package/src/components/PlatformList.tsx +144 -144
  146. package/src/components/PlatformToggle.tsx +226 -226
  147. package/src/components/PrimaryButton.tsx +213 -213
  148. package/src/components/SignInMatchAnimation.tsx +225 -225
  149. package/src/components/SignInStep.tsx +217 -217
  150. package/src/components/TrainingModal.tsx +1047 -1047
  151. package/src/components/UniversalOnboarding.tsx +2887 -2887
  152. package/src/components/VerificationStep.tsx +198 -198
  153. package/src/components/WelcomeScreen.tsx +490 -473
  154. package/src/components/icons/Basicproficon.tsx +30 -30
  155. package/src/components/icons/Basicprofile.tsx +17 -17
  156. package/src/components/icons/Checkbox.tsx +17 -17
  157. package/src/components/icons/Checkmark.tsx +24 -24
  158. package/src/components/icons/Contentanalysis.tsx +17 -17
  159. package/src/components/icons/Contenticon.tsx +30 -30
  160. package/src/components/icons/EnochE.tsx +39 -39
  161. package/src/components/icons/Personalityicon.tsx +22 -22
  162. package/src/components/icons/Personalityprofile.tsx +17 -17
  163. package/src/components/icons/Personalitytraits.tsx +17 -17
  164. package/src/components/icons/Userpreferences.tsx +17 -17
  165. package/src/components/icons/index.ts +12 -12
  166. package/src/components/onboarding/OAuthWebView.tsx +232 -232
  167. package/src/config/api.ts +25 -25
  168. package/src/context/AuthContext.tsx +393 -393
  169. package/src/hooks/useConnectedAccounts.ts +138 -138
  170. package/src/hooks/useConnections.ts +161 -161
  171. package/src/hooks/useCredentials.ts +174 -174
  172. package/src/hooks/useUserConnections.ts +165 -165
  173. package/src/index.js +14 -0
  174. package/src/index.ts +99 -96
  175. package/src/services/apiClient.ts +336 -336
  176. package/src/services/apiKeyService.ts +919 -919
  177. package/src/services/authService.ts +1008 -1008
  178. package/src/services/biometricPinService.ts +192 -192
  179. package/src/services/connectedAccountsService.ts +289 -289
  180. package/src/services/googleAuthService.ts +279 -279
  181. package/src/services/imageCompressionService.ts +302 -302
  182. package/src/services/jwtStorageService.ts +256 -256
  183. package/src/services/mobileTrainingService.ts +203 -203
  184. package/src/services/pinEncryptionService.ts +75 -75
  185. package/src/services/pinStorageUtils.ts +96 -96
  186. package/src/services/platformAuthService.ts +1346 -1346
  187. package/src/services/storageService.ts +451 -451
  188. package/src/services/trainingApiHelpers.ts +66 -66
  189. package/src/services/userConnectionsService.ts +556 -556
  190. package/src/services/youtubeMigrationService.ts +453 -453
  191. package/src/theme/index.ts +239 -239
  192. package/src/types/ambient.d.ts +28 -28
  193. package/src/types/index.ts +265 -265
  194. package/src/types/node-fix.d.ts +18 -18
  195. package/src/types/node-override.d.ts +23 -23
  196. package/src/types/opacity.d.ts +15 -15
  197. package/src/types/types.d.ts +17 -17
  198. package/src/utils/Portal.tsx +82 -82
  199. package/src/utils/api.js +111 -111
  200. package/src/utils/auth.js +103 -103
  201. package/src/utils/crypto.js +59 -59
  202. package/src/utils/encryption.ts +68 -68
  203. package/src/utils/eventUtils.ts +302 -302
  204. package/src/utils/haptics.ts +58 -58
  205. package/src/utils/imagePreloader.ts +2 -2
  206. package/src/utils/programmaticFlow.ts +112 -112
  207. package/src/utils/retryHelper.ts +274 -274
@@ -1,1047 +1,1047 @@
1
- import React, { useState, useEffect, useRef } from 'react';
2
- import {
3
- View,
4
- Text,
5
- StyleSheet,
6
- ScrollView,
7
- TouchableOpacity,
8
- ActivityIndicator,
9
- Dimensions,
10
- TextInput,
11
- Keyboard,
12
- Alert,
13
- Animated,
14
- Easing,
15
- Platform,
16
- KeyboardAvoidingView,
17
- Modal,
18
- TouchableWithoutFeedback
19
- } from 'react-native';
20
- import { io, Socket } from 'socket.io-client';
21
- import { getAuthToken, getUserProfile, saveEnochToken, getOnairosUsername } from '../services/authService';
22
- import LinearGradient from 'react-native-linear-gradient';
23
- import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
24
- import AsyncStorage from '@react-native-async-storage/async-storage';
25
- import { triggerHaptic, HapticType } from '../utils/haptics';
26
- import { getEncryptedPinForAPI } from '../services/pinEncryptionService';
27
- import { clearTemporaryPin } from '../services/pinStorageUtils';
28
- // import EmailVerification from '../../../components/EmailVerification'; // Commented out - file doesn't exist
29
- // import { sendEmailVerificationCode } from '../../../services/api'; // Commented out - use local API instead
30
- import { startEnochTrainingWithYouTubeCheck } from '../services/mobileTrainingService';
31
- import { ExistingUserDataConfirmation } from './ExistingUserDataConfirmation';
32
-
33
- const { width, height } = Dimensions.get('window');
34
-
35
- export interface TrainingModalProps {
36
- visible: boolean;
37
- progress?: number;
38
- eta?: string;
39
- onCancel: () => void;
40
- onComplete?: (emailOrToken: string) => void;
41
- modelKey?: string;
42
- username?: string;
43
- isPrimaryAuth?: boolean;
44
- autoFocusEmailInput?: boolean; // Add new prop
45
- navigation?: any;
46
- }
47
-
48
- export const TrainingModal: React.FC<TrainingModalProps> = ({
49
- visible,
50
- onCancel,
51
- onComplete,
52
- modelKey,
53
- username,
54
- isPrimaryAuth = false,
55
- autoFocusEmailInput = false, // Add default value
56
- navigation,
57
- }) => {
58
- const [progress, setProgress] = useState(0);
59
- const [email, setEmail] = useState('');
60
- const [emailError, setEmailError] = useState('');
61
- const [isTrainingComplete, setIsTrainingComplete] = useState(false);
62
- const [isSubmittingEmail, setIsSubmittingEmail] = useState(false);
63
- const [socketConnected, setSocketConnected] = useState(false);
64
- const [trainingStatus, setTrainingStatus] = useState('Initializing...');
65
- const [hasError, setHasError] = useState(false);
66
- const [userTraits, setUserTraits] = useState(null);
67
- const [inferenceResults, setInferenceResults] = useState(null);
68
- const socketRef = useRef<Socket | null>(null);
69
- const emailInputRef = useRef<TextInput>(null); // Add email input ref
70
- const [userToken, setUserToken] = useState<string | null>(null);
71
- const [userInfo, setUserInfo] = useState<any>(null);
72
- const [matchStep, setMatchStep] = useState<'email' | 'verification' | 'searching' | 'found' | 'existing_user'>('email');
73
- const [matchCount, setMatchCount] = useState(0);
74
- const [isContinueLoading, setIsContinueLoading] = useState(false);
75
- const [existingUserAccountInfo, setExistingUserAccountInfo] = useState<any>(null);
76
-
77
- // --- Modal slide animation ---
78
- const modalTranslateY = useRef(new Animated.Value(height)).current;
79
- const animationDuration = 300; // Increased duration for smoother animation
80
-
81
- // Reset state when modal becomes invisible
82
- useEffect(() => {
83
- if (!visible) {
84
- // Reset all state
85
- setEmail('');
86
- setEmailError('');
87
- setIsSubmittingEmail(false);
88
- setMatchStep('email');
89
- setMatchCount(0);
90
- setIsContinueLoading(false);
91
- setProgress(0);
92
- setIsTrainingComplete(false);
93
- setHasError(false);
94
- setTrainingStatus('Initializing...');
95
- setUserTraits(null);
96
- setInferenceResults(null);
97
- setUserToken(null);
98
- setUserInfo(null);
99
-
100
- // Reset animation value
101
- modalTranslateY.setValue(height);
102
- }
103
- }, [visible]);
104
-
105
- // Handle animation
106
- useEffect(() => {
107
- if (visible) {
108
- // Ensure we start from the bottom
109
- modalTranslateY.setValue(height);
110
- // Animate up
111
- Animated.spring(modalTranslateY, {
112
- toValue: 0,
113
- useNativeDriver: true,
114
- damping: 20,
115
- mass: 1,
116
- stiffness: 200,
117
- }).start();
118
- }
119
- }, [visible]);
120
-
121
- // Handle modal close
122
- const handleModalClose = () => {
123
- Animated.spring(modalTranslateY, {
124
- toValue: height,
125
- useNativeDriver: true,
126
- damping: 20,
127
- mass: 1,
128
- stiffness: 200,
129
- }).start(() => {
130
- onCancel();
131
- });
132
- };
133
-
134
- // Get auth token and user info on mount
135
- useEffect(() => {
136
- const loadUserData = async () => {
137
- try {
138
- const token = await getAuthToken();
139
- setUserToken(token);
140
-
141
- if (token) {
142
- // Use provided username instead of making API call to avoid 404 error
143
- // The backend JWT contains username but /api/user/profile endpoint doesn't exist
144
- const fallbackUsername = username || 'mobile_user';
145
-
146
- console.log('🔍 Using provided username for training:', fallbackUsername);
147
- setUserInfo({
148
- username: fallbackUsername,
149
- email: null,
150
- id: null // Will be filled by backend during training
151
- });
152
- }
153
- } catch (error) {
154
- console.error('Error loading user data:', error);
155
- // Fallback user info
156
- setUserInfo({
157
- username: username || 'mobile_user',
158
- email: null,
159
- id: null
160
- });
161
- }
162
- };
163
-
164
- if (visible) {
165
- loadUserData();
166
- }
167
- }, [visible, username]);
168
-
169
- // Start Enoch training via API
170
- const startEnochTraining = async (socketId: string) => {
171
- try {
172
- setTrainingStatus('Starting training...');
173
- setProgress(10);
174
-
175
- if (!userToken) {
176
- throw new Error('No authentication token available');
177
- }
178
-
179
- console.log('🚀 Starting Enoch training with socketId:', socketId);
180
-
181
- // Get stored Onairos username for API calls
182
- const storedUsername = await getOnairosUsername();
183
- const finalUsername = storedUsername || userInfo?.username || userInfo?.name || username || 'mobile_user';
184
-
185
- // Get connected platforms information
186
- const connectedPlatforms = await AsyncStorage.getItem('connectedPlatforms');
187
- const platformsList = connectedPlatforms ? JSON.parse(connectedPlatforms) : [];
188
-
189
- console.log('📱 Connected platforms for training:', platformsList);
190
-
191
- // Get encrypted PIN for training (if available)
192
- const encryptedPin = await getEncryptedPinForAPI().catch(error => {
193
- console.warn('⚠️ Could not get encrypted PIN for training:', error);
194
- return null;
195
- });
196
-
197
- // Prepare user data for training - match backend expected format
198
- const trainingData = {
199
- socketId,
200
- username: finalUsername, // This will be used as fallback in backend
201
- email: userInfo?.email || null, // Backend will use this if available
202
- modelKey: modelKey || null,
203
- connectedPlatforms: platformsList, // Include connected platforms
204
- // Include encrypted PIN if available
205
- ...(encryptedPin && {
206
- encryptedPin: encryptedPin,
207
- hasPinData: true
208
- })
209
- };
210
-
211
- console.log('📤 Starting training with YouTube migration check:', trainingData);
212
- console.log('🔑 Using stored username:', storedUsername);
213
- console.log('🔑 Final username for training:', finalUsername);
214
- console.log('🔗 Connected platforms:', platformsList);
215
- console.log('🔐 PIN included in training:', !!encryptedPin);
216
-
217
- // Use the new training function that includes YouTube migration check
218
- const result = await startEnochTrainingWithYouTubeCheck(trainingData);
219
-
220
- if (result.success) {
221
- console.log('🚀 Training Started:', result.message);
222
- console.log('🎯 Training Features:', result.features);
223
-
224
- // Log the new features from the spec
225
- if (result.features) {
226
- console.log('✅ Inference enabled:', result.features.inference);
227
- console.log('💾 Storage method:', result.features.storage);
228
- console.log('🔒 Compression enabled:', result.features.compression);
229
- console.log('🔐 Encryption enabled:', result.features.encryption);
230
- console.log('📊 Training type:', result.features.type);
231
- console.log('🗄️ Databases:', result.features.databases);
232
- console.log('📈 Query scores enabled:', result.features.queryScores);
233
- }
234
-
235
- setTrainingStatus('Training model...');
236
- setProgress(20);
237
- } else {
238
- console.error('Training start failed:', result.error);
239
- setTrainingStatus(`Error: ${result.error}`);
240
- setHasError(true);
241
- }
242
- } catch (error) {
243
- console.error('Training start error:', error);
244
- setTrainingStatus(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
245
- setHasError(true);
246
- }
247
- };
248
-
249
- // Setup socket connection and training
250
- useEffect(() => {
251
- if (!visible || !userToken || !userInfo) return;
252
-
253
- console.log('Setting up socket connection for training...');
254
- console.log('🧑‍💻 User info available:', userInfo);
255
-
256
- // Initialize socket connection
257
- socketRef.current = io('https://api2.onairos.uk', {
258
- transports: ['websocket'],
259
- autoConnect: false
260
- });
261
-
262
- // Socket event listeners
263
- socketRef.current.on('connect', () => {
264
- console.log('✅ Socket connected for training');
265
- setSocketConnected(true);
266
- const socketId = socketRef.current?.id;
267
- if (socketId) {
268
- // ❌ DISABLED: This was causing training to start too early (when modal opens)
269
- // Training should only start when transitioning from connectors to PIN screen
270
- // startEnochTraining(socketId);
271
- console.log('🔌 Socket ready, but training will start only when user transitions to PIN screen');
272
- }
273
- });
274
-
275
- socketRef.current.on('disconnect', () => {
276
- console.log('❌ Socket disconnected');
277
- setSocketConnected(false);
278
- });
279
-
280
- socketRef.current.on('trainingCompleted', (data) => {
281
- console.log('✅ Training Complete:', data);
282
- setTrainingStatus('Running test inference...');
283
- setProgress(60);
284
- });
285
-
286
- socketRef.current.on('inferenceCompleted', (data) => {
287
- console.log('🧠 Inference Complete:', data);
288
- setTrainingStatus('Uploading to S3...');
289
- setProgress(80);
290
- setUserTraits(data.traits);
291
- setInferenceResults(data.inferenceResults);
292
- });
293
-
294
- socketRef.current.on('modelStandby', (data) => {
295
- console.log('🎉 All Complete:', data);
296
-
297
- // Log completion details based on new spec
298
- if (data.completed) {
299
- console.log('✅ Training completed:', data.message);
300
- console.log('💾 Storage method:', data.storage);
301
- console.log('🔐 Encryption enabled:', data.encryption);
302
- console.log('🧠 Inference enabled:', data.inference);
303
-
304
- // Log database info for Enoch mode
305
- if (data.databases && Array.isArray(data.databases)) {
306
- console.log('🗄️ Databases used:', data.databases.join(', '));
307
- }
308
-
309
- // Log testing mode
310
- if (data.testing) {
311
- console.log('🧪 Testing mode enabled');
312
- }
313
- }
314
-
315
- setIsTrainingComplete(true);
316
- setTrainingStatus('Complete!');
317
- setProgress(100);
318
-
319
- // Clear temporary PIN after training is complete
320
- clearTemporaryPin();
321
-
322
- // Show continue button for primary auth when training is complete
323
- if (isPrimaryAuth) {
324
- setMatchStep('found');
325
- setMatchCount(Math.floor(Math.random() * 10) + 5);
326
- }
327
- });
328
-
329
- socketRef.current.on('trainingUpdate', (data) => {
330
- if (data.error) {
331
- console.error('Training update error:', data.error);
332
- setTrainingStatus(`Error: ${data.error}`);
333
- setHasError(true);
334
- } else if (data.progress) {
335
- setProgress(data.progress);
336
- setTrainingStatus(data.status || 'Training in progress...');
337
- }
338
- });
339
-
340
- // Connect to socket
341
- socketRef.current.connect();
342
-
343
- // Cleanup function
344
- return () => {
345
- if (socketRef.current) {
346
- console.log('🔌 Disconnecting training socket...');
347
- socketRef.current.disconnect();
348
- socketRef.current = null;
349
- }
350
- };
351
- }, [visible, userToken, userInfo]);
352
-
353
- // When modal becomes visible and autoFocus is requested, focus the email input
354
- useEffect(() => {
355
- if (visible && autoFocusEmailInput && matchStep === 'email' && emailInputRef.current) {
356
- const timer = setTimeout(() => {
357
- emailInputRef.current?.focus();
358
- }, 150); // Small delay to sync with modal animation
359
- return () => clearTimeout(timer);
360
- } else if (!visible && emailInputRef.current) {
361
- // Optional: Blur input when modal is not visible
362
- // emailInputRef.current?.blur();
363
- }
364
- }, [visible, autoFocusEmailInput, matchStep]);
365
-
366
- // Validate email format
367
- const validateEmail = (email: string) => {
368
- // Check for reviewer bypass first
369
- if (email.toLowerCase().trim() === 'reviewer') {
370
- return true; // Valid for reviewer bypass
371
- }
372
-
373
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
374
- return emailRegex.test(email);
375
- };
376
-
377
- const handleEmailSubmit = async () => {
378
- // Check for reviewer bypass
379
- if (email.toLowerCase().trim() === 'reviewer') {
380
- console.log('🔍 Reviewer bypass detected');
381
- setEmail('reviewer@bypass.com'); // Set a valid email for the flow
382
- if (onComplete) {
383
- onComplete('reviewer@bypass.com');
384
- }
385
- return;
386
- }
387
-
388
- // Regular email validation and submission
389
- if (!validateEmail(email)) {
390
- setEmailError('Please enter a valid email address');
391
- return;
392
- }
393
-
394
- setIsSubmittingEmail(true);
395
- setEmailError('');
396
-
397
- try {
398
- console.log('📧 Sending verification code to:', email);
399
-
400
- // Send verification code using the new API
401
- // const result = await sendEmailVerificationCode(email); // Function not available
402
- const result = { success: true, message: 'Email verification not implemented' };
403
-
404
- if (result.success) {
405
- console.log('✅ Verification code sent successfully');
406
- setMatchStep('verification'); // Move to verification step
407
- } else {
408
- console.log('❌ Failed to send verification code:', result.message);
409
- setEmailError(result.message || 'Failed to send verification code. Please try again.');
410
- }
411
- } catch (error) {
412
- console.error('❌ Error sending verification code:', error);
413
- setEmailError('Network error. Please check your connection and try again.');
414
- } finally {
415
- setIsSubmittingEmail(false);
416
- }
417
- };
418
-
419
- const handleVerificationComplete = async (verifiedEmail: string, isVerified: boolean, accountInfo?: any) => {
420
- if (isVerified) {
421
- console.log('✅ Email verification completed for:', verifiedEmail);
422
- console.log('🔍 AccountInfo received:', accountInfo);
423
- console.log('🔍 ExistingUser flag:', accountInfo?.existingUser);
424
-
425
- // Check if this is an existing user - must have accountInfo AND explicitly marked as existing
426
- if (accountInfo && accountInfo.existingUser === true) {
427
- console.log('🔍 Existing user detected:', accountInfo);
428
-
429
- // CRITICAL FIX: Ensure we have a valid JWT token before proceeding
430
- if (accountInfo.token && typeof accountInfo.token === 'string' && accountInfo.token.length > 50) {
431
- console.log('✅ Valid JWT token found for existing user');
432
-
433
- // Store the token immediately for existing users
434
- await AsyncStorage.setItem('onairos_jwt_token', accountInfo.token);
435
- await AsyncStorage.setItem('enoch_token', accountInfo.token);
436
- await AsyncStorage.setItem('auth_token', accountInfo.token);
437
- await AsyncStorage.setItem('user_email', verifiedEmail);
438
- await AsyncStorage.setItem('auth_method', 'onairos');
439
-
440
- console.log('✅ Existing user tokens stored successfully');
441
-
442
- // NEW: Show existing user confirmation modal instead of immediately completing
443
- setExistingUserAccountInfo(accountInfo);
444
- setMatchStep('existing_user');
445
- return;
446
- } else {
447
- console.error('❌ No valid JWT token for existing user - token:', accountInfo.token);
448
- console.error('❌ Cannot proceed without valid authentication token');
449
-
450
- // Don't fall back to email - throw error instead
451
- throw new Error('No valid authentication token received from server');
452
- }
453
- return;
454
- }
455
-
456
- console.log('🔍 New user detected - proceeding with account creation flow');
457
-
458
- // New user - proceed with account creation and continue to platform connectors
459
- try {
460
- console.log('📧 Creating Onairos account for verified email:', verifiedEmail);
461
-
462
- const username = verifiedEmail.split('@')[0];
463
-
464
- const onairosSignupResponse = await fetch('https://api2.onairos.uk/register/enoch', {
465
- method: 'POST',
466
- headers: {
467
- 'Content-Type': 'application/json'
468
- },
469
- body: JSON.stringify({
470
- email: verifiedEmail,
471
- username: username,
472
- name: username,
473
- emailVerified: true // Mark as verified
474
- })
475
- });
476
-
477
- const onairosResponseData = await onairosSignupResponse.json();
478
- console.log('🔗 Onairos account creation response:', onairosResponseData);
479
-
480
- if (onairosResponseData.token) {
481
- // Store tokens
482
- await AsyncStorage.setItem('onairos_jwt_token', onairosResponseData.token);
483
- await AsyncStorage.setItem('auth_token', onairosResponseData.token);
484
- await AsyncStorage.setItem('enoch_token', onairosResponseData.token);
485
- await AsyncStorage.setItem('onairos_username', onairosResponseData.username);
486
- await AsyncStorage.setItem('user_email', verifiedEmail);
487
-
488
- console.log('✅ Account created and tokens stored');
489
- console.log('🔗 Proceeding to UniversalOnboarding platform connectors');
490
-
491
- // Close the TrainingModal and proceed to UniversalOnboarding platform connectors
492
- if (onComplete) {
493
- // This is the account creation path for new users
494
- // Always proceed to platform connectors for new users, regardless of backend response
495
- console.log('📧 TrainingModal: New user account created - proceeding to platform connectors:', verifiedEmail);
496
- onComplete(verifiedEmail);
497
- }
498
- } else {
499
- // No token returned, continue with email to platform connectors
500
- console.log('🔗 No token returned, proceeding to platform connectors with email');
501
- // CRITICAL FIX: Store email even when account creation fails
502
- await AsyncStorage.setItem('user_email', verifiedEmail);
503
- console.log('✅ Email stored despite no token for admin check compatibility');
504
- if (onComplete) {
505
- onComplete(verifiedEmail);
506
- }
507
- }
508
- } catch (error) {
509
- console.error('❌ Error creating account after verification:', error);
510
- // Continue anyway to platform connectors
511
- console.log('🔗 Error occurred, but proceeding to platform connectors');
512
- // CRITICAL FIX: Store email even when account creation throws an error
513
- await AsyncStorage.setItem('user_email', verifiedEmail);
514
- console.log('✅ Email stored despite error for admin check compatibility');
515
- if (onComplete) {
516
- onComplete(verifiedEmail);
517
- }
518
- }
519
- }
520
- };
521
-
522
- const handleVerificationBack = () => {
523
- setMatchStep('email');
524
- setEmailError('');
525
- };
526
-
527
- // Handle existing user confirmation
528
- const handleExistingUserConfirm = async () => {
529
- console.log('✅ Existing user confirmed data sharing');
530
-
531
- if (existingUserAccountInfo && existingUserAccountInfo.token) {
532
- if (onComplete) {
533
- onComplete(existingUserAccountInfo.token);
534
- }
535
- } else {
536
- console.error('❌ No token available for existing user');
537
- setEmailError('Authentication error. Please try again.');
538
- setMatchStep('email');
539
- }
540
- };
541
-
542
- const handleExistingUserCancel = () => {
543
- console.log('❌ Existing user cancelled data sharing');
544
- setMatchStep('email');
545
- setExistingUserAccountInfo(null);
546
- setEmailError('');
547
- };
548
-
549
- const handleExistingUserAddMoreData = () => {
550
- console.log('🔗 Existing user wants to add more data');
551
-
552
- // Store the existing user info for later use
553
- if (existingUserAccountInfo && existingUserAccountInfo.token) {
554
- AsyncStorage.setItem('existing_user_token', existingUserAccountInfo.token);
555
- AsyncStorage.setItem('existing_user_info', JSON.stringify(existingUserAccountInfo));
556
- }
557
-
558
- // Continue to new user flow (UniversalOnboarding) but with existing user context
559
- setMatchStep('email');
560
- setExistingUserAccountInfo(null);
561
-
562
- // Call onComplete with email to trigger UniversalOnboarding
563
- if (onComplete) {
564
- onComplete(email);
565
- }
566
- };
567
-
568
- // Ensure cancel button always works
569
- const handleCancelPress = () => {
570
- // Clear temporary PIN when cancelling
571
- clearTemporaryPin();
572
- onCancel();
573
- };
574
- const handleContinue = () => {
575
- // Trigger haptic feedback when button is pressed
576
- ReactNativeHapticFeedback.trigger('impactMedium', {
577
- enableVibrateFallback: true,
578
- ignoreAndroidSystemSettings: false
579
- });
580
-
581
- console.log('handleContinue called. Current email state:', email);
582
- console.log('Continue button pressed, completing onboarding with email:', email);
583
-
584
- // Make sure we have a valid email
585
- if (!validateEmail(email)) {
586
- console.log('Email validation failed, cannot continue');
587
- return;
588
- }
589
-
590
- // Show loading state
591
- setIsContinueLoading(true);
592
-
593
- // Call onComplete immediately to prevent splash screen flash
594
- // The loading state will still be visible during the transition
595
- if (onComplete) {
596
- console.log('Calling onComplete with email:', email);
597
- onComplete(email);
598
- // Note: We don't reset isContinueLoading because the component will unmount
599
- }
600
- };
601
-
602
- // Calculate user-friendly message based on progress
603
- const getUserFriendlyMessage = () => {
604
- if (hasError) return 'Something went wrong. Please try again.';
605
- if (isTrainingComplete) return 'Your persona is ready! 🎉';
606
-
607
- if (progress < 20) {
608
- return 'Keeping your data private...';
609
- } else if (progress < 40) {
610
- return 'Trying to understand your mind...';
611
- } else if (progress < 60) {
612
- return 'You\'re more interesting than I expected...';
613
- } else if (progress < 80) {
614
- return 'Finalizing your unique persona...';
615
- } else if (progress < 95) {
616
- return 'Almost done...';
617
- } else {
618
- return 'Just a few more seconds...';
619
- }
620
- };
621
-
622
- // Calculate remaining time estimate
623
- const getTimeEstimate = () => {
624
- if (hasError) return 'Please try again';
625
- if (isTrainingComplete) return 'Complete!';
626
-
627
- // Rough time estimates based on progress
628
- const remainingProgress = 100 - progress;
629
- const estimatedSeconds = Math.ceil((remainingProgress / 100) * 120); // Assume 2 minutes total
630
-
631
- if (estimatedSeconds > 60) {
632
- return `About ${Math.ceil(estimatedSeconds / 60)} minute${Math.ceil(estimatedSeconds / 60) > 1 ? 's' : ''} remaining`;
633
- } else if (estimatedSeconds > 0) {
634
- return `About ${estimatedSeconds} seconds remaining`;
635
- } else {
636
- return 'Almost there...';
637
- }
638
- };
639
-
640
- // Grabber bar
641
- const Grabber = () => (
642
- <View style={styles.grabberContainer}>
643
- <View style={styles.grabberBar} />
644
- </View>
645
- );
646
-
647
- if (!visible) return null;
648
-
649
- const progressPercentage = Math.min(Math.max(progress, 0), 100);
650
-
651
- return (
652
- <Modal
653
- visible={visible}
654
- transparent
655
- animationType="none" // Changed to none since we're handling animation
656
- onRequestClose={handleModalClose}
657
- >
658
- <TouchableWithoutFeedback onPress={handleModalClose}>
659
- <View style={styles.modalOverlay}>
660
- <KeyboardAvoidingView
661
- behavior={Platform.OS === "ios" ? "padding" : "height"}
662
- keyboardVerticalOffset={0}
663
- style={styles.keyboardAvoidingView}
664
- >
665
- <TouchableWithoutFeedback onPress={(e) => e.stopPropagation()}>
666
- <Animated.View
667
- style={[
668
- styles.modalContent,
669
- {
670
- transform: [{ translateY: modalTranslateY }],
671
- }
672
- ]}
673
- >
674
- <Grabber />
675
-
676
- {/* START: Add X Close Button */}
677
- <TouchableOpacity style={styles.closeButton} onPress={onCancel}>
678
- <Text style={styles.closeButtonText}>✕</Text>
679
- </TouchableOpacity>
680
- {/* END: Add X Close Button */}
681
-
682
- {matchStep === 'email' && (
683
- <View style={[styles.primaryAuthHeader, { marginTop: 40 }]}>
684
- <Text style={styles.primaryAuthTitle}>Sign in with Onairos</Text>
685
- <Text style={styles.primaryAuthSubtitle}>
686
- </Text>
687
- </View>
688
- )}
689
-
690
-
691
-
692
- {/* Email Entry Step */}
693
- {isPrimaryAuth && matchStep === 'email' && (
694
- <View style={styles.emailSection}>
695
- <Text style={styles.emailLabel}>Enter your email to sign in / create an account</Text>
696
- <TextInput
697
- ref={emailInputRef}
698
- style={[styles.emailInput, emailError ? styles.emailInputError : null]}
699
- value={email}
700
- onChangeText={(text) => {
701
- setEmail(text);
702
- if (emailError) setEmailError('');
703
- }}
704
- placeholder="your.email@example.com"
705
- keyboardType="email-address"
706
- autoCapitalize="none"
707
- autoCorrect={false}
708
- returnKeyType="done"
709
- onSubmitEditing={() => Keyboard.dismiss()}
710
- />
711
- {emailError ? <Text style={styles.errorText}>{emailError}</Text> : null}
712
- <TouchableOpacity
713
- style={[
714
- styles.continueButton,
715
- (isSubmittingEmail || (!email.includes('@') && email.toLowerCase().trim() !== 'reviewer')) && styles.disabledButton,
716
- { marginTop: 24 }
717
- ]}
718
- onPress={handleEmailSubmit}
719
- disabled={isSubmittingEmail || (!email.includes('@') && email.toLowerCase().trim() !== 'reviewer')}
720
- activeOpacity={0.8}
721
- >
722
- {isSubmittingEmail ? (
723
- <ActivityIndicator size="small" color="#FFFFFF" />
724
- ) : (
725
- <Text style={styles.continueButtonText}>Continue</Text>
726
- )}
727
- </TouchableOpacity>
728
- </View>
729
- )}
730
-
731
- {/* Email Verification Step */}
732
- {isPrimaryAuth && matchStep === 'verification' && (
733
- <View>
734
- {/* EmailVerification component not available - placeholder */}
735
- <Text>Email verification step (not implemented)</Text>
736
- </View>
737
- )}
738
-
739
- {/* Existing User Data Confirmation Step */}
740
- {isPrimaryAuth && matchStep === 'existing_user' && existingUserAccountInfo && (
741
- <ExistingUserDataConfirmation
742
- visible={true}
743
- onConfirm={handleExistingUserConfirm}
744
- onCancel={handleExistingUserCancel}
745
- onAddMoreData={handleExistingUserAddMoreData}
746
- accountInfo={existingUserAccountInfo}
747
- userEmail={email}
748
- />
749
- )}
750
-
751
- {/* Existing steps remain the same */}
752
- {isPrimaryAuth && matchStep === 'searching' && (
753
- <View style={styles.matchContainer}>
754
- <Text style={styles.matchTitle}></Text>
755
- <ActivityIndicator size="large" color="#E9C46A" style={styles.matchLoader} />
756
- <Text style={styles.matchMessage}>
757
- Searching for potential connections based on your email...
758
- </Text>
759
- </View>
760
- )}
761
-
762
- {isPrimaryAuth && matchStep === 'found' && progress >= 100 && (
763
- <View style={styles.matchContainer}>
764
- <Text style={styles.matchTitle}>Connections Found</Text>
765
- <View style={styles.matchCircle}>
766
- <Text style={styles.matchCount}>{matchCount}</Text>
767
- </View>
768
- <Text style={styles.matchMessage}>
769
- We found {matchCount} potential connections for you!
770
- </Text>
771
- <Text style={styles.matchSubMessage}>
772
- Complete your profile to see your personalized matches
773
- </Text>
774
- <TouchableOpacity
775
- style={[
776
- styles.continueButton,
777
- isContinueLoading && styles.disabledButton
778
- ]}
779
- onPress={handleContinue}
780
- disabled={isContinueLoading}
781
- >
782
- {isContinueLoading ? (
783
- <ActivityIndicator size="small" color="#FFFFFF" />
784
- ) : (
785
- <Text style={styles.continueButtonText}>continue to setup</Text>
786
- )}
787
- </TouchableOpacity>
788
- </View>
789
- )}
790
- </Animated.View>
791
- </TouchableWithoutFeedback>
792
- </KeyboardAvoidingView>
793
- </View>
794
- </TouchableWithoutFeedback>
795
- </Modal>
796
- );
797
- };
798
-
799
- const styles = StyleSheet.create({
800
- container: {
801
- flex: 1,
802
- backgroundColor: '#fff',
803
- padding: 24,
804
- },
805
- scrollContentContainer: {
806
- flexGrow: 1,
807
- justifyContent: 'flex-start',
808
- paddingBottom: 0,
809
- marginBottom: 0,
810
- },
811
- actualContentWrapper: {
812
- width: '100%',
813
- alignItems: 'center', // Center content like headers if they are not full width
814
- },
815
- content: {
816
- // This style might be unused or redefined if still needed for specific children
817
- // flex: 1, // Original content style, likely not needed for the wrapper anymore
818
- },
819
- title: {
820
- fontSize: 24,
821
- fontWeight: '600',
822
- color: '#333',
823
- marginBottom: 24,
824
- textAlign: 'center',
825
- },
826
- subtitle: {
827
- fontSize: 16,
828
- color: '#666',
829
- marginBottom: 32,
830
- textAlign: 'center',
831
- },
832
- progressContainer: {
833
- marginBottom: 24,
834
- },
835
- progressBar: {
836
- height: 8,
837
- backgroundColor: '#F5F5F5',
838
- borderRadius: 4,
839
- overflow: 'hidden',
840
- marginBottom: 8,
841
- },
842
- progressFill: {
843
- height: '100%',
844
- backgroundColor: '#1BA9D4',
845
- borderRadius: 4,
846
- },
847
- progressText: {
848
- fontSize: 14,
849
- color: '#666',
850
- textAlign: 'right',
851
- },
852
- infoContainer: {
853
- flexDirection: 'row',
854
- alignItems: 'center',
855
- marginBottom: 32,
856
- },
857
- etaText: {
858
- fontSize: 14,
859
- color: '#666',
860
- marginLeft: 8,
861
- textAlign: 'center',
862
- marginBottom: 16,
863
- },
864
- detailText: {
865
- fontSize: 14,
866
- color: '#666',
867
- lineHeight: 20,
868
- },
869
- emailSection: {
870
- marginTop: 24,
871
- marginBottom: 16,
872
- width: '100%',
873
- },
874
- emailSectionPrimary: {
875
- marginTop: 40,
876
- marginBottom: 40,
877
- },
878
- emailLabel: {
879
- fontSize: 16,
880
- fontWeight: '500',
881
- color: '#333',
882
- marginBottom: 12,
883
- textAlign: 'center',
884
- },
885
- emailInput: {
886
- height: 48,
887
- borderWidth: 1,
888
- borderColor: '#E0E0E0',
889
- borderRadius: 8,
890
- paddingHorizontal: 16,
891
- fontSize: 16,
892
- marginBottom: 8,
893
- },
894
- emailInputError: {
895
- borderColor: '#FF3B30',
896
- },
897
- errorText: {
898
- color: '#FF3B30',
899
- fontSize: 14,
900
- marginBottom: 16,
901
- },
902
- footer: {
903
- flexDirection: 'row',
904
- justifyContent: 'space-between',
905
- marginTop: 24,
906
- paddingTop: 16,
907
- },
908
- completeButton: {
909
- paddingVertical: 12,
910
- paddingHorizontal: 24,
911
- backgroundColor: '#1BA9D4',
912
- borderRadius: 8,
913
- },
914
- disabledButton: {
915
- backgroundColor: '#CCCCCC',
916
- opacity: 0.7,
917
- },
918
- completeButtonText: {
919
- color: '#fff',
920
- fontSize: 16,
921
- fontWeight: '600',
922
- },
923
- statusText: {
924
- fontSize: 14,
925
- color: '#666',
926
- marginLeft: 8,
927
- },
928
- progressError: {
929
- backgroundColor: '#FF3B30',
930
- },
931
- primaryAuthHeader: {
932
- alignItems: 'center',
933
- marginBottom: 24,
934
- paddingHorizontal: 16,
935
- },
936
- primaryAuthTitle: {
937
- fontSize: 24,
938
- fontWeight: '600',
939
- color: '#333',
940
- marginBottom: 8,
941
- textAlign: 'center',
942
- },
943
- primaryAuthSubtitle: {
944
- fontSize: 16,
945
- color: '#555',
946
- textAlign: 'center',
947
- marginBottom: 16,
948
- },
949
- matchContainer: {
950
- alignItems: 'center',
951
- padding: 20,
952
- },
953
- matchTitle: {
954
- fontSize: 24,
955
- fontWeight: '600',
956
- color: '#333',
957
- marginBottom: 16,
958
- textAlign: 'center',
959
- },
960
- matchLoader: {
961
- marginVertical: 20,
962
- },
963
- matchMessage: {
964
- fontSize: 16,
965
- color: '#555',
966
- textAlign: 'center',
967
- marginVertical: 12,
968
- },
969
- matchSubMessage: {
970
- fontSize: 14,
971
- color: '#777',
972
- textAlign: 'center',
973
- marginTop: 8,
974
- },
975
- matchCircle: {
976
- width: 80,
977
- height: 80,
978
- borderRadius: 40,
979
- backgroundColor: '#E9C46A',
980
- justifyContent: 'center',
981
- alignItems: 'center',
982
- marginVertical: 16,
983
- },
984
- matchCount: {
985
- fontSize: 32,
986
- fontWeight: 'bold',
987
- color: '#FFFFFF',
988
- },
989
- continueButton: {
990
- marginTop: 40,
991
- width: '85%',
992
- height: 50,
993
- backgroundColor: '#000000',
994
- borderRadius: 8,
995
- justifyContent: 'center',
996
- alignItems: 'center',
997
- alignSelf: 'center',
998
- },
999
- continueButtonText: {
1000
- color: '#FFFFFF',
1001
- fontSize: 18,
1002
- fontWeight: '600',
1003
- textAlign: 'center',
1004
- },
1005
- modalOverlay: {
1006
- flex: 1,
1007
- backgroundColor: 'rgba(0, 0, 0, 0.5)',
1008
- justifyContent: 'flex-end',
1009
- },
1010
- keyboardAvoidingView: {
1011
- flex: 1,
1012
- justifyContent: 'flex-end',
1013
- alignItems: 'stretch',
1014
- margin: 0,
1015
- padding: 0,
1016
- },
1017
- modalContent: {
1018
- backgroundColor: '#fff',
1019
- borderTopLeftRadius: 20,
1020
- borderTopRightRadius: 20,
1021
- padding: 24,
1022
- paddingTop: 12,
1023
- minHeight: height * 0.4,
1024
- maxHeight: height * 0.9,
1025
- },
1026
- closeButton: {
1027
- position: 'absolute',
1028
- top: 15, // Adjust as needed for visual balance with grabber and padding
1029
- right: 20, // Adjust as needed
1030
- padding: 5, // Make it easier to tap
1031
- zIndex: 10, // Ensure it's above other content
1032
- },
1033
- closeButtonText: {
1034
- fontSize: 24, // Make it a bit larger
1035
- color: '#888', // Subtle color
1036
- fontWeight: '300',
1037
- },
1038
- grabberContainer: {
1039
- alignItems: 'center',
1040
- },
1041
- grabberBar: {
1042
- width: 48,
1043
- height: 5,
1044
- borderRadius: 3,
1045
- backgroundColor: '#E0E0E0',
1046
- },
1047
- });
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import {
3
+ View,
4
+ Text,
5
+ StyleSheet,
6
+ ScrollView,
7
+ TouchableOpacity,
8
+ ActivityIndicator,
9
+ Dimensions,
10
+ TextInput,
11
+ Keyboard,
12
+ Alert,
13
+ Animated,
14
+ Easing,
15
+ Platform,
16
+ KeyboardAvoidingView,
17
+ Modal,
18
+ TouchableWithoutFeedback
19
+ } from 'react-native';
20
+ import { io, Socket } from 'socket.io-client';
21
+ import { getAuthToken, getUserProfile, saveEnochToken, getOnairosUsername } from '../services/authService';
22
+ import LinearGradient from 'react-native-linear-gradient';
23
+ import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
24
+ import AsyncStorage from '@react-native-async-storage/async-storage';
25
+ import { triggerHaptic, HapticType } from '../utils/haptics';
26
+ import { getEncryptedPinForAPI } from '../services/pinEncryptionService';
27
+ import { clearTemporaryPin } from '../services/pinStorageUtils';
28
+ // import EmailVerification from '../../../components/EmailVerification'; // Commented out - file doesn't exist
29
+ // import { sendEmailVerificationCode } from '../../../services/api'; // Commented out - use local API instead
30
+ import { startEnochTrainingWithYouTubeCheck } from '../services/mobileTrainingService';
31
+ import { ExistingUserDataConfirmation } from './ExistingUserDataConfirmation';
32
+
33
+ const { width, height } = Dimensions.get('window');
34
+
35
+ export interface TrainingModalProps {
36
+ visible: boolean;
37
+ progress?: number;
38
+ eta?: string;
39
+ onCancel: () => void;
40
+ onComplete?: (emailOrToken: string) => void;
41
+ modelKey?: string;
42
+ username?: string;
43
+ isPrimaryAuth?: boolean;
44
+ autoFocusEmailInput?: boolean; // Add new prop
45
+ navigation?: any;
46
+ }
47
+
48
+ export const TrainingModal: React.FC<TrainingModalProps> = ({
49
+ visible,
50
+ onCancel,
51
+ onComplete,
52
+ modelKey,
53
+ username,
54
+ isPrimaryAuth = false,
55
+ autoFocusEmailInput = false, // Add default value
56
+ navigation,
57
+ }) => {
58
+ const [progress, setProgress] = useState(0);
59
+ const [email, setEmail] = useState('');
60
+ const [emailError, setEmailError] = useState('');
61
+ const [isTrainingComplete, setIsTrainingComplete] = useState(false);
62
+ const [isSubmittingEmail, setIsSubmittingEmail] = useState(false);
63
+ const [socketConnected, setSocketConnected] = useState(false);
64
+ const [trainingStatus, setTrainingStatus] = useState('Initializing...');
65
+ const [hasError, setHasError] = useState(false);
66
+ const [userTraits, setUserTraits] = useState(null);
67
+ const [inferenceResults, setInferenceResults] = useState(null);
68
+ const socketRef = useRef<Socket | null>(null);
69
+ const emailInputRef = useRef<TextInput>(null); // Add email input ref
70
+ const [userToken, setUserToken] = useState<string | null>(null);
71
+ const [userInfo, setUserInfo] = useState<any>(null);
72
+ const [matchStep, setMatchStep] = useState<'email' | 'verification' | 'searching' | 'found' | 'existing_user'>('email');
73
+ const [matchCount, setMatchCount] = useState(0);
74
+ const [isContinueLoading, setIsContinueLoading] = useState(false);
75
+ const [existingUserAccountInfo, setExistingUserAccountInfo] = useState<any>(null);
76
+
77
+ // --- Modal slide animation ---
78
+ const modalTranslateY = useRef(new Animated.Value(height)).current;
79
+ const animationDuration = 300; // Increased duration for smoother animation
80
+
81
+ // Reset state when modal becomes invisible
82
+ useEffect(() => {
83
+ if (!visible) {
84
+ // Reset all state
85
+ setEmail('');
86
+ setEmailError('');
87
+ setIsSubmittingEmail(false);
88
+ setMatchStep('email');
89
+ setMatchCount(0);
90
+ setIsContinueLoading(false);
91
+ setProgress(0);
92
+ setIsTrainingComplete(false);
93
+ setHasError(false);
94
+ setTrainingStatus('Initializing...');
95
+ setUserTraits(null);
96
+ setInferenceResults(null);
97
+ setUserToken(null);
98
+ setUserInfo(null);
99
+
100
+ // Reset animation value
101
+ modalTranslateY.setValue(height);
102
+ }
103
+ }, [visible]);
104
+
105
+ // Handle animation
106
+ useEffect(() => {
107
+ if (visible) {
108
+ // Ensure we start from the bottom
109
+ modalTranslateY.setValue(height);
110
+ // Animate up
111
+ Animated.spring(modalTranslateY, {
112
+ toValue: 0,
113
+ useNativeDriver: true,
114
+ damping: 20,
115
+ mass: 1,
116
+ stiffness: 200,
117
+ }).start();
118
+ }
119
+ }, [visible]);
120
+
121
+ // Handle modal close
122
+ const handleModalClose = () => {
123
+ Animated.spring(modalTranslateY, {
124
+ toValue: height,
125
+ useNativeDriver: true,
126
+ damping: 20,
127
+ mass: 1,
128
+ stiffness: 200,
129
+ }).start(() => {
130
+ onCancel();
131
+ });
132
+ };
133
+
134
+ // Get auth token and user info on mount
135
+ useEffect(() => {
136
+ const loadUserData = async () => {
137
+ try {
138
+ const token = await getAuthToken();
139
+ setUserToken(token);
140
+
141
+ if (token) {
142
+ // Use provided username instead of making API call to avoid 404 error
143
+ // The backend JWT contains username but /api/user/profile endpoint doesn't exist
144
+ const fallbackUsername = username || 'mobile_user';
145
+
146
+ console.log('🔍 Using provided username for training:', fallbackUsername);
147
+ setUserInfo({
148
+ username: fallbackUsername,
149
+ email: null,
150
+ id: null // Will be filled by backend during training
151
+ });
152
+ }
153
+ } catch (error) {
154
+ console.error('Error loading user data:', error);
155
+ // Fallback user info
156
+ setUserInfo({
157
+ username: username || 'mobile_user',
158
+ email: null,
159
+ id: null
160
+ });
161
+ }
162
+ };
163
+
164
+ if (visible) {
165
+ loadUserData();
166
+ }
167
+ }, [visible, username]);
168
+
169
+ // Start Enoch training via API
170
+ const startEnochTraining = async (socketId: string) => {
171
+ try {
172
+ setTrainingStatus('Starting training...');
173
+ setProgress(10);
174
+
175
+ if (!userToken) {
176
+ throw new Error('No authentication token available');
177
+ }
178
+
179
+ console.log('🚀 Starting Enoch training with socketId:', socketId);
180
+
181
+ // Get stored Onairos username for API calls
182
+ const storedUsername = await getOnairosUsername();
183
+ const finalUsername = storedUsername || userInfo?.username || userInfo?.name || username || 'mobile_user';
184
+
185
+ // Get connected platforms information
186
+ const connectedPlatforms = await AsyncStorage.getItem('connectedPlatforms');
187
+ const platformsList = connectedPlatforms ? JSON.parse(connectedPlatforms) : [];
188
+
189
+ console.log('📱 Connected platforms for training:', platformsList);
190
+
191
+ // Get encrypted PIN for training (if available)
192
+ const encryptedPin = await getEncryptedPinForAPI().catch(error => {
193
+ console.warn('⚠️ Could not get encrypted PIN for training:', error);
194
+ return null;
195
+ });
196
+
197
+ // Prepare user data for training - match backend expected format
198
+ const trainingData = {
199
+ socketId,
200
+ username: finalUsername, // This will be used as fallback in backend
201
+ email: userInfo?.email || null, // Backend will use this if available
202
+ modelKey: modelKey || null,
203
+ connectedPlatforms: platformsList, // Include connected platforms
204
+ // Include encrypted PIN if available
205
+ ...(encryptedPin && {
206
+ encryptedPin: encryptedPin,
207
+ hasPinData: true
208
+ })
209
+ };
210
+
211
+ console.log('📤 Starting training with YouTube migration check:', trainingData);
212
+ console.log('🔑 Using stored username:', storedUsername);
213
+ console.log('🔑 Final username for training:', finalUsername);
214
+ console.log('🔗 Connected platforms:', platformsList);
215
+ console.log('🔐 PIN included in training:', !!encryptedPin);
216
+
217
+ // Use the new training function that includes YouTube migration check
218
+ const result = await startEnochTrainingWithYouTubeCheck(trainingData);
219
+
220
+ if (result.success) {
221
+ console.log('🚀 Training Started:', result.message);
222
+ console.log('🎯 Training Features:', result.features);
223
+
224
+ // Log the new features from the spec
225
+ if (result.features) {
226
+ console.log('✅ Inference enabled:', result.features.inference);
227
+ console.log('💾 Storage method:', result.features.storage);
228
+ console.log('🔒 Compression enabled:', result.features.compression);
229
+ console.log('🔐 Encryption enabled:', result.features.encryption);
230
+ console.log('📊 Training type:', result.features.type);
231
+ console.log('🗄️ Databases:', result.features.databases);
232
+ console.log('📈 Query scores enabled:', result.features.queryScores);
233
+ }
234
+
235
+ setTrainingStatus('Training model...');
236
+ setProgress(20);
237
+ } else {
238
+ console.error('Training start failed:', result.error);
239
+ setTrainingStatus(`Error: ${result.error}`);
240
+ setHasError(true);
241
+ }
242
+ } catch (error) {
243
+ console.error('Training start error:', error);
244
+ setTrainingStatus(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
245
+ setHasError(true);
246
+ }
247
+ };
248
+
249
+ // Setup socket connection and training
250
+ useEffect(() => {
251
+ if (!visible || !userToken || !userInfo) return;
252
+
253
+ console.log('Setting up socket connection for training...');
254
+ console.log('🧑‍💻 User info available:', userInfo);
255
+
256
+ // Initialize socket connection
257
+ socketRef.current = io('https://api2.onairos.uk', {
258
+ transports: ['websocket'],
259
+ autoConnect: false
260
+ });
261
+
262
+ // Socket event listeners
263
+ socketRef.current.on('connect', () => {
264
+ console.log('✅ Socket connected for training');
265
+ setSocketConnected(true);
266
+ const socketId = socketRef.current?.id;
267
+ if (socketId) {
268
+ // ❌ DISABLED: This was causing training to start too early (when modal opens)
269
+ // Training should only start when transitioning from connectors to PIN screen
270
+ // startEnochTraining(socketId);
271
+ console.log('🔌 Socket ready, but training will start only when user transitions to PIN screen');
272
+ }
273
+ });
274
+
275
+ socketRef.current.on('disconnect', () => {
276
+ console.log('❌ Socket disconnected');
277
+ setSocketConnected(false);
278
+ });
279
+
280
+ socketRef.current.on('trainingCompleted', (data) => {
281
+ console.log('✅ Training Complete:', data);
282
+ setTrainingStatus('Running test inference...');
283
+ setProgress(60);
284
+ });
285
+
286
+ socketRef.current.on('inferenceCompleted', (data) => {
287
+ console.log('🧠 Inference Complete:', data);
288
+ setTrainingStatus('Uploading to S3...');
289
+ setProgress(80);
290
+ setUserTraits(data.traits);
291
+ setInferenceResults(data.inferenceResults);
292
+ });
293
+
294
+ socketRef.current.on('modelStandby', (data) => {
295
+ console.log('🎉 All Complete:', data);
296
+
297
+ // Log completion details based on new spec
298
+ if (data.completed) {
299
+ console.log('✅ Training completed:', data.message);
300
+ console.log('💾 Storage method:', data.storage);
301
+ console.log('🔐 Encryption enabled:', data.encryption);
302
+ console.log('🧠 Inference enabled:', data.inference);
303
+
304
+ // Log database info for Enoch mode
305
+ if (data.databases && Array.isArray(data.databases)) {
306
+ console.log('🗄️ Databases used:', data.databases.join(', '));
307
+ }
308
+
309
+ // Log testing mode
310
+ if (data.testing) {
311
+ console.log('🧪 Testing mode enabled');
312
+ }
313
+ }
314
+
315
+ setIsTrainingComplete(true);
316
+ setTrainingStatus('Complete!');
317
+ setProgress(100);
318
+
319
+ // Clear temporary PIN after training is complete
320
+ clearTemporaryPin();
321
+
322
+ // Show continue button for primary auth when training is complete
323
+ if (isPrimaryAuth) {
324
+ setMatchStep('found');
325
+ setMatchCount(Math.floor(Math.random() * 10) + 5);
326
+ }
327
+ });
328
+
329
+ socketRef.current.on('trainingUpdate', (data) => {
330
+ if (data.error) {
331
+ console.error('Training update error:', data.error);
332
+ setTrainingStatus(`Error: ${data.error}`);
333
+ setHasError(true);
334
+ } else if (data.progress) {
335
+ setProgress(data.progress);
336
+ setTrainingStatus(data.status || 'Training in progress...');
337
+ }
338
+ });
339
+
340
+ // Connect to socket
341
+ socketRef.current.connect();
342
+
343
+ // Cleanup function
344
+ return () => {
345
+ if (socketRef.current) {
346
+ console.log('🔌 Disconnecting training socket...');
347
+ socketRef.current.disconnect();
348
+ socketRef.current = null;
349
+ }
350
+ };
351
+ }, [visible, userToken, userInfo]);
352
+
353
+ // When modal becomes visible and autoFocus is requested, focus the email input
354
+ useEffect(() => {
355
+ if (visible && autoFocusEmailInput && matchStep === 'email' && emailInputRef.current) {
356
+ const timer = setTimeout(() => {
357
+ emailInputRef.current?.focus();
358
+ }, 150); // Small delay to sync with modal animation
359
+ return () => clearTimeout(timer);
360
+ } else if (!visible && emailInputRef.current) {
361
+ // Optional: Blur input when modal is not visible
362
+ // emailInputRef.current?.blur();
363
+ }
364
+ }, [visible, autoFocusEmailInput, matchStep]);
365
+
366
+ // Validate email format
367
+ const validateEmail = (email: string) => {
368
+ // Check for reviewer bypass first
369
+ if (email.toLowerCase().trim() === 'reviewer') {
370
+ return true; // Valid for reviewer bypass
371
+ }
372
+
373
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
374
+ return emailRegex.test(email);
375
+ };
376
+
377
+ const handleEmailSubmit = async () => {
378
+ // Check for reviewer bypass
379
+ if (email.toLowerCase().trim() === 'reviewer') {
380
+ console.log('🔍 Reviewer bypass detected');
381
+ setEmail('reviewer@bypass.com'); // Set a valid email for the flow
382
+ if (onComplete) {
383
+ onComplete('reviewer@bypass.com');
384
+ }
385
+ return;
386
+ }
387
+
388
+ // Regular email validation and submission
389
+ if (!validateEmail(email)) {
390
+ setEmailError('Please enter a valid email address');
391
+ return;
392
+ }
393
+
394
+ setIsSubmittingEmail(true);
395
+ setEmailError('');
396
+
397
+ try {
398
+ console.log('📧 Sending verification code to:', email);
399
+
400
+ // Send verification code using the new API
401
+ // const result = await sendEmailVerificationCode(email); // Function not available
402
+ const result = { success: true, message: 'Email verification not implemented' };
403
+
404
+ if (result.success) {
405
+ console.log('✅ Verification code sent successfully');
406
+ setMatchStep('verification'); // Move to verification step
407
+ } else {
408
+ console.log('❌ Failed to send verification code:', result.message);
409
+ setEmailError(result.message || 'Failed to send verification code. Please try again.');
410
+ }
411
+ } catch (error) {
412
+ console.error('❌ Error sending verification code:', error);
413
+ setEmailError('Network error. Please check your connection and try again.');
414
+ } finally {
415
+ setIsSubmittingEmail(false);
416
+ }
417
+ };
418
+
419
+ const handleVerificationComplete = async (verifiedEmail: string, isVerified: boolean, accountInfo?: any) => {
420
+ if (isVerified) {
421
+ console.log('✅ Email verification completed for:', verifiedEmail);
422
+ console.log('🔍 AccountInfo received:', accountInfo);
423
+ console.log('🔍 ExistingUser flag:', accountInfo?.existingUser);
424
+
425
+ // Check if this is an existing user - must have accountInfo AND explicitly marked as existing
426
+ if (accountInfo && accountInfo.existingUser === true) {
427
+ console.log('🔍 Existing user detected:', accountInfo);
428
+
429
+ // CRITICAL FIX: Ensure we have a valid JWT token before proceeding
430
+ if (accountInfo.token && typeof accountInfo.token === 'string' && accountInfo.token.length > 50) {
431
+ console.log('✅ Valid JWT token found for existing user');
432
+
433
+ // Store the token immediately for existing users
434
+ await AsyncStorage.setItem('onairos_jwt_token', accountInfo.token);
435
+ await AsyncStorage.setItem('enoch_token', accountInfo.token);
436
+ await AsyncStorage.setItem('auth_token', accountInfo.token);
437
+ await AsyncStorage.setItem('user_email', verifiedEmail);
438
+ await AsyncStorage.setItem('auth_method', 'onairos');
439
+
440
+ console.log('✅ Existing user tokens stored successfully');
441
+
442
+ // NEW: Show existing user confirmation modal instead of immediately completing
443
+ setExistingUserAccountInfo(accountInfo);
444
+ setMatchStep('existing_user');
445
+ return;
446
+ } else {
447
+ console.error('❌ No valid JWT token for existing user - token:', accountInfo.token);
448
+ console.error('❌ Cannot proceed without valid authentication token');
449
+
450
+ // Don't fall back to email - throw error instead
451
+ throw new Error('No valid authentication token received from server');
452
+ }
453
+ return;
454
+ }
455
+
456
+ console.log('🔍 New user detected - proceeding with account creation flow');
457
+
458
+ // New user - proceed with account creation and continue to platform connectors
459
+ try {
460
+ console.log('📧 Creating Onairos account for verified email:', verifiedEmail);
461
+
462
+ const username = verifiedEmail.split('@')[0];
463
+
464
+ const onairosSignupResponse = await fetch('https://api2.onairos.uk/register/enoch', {
465
+ method: 'POST',
466
+ headers: {
467
+ 'Content-Type': 'application/json'
468
+ },
469
+ body: JSON.stringify({
470
+ email: verifiedEmail,
471
+ username: username,
472
+ name: username,
473
+ emailVerified: true // Mark as verified
474
+ })
475
+ });
476
+
477
+ const onairosResponseData = await onairosSignupResponse.json();
478
+ console.log('🔗 Onairos account creation response:', onairosResponseData);
479
+
480
+ if (onairosResponseData.token) {
481
+ // Store tokens
482
+ await AsyncStorage.setItem('onairos_jwt_token', onairosResponseData.token);
483
+ await AsyncStorage.setItem('auth_token', onairosResponseData.token);
484
+ await AsyncStorage.setItem('enoch_token', onairosResponseData.token);
485
+ await AsyncStorage.setItem('onairos_username', onairosResponseData.username);
486
+ await AsyncStorage.setItem('user_email', verifiedEmail);
487
+
488
+ console.log('✅ Account created and tokens stored');
489
+ console.log('🔗 Proceeding to UniversalOnboarding platform connectors');
490
+
491
+ // Close the TrainingModal and proceed to UniversalOnboarding platform connectors
492
+ if (onComplete) {
493
+ // This is the account creation path for new users
494
+ // Always proceed to platform connectors for new users, regardless of backend response
495
+ console.log('📧 TrainingModal: New user account created - proceeding to platform connectors:', verifiedEmail);
496
+ onComplete(verifiedEmail);
497
+ }
498
+ } else {
499
+ // No token returned, continue with email to platform connectors
500
+ console.log('🔗 No token returned, proceeding to platform connectors with email');
501
+ // CRITICAL FIX: Store email even when account creation fails
502
+ await AsyncStorage.setItem('user_email', verifiedEmail);
503
+ console.log('✅ Email stored despite no token for admin check compatibility');
504
+ if (onComplete) {
505
+ onComplete(verifiedEmail);
506
+ }
507
+ }
508
+ } catch (error) {
509
+ console.error('❌ Error creating account after verification:', error);
510
+ // Continue anyway to platform connectors
511
+ console.log('🔗 Error occurred, but proceeding to platform connectors');
512
+ // CRITICAL FIX: Store email even when account creation throws an error
513
+ await AsyncStorage.setItem('user_email', verifiedEmail);
514
+ console.log('✅ Email stored despite error for admin check compatibility');
515
+ if (onComplete) {
516
+ onComplete(verifiedEmail);
517
+ }
518
+ }
519
+ }
520
+ };
521
+
522
+ const handleVerificationBack = () => {
523
+ setMatchStep('email');
524
+ setEmailError('');
525
+ };
526
+
527
+ // Handle existing user confirmation
528
+ const handleExistingUserConfirm = async () => {
529
+ console.log('✅ Existing user confirmed data sharing');
530
+
531
+ if (existingUserAccountInfo && existingUserAccountInfo.token) {
532
+ if (onComplete) {
533
+ onComplete(existingUserAccountInfo.token);
534
+ }
535
+ } else {
536
+ console.error('❌ No token available for existing user');
537
+ setEmailError('Authentication error. Please try again.');
538
+ setMatchStep('email');
539
+ }
540
+ };
541
+
542
+ const handleExistingUserCancel = () => {
543
+ console.log('❌ Existing user cancelled data sharing');
544
+ setMatchStep('email');
545
+ setExistingUserAccountInfo(null);
546
+ setEmailError('');
547
+ };
548
+
549
+ const handleExistingUserAddMoreData = () => {
550
+ console.log('🔗 Existing user wants to add more data');
551
+
552
+ // Store the existing user info for later use
553
+ if (existingUserAccountInfo && existingUserAccountInfo.token) {
554
+ AsyncStorage.setItem('existing_user_token', existingUserAccountInfo.token);
555
+ AsyncStorage.setItem('existing_user_info', JSON.stringify(existingUserAccountInfo));
556
+ }
557
+
558
+ // Continue to new user flow (UniversalOnboarding) but with existing user context
559
+ setMatchStep('email');
560
+ setExistingUserAccountInfo(null);
561
+
562
+ // Call onComplete with email to trigger UniversalOnboarding
563
+ if (onComplete) {
564
+ onComplete(email);
565
+ }
566
+ };
567
+
568
+ // Ensure cancel button always works
569
+ const handleCancelPress = () => {
570
+ // Clear temporary PIN when cancelling
571
+ clearTemporaryPin();
572
+ onCancel();
573
+ };
574
+ const handleContinue = () => {
575
+ // Trigger haptic feedback when button is pressed
576
+ ReactNativeHapticFeedback.trigger('impactMedium', {
577
+ enableVibrateFallback: true,
578
+ ignoreAndroidSystemSettings: false
579
+ });
580
+
581
+ console.log('handleContinue called. Current email state:', email);
582
+ console.log('Continue button pressed, completing onboarding with email:', email);
583
+
584
+ // Make sure we have a valid email
585
+ if (!validateEmail(email)) {
586
+ console.log('Email validation failed, cannot continue');
587
+ return;
588
+ }
589
+
590
+ // Show loading state
591
+ setIsContinueLoading(true);
592
+
593
+ // Call onComplete immediately to prevent splash screen flash
594
+ // The loading state will still be visible during the transition
595
+ if (onComplete) {
596
+ console.log('Calling onComplete with email:', email);
597
+ onComplete(email);
598
+ // Note: We don't reset isContinueLoading because the component will unmount
599
+ }
600
+ };
601
+
602
+ // Calculate user-friendly message based on progress
603
+ const getUserFriendlyMessage = () => {
604
+ if (hasError) return 'Something went wrong. Please try again.';
605
+ if (isTrainingComplete) return 'Your persona is ready! 🎉';
606
+
607
+ if (progress < 20) {
608
+ return 'Keeping your data private...';
609
+ } else if (progress < 40) {
610
+ return 'Trying to understand your mind...';
611
+ } else if (progress < 60) {
612
+ return 'You\'re more interesting than I expected...';
613
+ } else if (progress < 80) {
614
+ return 'Finalizing your unique persona...';
615
+ } else if (progress < 95) {
616
+ return 'Almost done...';
617
+ } else {
618
+ return 'Just a few more seconds...';
619
+ }
620
+ };
621
+
622
+ // Calculate remaining time estimate
623
+ const getTimeEstimate = () => {
624
+ if (hasError) return 'Please try again';
625
+ if (isTrainingComplete) return 'Complete!';
626
+
627
+ // Rough time estimates based on progress
628
+ const remainingProgress = 100 - progress;
629
+ const estimatedSeconds = Math.ceil((remainingProgress / 100) * 120); // Assume 2 minutes total
630
+
631
+ if (estimatedSeconds > 60) {
632
+ return `About ${Math.ceil(estimatedSeconds / 60)} minute${Math.ceil(estimatedSeconds / 60) > 1 ? 's' : ''} remaining`;
633
+ } else if (estimatedSeconds > 0) {
634
+ return `About ${estimatedSeconds} seconds remaining`;
635
+ } else {
636
+ return 'Almost there...';
637
+ }
638
+ };
639
+
640
+ // Grabber bar
641
+ const Grabber = () => (
642
+ <View style={styles.grabberContainer}>
643
+ <View style={styles.grabberBar} />
644
+ </View>
645
+ );
646
+
647
+ if (!visible) return null;
648
+
649
+ const progressPercentage = Math.min(Math.max(progress, 0), 100);
650
+
651
+ return (
652
+ <Modal
653
+ visible={visible}
654
+ transparent
655
+ animationType="none" // Changed to none since we're handling animation
656
+ onRequestClose={handleModalClose}
657
+ >
658
+ <TouchableWithoutFeedback onPress={handleModalClose}>
659
+ <View style={styles.modalOverlay}>
660
+ <KeyboardAvoidingView
661
+ behavior={Platform.OS === "ios" ? "padding" : "height"}
662
+ keyboardVerticalOffset={0}
663
+ style={styles.keyboardAvoidingView}
664
+ >
665
+ <TouchableWithoutFeedback onPress={(e) => e.stopPropagation()}>
666
+ <Animated.View
667
+ style={[
668
+ styles.modalContent,
669
+ {
670
+ transform: [{ translateY: modalTranslateY }],
671
+ }
672
+ ]}
673
+ >
674
+ <Grabber />
675
+
676
+ {/* START: Add X Close Button */}
677
+ <TouchableOpacity style={styles.closeButton} onPress={onCancel}>
678
+ <Text style={styles.closeButtonText}>✕</Text>
679
+ </TouchableOpacity>
680
+ {/* END: Add X Close Button */}
681
+
682
+ {matchStep === 'email' && (
683
+ <View style={[styles.primaryAuthHeader, { marginTop: 40 }]}>
684
+ <Text style={styles.primaryAuthTitle}>Sign in with Onairos</Text>
685
+ <Text style={styles.primaryAuthSubtitle}>
686
+ </Text>
687
+ </View>
688
+ )}
689
+
690
+
691
+
692
+ {/* Email Entry Step */}
693
+ {isPrimaryAuth && matchStep === 'email' && (
694
+ <View style={styles.emailSection}>
695
+ <Text style={styles.emailLabel}>Enter your email to sign in / create an account</Text>
696
+ <TextInput
697
+ ref={emailInputRef}
698
+ style={[styles.emailInput, emailError ? styles.emailInputError : null]}
699
+ value={email}
700
+ onChangeText={(text) => {
701
+ setEmail(text);
702
+ if (emailError) setEmailError('');
703
+ }}
704
+ placeholder="your.email@example.com"
705
+ keyboardType="email-address"
706
+ autoCapitalize="none"
707
+ autoCorrect={false}
708
+ returnKeyType="done"
709
+ onSubmitEditing={() => Keyboard.dismiss()}
710
+ />
711
+ {emailError ? <Text style={styles.errorText}>{emailError}</Text> : null}
712
+ <TouchableOpacity
713
+ style={[
714
+ styles.continueButton,
715
+ (isSubmittingEmail || (!email.includes('@') && email.toLowerCase().trim() !== 'reviewer')) && styles.disabledButton,
716
+ { marginTop: 24 }
717
+ ]}
718
+ onPress={handleEmailSubmit}
719
+ disabled={isSubmittingEmail || (!email.includes('@') && email.toLowerCase().trim() !== 'reviewer')}
720
+ activeOpacity={0.8}
721
+ >
722
+ {isSubmittingEmail ? (
723
+ <ActivityIndicator size="small" color="#FFFFFF" />
724
+ ) : (
725
+ <Text style={styles.continueButtonText}>Continue</Text>
726
+ )}
727
+ </TouchableOpacity>
728
+ </View>
729
+ )}
730
+
731
+ {/* Email Verification Step */}
732
+ {isPrimaryAuth && matchStep === 'verification' && (
733
+ <View>
734
+ {/* EmailVerification component not available - placeholder */}
735
+ <Text>Email verification step (not implemented)</Text>
736
+ </View>
737
+ )}
738
+
739
+ {/* Existing User Data Confirmation Step */}
740
+ {isPrimaryAuth && matchStep === 'existing_user' && existingUserAccountInfo && (
741
+ <ExistingUserDataConfirmation
742
+ visible={true}
743
+ onConfirm={handleExistingUserConfirm}
744
+ onCancel={handleExistingUserCancel}
745
+ onAddMoreData={handleExistingUserAddMoreData}
746
+ accountInfo={existingUserAccountInfo}
747
+ userEmail={email}
748
+ />
749
+ )}
750
+
751
+ {/* Existing steps remain the same */}
752
+ {isPrimaryAuth && matchStep === 'searching' && (
753
+ <View style={styles.matchContainer}>
754
+ <Text style={styles.matchTitle}></Text>
755
+ <ActivityIndicator size="large" color="#E9C46A" style={styles.matchLoader} />
756
+ <Text style={styles.matchMessage}>
757
+ Searching for potential connections based on your email...
758
+ </Text>
759
+ </View>
760
+ )}
761
+
762
+ {isPrimaryAuth && matchStep === 'found' && progress >= 100 && (
763
+ <View style={styles.matchContainer}>
764
+ <Text style={styles.matchTitle}>Connections Found</Text>
765
+ <View style={styles.matchCircle}>
766
+ <Text style={styles.matchCount}>{matchCount}</Text>
767
+ </View>
768
+ <Text style={styles.matchMessage}>
769
+ We found {matchCount} potential connections for you!
770
+ </Text>
771
+ <Text style={styles.matchSubMessage}>
772
+ Complete your profile to see your personalized matches
773
+ </Text>
774
+ <TouchableOpacity
775
+ style={[
776
+ styles.continueButton,
777
+ isContinueLoading && styles.disabledButton
778
+ ]}
779
+ onPress={handleContinue}
780
+ disabled={isContinueLoading}
781
+ >
782
+ {isContinueLoading ? (
783
+ <ActivityIndicator size="small" color="#FFFFFF" />
784
+ ) : (
785
+ <Text style={styles.continueButtonText}>continue to setup</Text>
786
+ )}
787
+ </TouchableOpacity>
788
+ </View>
789
+ )}
790
+ </Animated.View>
791
+ </TouchableWithoutFeedback>
792
+ </KeyboardAvoidingView>
793
+ </View>
794
+ </TouchableWithoutFeedback>
795
+ </Modal>
796
+ );
797
+ };
798
+
799
+ const styles = StyleSheet.create({
800
+ container: {
801
+ flex: 1,
802
+ backgroundColor: '#fff',
803
+ padding: 24,
804
+ },
805
+ scrollContentContainer: {
806
+ flexGrow: 1,
807
+ justifyContent: 'flex-start',
808
+ paddingBottom: 0,
809
+ marginBottom: 0,
810
+ },
811
+ actualContentWrapper: {
812
+ width: '100%',
813
+ alignItems: 'center', // Center content like headers if they are not full width
814
+ },
815
+ content: {
816
+ // This style might be unused or redefined if still needed for specific children
817
+ // flex: 1, // Original content style, likely not needed for the wrapper anymore
818
+ },
819
+ title: {
820
+ fontSize: 24,
821
+ fontWeight: '600',
822
+ color: '#333',
823
+ marginBottom: 24,
824
+ textAlign: 'center',
825
+ },
826
+ subtitle: {
827
+ fontSize: 16,
828
+ color: '#666',
829
+ marginBottom: 32,
830
+ textAlign: 'center',
831
+ },
832
+ progressContainer: {
833
+ marginBottom: 24,
834
+ },
835
+ progressBar: {
836
+ height: 8,
837
+ backgroundColor: '#F5F5F5',
838
+ borderRadius: 4,
839
+ overflow: 'hidden',
840
+ marginBottom: 8,
841
+ },
842
+ progressFill: {
843
+ height: '100%',
844
+ backgroundColor: '#1BA9D4',
845
+ borderRadius: 4,
846
+ },
847
+ progressText: {
848
+ fontSize: 14,
849
+ color: '#666',
850
+ textAlign: 'right',
851
+ },
852
+ infoContainer: {
853
+ flexDirection: 'row',
854
+ alignItems: 'center',
855
+ marginBottom: 32,
856
+ },
857
+ etaText: {
858
+ fontSize: 14,
859
+ color: '#666',
860
+ marginLeft: 8,
861
+ textAlign: 'center',
862
+ marginBottom: 16,
863
+ },
864
+ detailText: {
865
+ fontSize: 14,
866
+ color: '#666',
867
+ lineHeight: 20,
868
+ },
869
+ emailSection: {
870
+ marginTop: 24,
871
+ marginBottom: 16,
872
+ width: '100%',
873
+ },
874
+ emailSectionPrimary: {
875
+ marginTop: 40,
876
+ marginBottom: 40,
877
+ },
878
+ emailLabel: {
879
+ fontSize: 16,
880
+ fontWeight: '500',
881
+ color: '#333',
882
+ marginBottom: 12,
883
+ textAlign: 'center',
884
+ },
885
+ emailInput: {
886
+ height: 48,
887
+ borderWidth: 1,
888
+ borderColor: '#E0E0E0',
889
+ borderRadius: 8,
890
+ paddingHorizontal: 16,
891
+ fontSize: 16,
892
+ marginBottom: 8,
893
+ },
894
+ emailInputError: {
895
+ borderColor: '#FF3B30',
896
+ },
897
+ errorText: {
898
+ color: '#FF3B30',
899
+ fontSize: 14,
900
+ marginBottom: 16,
901
+ },
902
+ footer: {
903
+ flexDirection: 'row',
904
+ justifyContent: 'space-between',
905
+ marginTop: 24,
906
+ paddingTop: 16,
907
+ },
908
+ completeButton: {
909
+ paddingVertical: 12,
910
+ paddingHorizontal: 24,
911
+ backgroundColor: '#1BA9D4',
912
+ borderRadius: 8,
913
+ },
914
+ disabledButton: {
915
+ backgroundColor: '#CCCCCC',
916
+ opacity: 0.7,
917
+ },
918
+ completeButtonText: {
919
+ color: '#fff',
920
+ fontSize: 16,
921
+ fontWeight: '600',
922
+ },
923
+ statusText: {
924
+ fontSize: 14,
925
+ color: '#666',
926
+ marginLeft: 8,
927
+ },
928
+ progressError: {
929
+ backgroundColor: '#FF3B30',
930
+ },
931
+ primaryAuthHeader: {
932
+ alignItems: 'center',
933
+ marginBottom: 24,
934
+ paddingHorizontal: 16,
935
+ },
936
+ primaryAuthTitle: {
937
+ fontSize: 24,
938
+ fontWeight: '600',
939
+ color: '#333',
940
+ marginBottom: 8,
941
+ textAlign: 'center',
942
+ },
943
+ primaryAuthSubtitle: {
944
+ fontSize: 16,
945
+ color: '#555',
946
+ textAlign: 'center',
947
+ marginBottom: 16,
948
+ },
949
+ matchContainer: {
950
+ alignItems: 'center',
951
+ padding: 20,
952
+ },
953
+ matchTitle: {
954
+ fontSize: 24,
955
+ fontWeight: '600',
956
+ color: '#333',
957
+ marginBottom: 16,
958
+ textAlign: 'center',
959
+ },
960
+ matchLoader: {
961
+ marginVertical: 20,
962
+ },
963
+ matchMessage: {
964
+ fontSize: 16,
965
+ color: '#555',
966
+ textAlign: 'center',
967
+ marginVertical: 12,
968
+ },
969
+ matchSubMessage: {
970
+ fontSize: 14,
971
+ color: '#777',
972
+ textAlign: 'center',
973
+ marginTop: 8,
974
+ },
975
+ matchCircle: {
976
+ width: 80,
977
+ height: 80,
978
+ borderRadius: 40,
979
+ backgroundColor: '#E9C46A',
980
+ justifyContent: 'center',
981
+ alignItems: 'center',
982
+ marginVertical: 16,
983
+ },
984
+ matchCount: {
985
+ fontSize: 32,
986
+ fontWeight: 'bold',
987
+ color: '#FFFFFF',
988
+ },
989
+ continueButton: {
990
+ marginTop: 40,
991
+ width: '85%',
992
+ height: 50,
993
+ backgroundColor: '#000000',
994
+ borderRadius: 8,
995
+ justifyContent: 'center',
996
+ alignItems: 'center',
997
+ alignSelf: 'center',
998
+ },
999
+ continueButtonText: {
1000
+ color: '#FFFFFF',
1001
+ fontSize: 18,
1002
+ fontWeight: '600',
1003
+ textAlign: 'center',
1004
+ },
1005
+ modalOverlay: {
1006
+ flex: 1,
1007
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
1008
+ justifyContent: 'flex-end',
1009
+ },
1010
+ keyboardAvoidingView: {
1011
+ flex: 1,
1012
+ justifyContent: 'flex-end',
1013
+ alignItems: 'stretch',
1014
+ margin: 0,
1015
+ padding: 0,
1016
+ },
1017
+ modalContent: {
1018
+ backgroundColor: '#fff',
1019
+ borderTopLeftRadius: 20,
1020
+ borderTopRightRadius: 20,
1021
+ padding: 24,
1022
+ paddingTop: 12,
1023
+ minHeight: height * 0.4,
1024
+ maxHeight: height * 0.9,
1025
+ },
1026
+ closeButton: {
1027
+ position: 'absolute',
1028
+ top: 15, // Adjust as needed for visual balance with grabber and padding
1029
+ right: 20, // Adjust as needed
1030
+ padding: 5, // Make it easier to tap
1031
+ zIndex: 10, // Ensure it's above other content
1032
+ },
1033
+ closeButtonText: {
1034
+ fontSize: 24, // Make it a bit larger
1035
+ color: '#888', // Subtle color
1036
+ fontWeight: '300',
1037
+ },
1038
+ grabberContainer: {
1039
+ alignItems: 'center',
1040
+ },
1041
+ grabberBar: {
1042
+ width: 48,
1043
+ height: 5,
1044
+ borderRadius: 3,
1045
+ backgroundColor: '#E0E0E0',
1046
+ },
1047
+ });