@onairos/react-native 3.0.54 → 3.0.56

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.
@@ -22,11 +22,11 @@ import Icon from 'react-native-vector-icons/MaterialIcons';
22
22
  import { PlatformList } from './PlatformList';
23
23
  import { PinInput } from './PinInput';
24
24
  import { TrainingModal } from './TrainingModal';
25
+ import { DataRequestModal } from './DataRequestModal';
25
26
  import { OAuthWebView } from './onboarding/OAuthWebView';
26
27
  import { useConnections } from '../hooks/useConnections';
27
28
  import { COLORS, DEEP_LINK_CONFIG } from '../constants';
28
29
  import { initiateOAuth, initiateNativeAuth, hasNativeSDK, isOAuthCallback, testApiConnectivity, handleOAuthCallbackUrl, refreshYouTubeTokens, requestEmailVerification, verifyEmailCode, checkEmailVerificationStatus, disconnectPlatform } from '../services/platformAuthService';
29
- import { EmailVerificationModal } from './EmailVerificationModal';
30
30
  import type { UniversalOnboardingProps, ConnectionStatus } from '../types';
31
31
 
32
32
  // Optional Opacity SDK imports with error handling
@@ -61,7 +61,7 @@ export const UniversalOnboarding: React.FC<UniversalOnboardingProps> = ({
61
61
  auto = false,
62
62
  partner,
63
63
  }) => {
64
- const [step, setStep] = useState<'email' | 'connect' | 'pin' | 'training' | 'oauth' | 'success'>('email');
64
+ const [step, setStep] = useState<'email' | 'verify' | 'dataRequest' | 'connect' | 'pin' | 'training' | 'oauth' | 'success'>('email');
65
65
  const [connections, setConnections] = useState<ConnectionStatus>({});
66
66
  const [pin, setPin] = useState<string>('');
67
67
  const [selectedTier, setSelectedTier] = useState<'Small' | 'Medium' | 'Large'>('Medium');
@@ -77,7 +77,10 @@ export const UniversalOnboarding: React.FC<UniversalOnboardingProps> = ({
77
77
  const [isConnectingPlatform, setIsConnectingPlatform] = useState<boolean>(false);
78
78
  const [showLoginWebView, setShowLoginWebView] = useState<boolean>(false);
79
79
  const [email, setEmail] = useState<string>('');
80
- const [showEmailVerification, setShowEmailVerification] = useState<boolean>(false);
80
+ const [verificationCode, setVerificationCode] = useState<string>('');
81
+ const [isVerifyingCode, setIsVerifyingCode] = useState<boolean>(false);
82
+ const [showDataRequestModal, setShowDataRequestModal] = useState<boolean>(false);
83
+ const [isExistingUser, setIsExistingUser] = useState<boolean>(false);
81
84
 
82
85
  // Add refs for cleanup
83
86
  const successTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -237,23 +240,12 @@ export const UniversalOnboarding: React.FC<UniversalOnboardingProps> = ({
237
240
 
238
241
  console.log('✅ API connectivity confirmed');
239
242
 
240
- // Instagram: Use Opacity SDK exclusively (if available)
243
+ // Instagram: Use Opacity SDK exclusively
241
244
  if (platformId === 'instagram') {
242
245
  // Check if Opacity SDK is available
243
246
  if (!opacityInit || !OpacityEnvironment || !opacityGet) {
244
- console.warn('⚠️ Opacity SDK not available, falling back to OAuth for Instagram');
245
- // Fall back to OAuth flow for Instagram
246
- const oauthUrl = await initiateOAuth(platformId, username, AppName);
247
-
248
- if (oauthUrl) {
249
- console.log(`✅ Received OAuth URL for ${platformId}:`, oauthUrl);
250
- setCurrentPlatform(platformId);
251
- setOauthUrl(oauthUrl);
252
- setStep('oauth');
253
- } else {
254
- throw new Error(`Failed to get authorization URL for ${platformId}. Please try again.`);
255
- }
256
- return;
247
+ console.error(' Opacity SDK not available for Instagram');
248
+ throw new Error('Instagram connection requires the Opacity SDK. Please ensure @opacity-labs/react-native-opacity is properly installed and configured.');
257
249
  }
258
250
 
259
251
  console.log('🔌 Initializing Opacity SDK for Instagram...');
@@ -461,7 +453,7 @@ export const UniversalOnboarding: React.FC<UniversalOnboardingProps> = ({
461
453
  }, []);
462
454
 
463
455
  // Function to handle email submission
464
- const handleEmailSubmit = useCallback(() => {
456
+ const handleEmailSubmit = useCallback(async () => {
465
457
  if (!email.trim()) {
466
458
  Alert.alert('Error', 'Please enter your email address');
467
459
  return;
@@ -475,34 +467,60 @@ export const UniversalOnboarding: React.FC<UniversalOnboardingProps> = ({
475
467
  }
476
468
 
477
469
  console.log('📧 Email submitted:', email.trim());
478
- setShowEmailVerification(true);
470
+
471
+ // Request verification code
472
+ try {
473
+ const result = await requestEmailVerification(email.trim());
474
+ if (result.success) {
475
+ console.log('✅ Verification code requested');
476
+ setStep('verify');
477
+ } else {
478
+ Alert.alert('Error', result.error || 'Failed to send verification code');
479
+ }
480
+ } catch (error) {
481
+ console.error('❌ Error requesting verification:', error);
482
+ Alert.alert('Error', 'Failed to send verification code');
483
+ }
479
484
  }, [email]);
480
485
 
481
- // Function to handle email verification completion
482
- const handleEmailVerificationComplete = useCallback((verifiedEmail: string, isExistingUser: boolean) => {
483
- console.log('✅ Email verification completed:', { verifiedEmail, isExistingUser });
484
- setShowEmailVerification(false);
486
+ // Function to handle verification code submission
487
+ const handleVerificationSubmit = useCallback(async () => {
488
+ if (!verificationCode.trim() || verificationCode.trim().length !== 6) {
489
+ Alert.alert('Error', 'Please enter a 6-digit verification code');
490
+ return;
491
+ }
485
492
 
486
- if (isExistingUser) {
487
- console.log('Existing user detected, skipping onboarding');
488
- onComplete('https://api2.onairos.uk', 'existing-session-token', {
489
- existingAccount: true,
490
- email: verifiedEmail,
491
- skipOnboarding: true,
492
- });
493
- } else {
494
- console.log('New user, proceeding to platform connection');
495
- setUsername(verifiedEmail.split('@')[0]); // Use email prefix as username
496
- setStep('connect');
493
+ setIsVerifyingCode(true);
494
+
495
+ try {
496
+ const result = await verifyEmailCode(email.trim(), verificationCode.trim());
497
+
498
+ if (result.success) {
499
+ console.log('✅ Email verification successful');
500
+
501
+ // For now, always treat as new users for testing (can be updated later)
502
+ // In production, this would check if user exists in database
503
+ const existingUser = false; // TODO: Check backend for existing user
504
+ setIsExistingUser(existingUser);
505
+
506
+ if (existingUser) {
507
+ console.log('Existing user detected, showing data request modal');
508
+ setShowDataRequestModal(true);
509
+ } else {
510
+ console.log('New user, proceeding to platform connection');
511
+ setUsername(email.split('@')[0]); // Use email prefix as username
512
+ setStep('connect');
513
+ }
514
+ } else {
515
+ Alert.alert('Verification Failed', result.error || 'Invalid verification code');
516
+ }
517
+ } catch (error) {
518
+ console.error('❌ Error verifying code:', error);
519
+ Alert.alert('Error', 'Failed to verify code');
520
+ } finally {
521
+ setIsVerifyingCode(false);
497
522
  }
498
- }, [onComplete]);
499
-
500
- // Function to handle email verification failure
501
- const handleEmailVerificationFailed = useCallback((error: string) => {
502
- console.error('❌ Email verification failed:', error);
503
- setShowEmailVerification(false);
504
- Alert.alert('Verification Failed', error);
505
- }, []);
523
+ }, [email, verificationCode, onComplete]);
506
524
 
507
525
  const handlePinSubmit = useCallback(async (userPin: string) => {
508
526
  setPin(userPin);
@@ -537,39 +555,54 @@ export const UniversalOnboarding: React.FC<UniversalOnboardingProps> = ({
537
555
  } catch (error) {
538
556
  console.error('Failed to save session data:', error);
539
557
  }
558
+ }, [connections, selectedTier, platformToggles, username, AppName, auto, inferenceData, partner]);
559
+
560
+ const handleTrainingComplete = useCallback(() => {
561
+ console.log('🎉 Training completed successfully');
540
562
 
541
- // Simulate training progress over 10 seconds
542
- let progress = 0;
543
- const interval = setInterval(() => {
544
- progress += 0.1; // 10% every second for 10 seconds total
545
- setTraining({
546
- progress,
547
- eta: `${Math.round((1 - progress) * 10)} seconds remaining`,
548
- });
549
- if (progress >= 1) {
550
- clearInterval(interval);
551
-
552
- // Prepare completion data
553
- const completionData = {
554
- pin: userPin,
555
- connections,
556
- platformToggles,
557
- selectedTier,
558
- tierData: requestData?.[selectedTier],
559
- sessionSaved: true,
560
- // Add inference data if auto mode is enabled
561
- ...(auto && inferenceData && { inferenceData }),
562
- // Add partner info for special partners
563
- ...(partner && { partner: partner === 'couplebible' ? 'CoupleBible' : partner }),
564
- };
565
-
566
- console.log('Completion data prepared:', completionData);
567
-
568
- // Close overlay and call the original onComplete callback
569
- onComplete('https://api2.onairos.uk', 'dummy-token', completionData);
570
- }
571
- }, 1000); // Update every 1 second
572
- }, [connections, onComplete, selectedTier, requestData, platformToggles, username, AppName, auto, inferenceData, partner]);
563
+ // Prepare completion data
564
+ const completionData = {
565
+ pin,
566
+ connections,
567
+ platformToggles,
568
+ selectedTier,
569
+ tierData: requestData?.[selectedTier],
570
+ sessionSaved: true,
571
+ // Add inference data if auto mode is enabled
572
+ ...(auto && inferenceData && { inferenceData }),
573
+ // Add partner info for special partners
574
+ ...(partner && { partner: partner === 'couplebible' ? 'CoupleBible' : partner }),
575
+ };
576
+
577
+ console.log('Completion data prepared:', completionData);
578
+
579
+ // Close the modal first
580
+ handleClose();
581
+
582
+ // Then call the completion callback
583
+ setTimeout(() => {
584
+ onComplete('https://api2.onairos.uk', 'dummy-token', completionData);
585
+ }, 100);
586
+ }, [pin, connections, platformToggles, selectedTier, requestData, auto, inferenceData, partner, handleClose, onComplete]);
587
+
588
+ const handleDataRequestAccept = useCallback(() => {
589
+ console.log('Data request accepted for existing user');
590
+ setShowDataRequestModal(false);
591
+
592
+ // Complete onboarding for existing user
593
+ onComplete('https://api2.onairos.uk', 'existing-session-token', {
594
+ existingAccount: true,
595
+ email: email.trim(),
596
+ dataRequestAccepted: true,
597
+ requestData,
598
+ });
599
+ }, [email, onComplete, requestData]);
600
+
601
+ const handleDataRequestDecline = useCallback(() => {
602
+ console.log('Data request declined');
603
+ setShowDataRequestModal(false);
604
+ handleClose();
605
+ }, [handleClose]);
573
606
 
574
607
  const canProceedToPin = useCallback(() => {
575
608
  // Check if at least one platform is toggled on
@@ -665,6 +698,81 @@ export const UniversalOnboarding: React.FC<UniversalOnboardingProps> = ({
665
698
  </View>
666
699
  )}
667
700
 
701
+ {step === 'verify' && (
702
+ <View style={styles.emailInputContainer}>
703
+ <View style={styles.emailHeader}>
704
+ <View style={styles.onairosIcon}>
705
+ <Image
706
+ source={require('../assets/images/onairos_logo.png')}
707
+ style={styles.onairosLogo}
708
+ resizeMode="contain"
709
+ />
710
+ </View>
711
+ <Text style={styles.emailTitle}>Enter Verification Code</Text>
712
+ <Text style={styles.emailSubtitle}>
713
+ We've sent a 6-digit code to {email}
714
+ </Text>
715
+ <Text style={styles.developmentNote}>
716
+ 🔍 Development Mode: Any 6-digit code will work
717
+ </Text>
718
+ </View>
719
+
720
+ <View style={styles.emailInputSection}>
721
+ <View style={styles.codeInputContainer}>
722
+ {[0, 1, 2, 3, 4, 5].map((index) => (
723
+ <TextInput
724
+ key={index}
725
+ style={[
726
+ styles.codeDigit,
727
+ verificationCode.length === index && styles.codeDigitActive
728
+ ]}
729
+ value={verificationCode[index] || ''}
730
+ onChangeText={(text) => {
731
+ if (text.length <= 1 && /^\d*$/.test(text)) {
732
+ const newCode = verificationCode.split('');
733
+ newCode[index] = text;
734
+ const updatedCode = newCode.join('').slice(0, 6);
735
+ setVerificationCode(updatedCode);
736
+
737
+ // Auto-focus next input
738
+ if (text && index < 5) {
739
+ // Focus next input (would need refs for actual implementation)
740
+ }
741
+ }
742
+ }}
743
+ keyboardType="number-pad"
744
+ maxLength={1}
745
+ textAlign="center"
746
+ autoFocus={index === 0}
747
+ />
748
+ ))}
749
+ </View>
750
+
751
+ <TouchableOpacity
752
+ style={[
753
+ styles.emailSubmitButton,
754
+ (verificationCode.length !== 6 || isVerifyingCode) && styles.emailSubmitButtonDisabled
755
+ ]}
756
+ onPress={handleVerificationSubmit}
757
+ disabled={verificationCode.length !== 6 || isVerifyingCode}
758
+ >
759
+ {isVerifyingCode ? (
760
+ <ActivityIndicator size="small" color="#fff" />
761
+ ) : (
762
+ <Text style={styles.emailSubmitButtonText}>Verify</Text>
763
+ )}
764
+ </TouchableOpacity>
765
+
766
+ <TouchableOpacity
767
+ style={styles.backButton}
768
+ onPress={() => setStep('email')}
769
+ >
770
+ <Text style={styles.backButtonText}>← Back to email</Text>
771
+ </TouchableOpacity>
772
+ </View>
773
+ </View>
774
+ )}
775
+
668
776
  {step === 'connect' && (
669
777
  <>
670
778
  {/* Header with app icon and arrow to Onairos icon */}
@@ -812,15 +920,7 @@ export const UniversalOnboarding: React.FC<UniversalOnboardingProps> = ({
812
920
  progress={training.progress}
813
921
  eta={training.eta}
814
922
  onCancel={handleClose}
815
- onComplete={() => {
816
- onComplete('https://api2.onairos.uk', 'dummy-token', {
817
- pin,
818
- connections,
819
- platformToggles,
820
- selectedTier,
821
- tierData: requestData?.[selectedTier],
822
- });
823
- }}
923
+ onComplete={handleTrainingComplete}
824
924
  modelKey="onairosTrainingModel"
825
925
  username={username}
826
926
  />
@@ -839,22 +939,38 @@ export const UniversalOnboarding: React.FC<UniversalOnboardingProps> = ({
839
939
  />
840
940
  )}
841
941
 
842
- {/* Email Verification Modal */}
843
- {showEmailVerification && (
844
- <EmailVerificationModal
845
- visible={showEmailVerification}
846
- email={email.trim()}
847
- onClose={() => setShowEmailVerification(false)}
848
- onVerificationComplete={handleEmailVerificationComplete}
849
- onVerificationFailed={handleEmailVerificationFailed}
850
- />
851
- )}
942
+
852
943
 
853
944
  </SafeAreaView>
854
945
  </Animated.View>
855
946
  </TouchableWithoutFeedback>
856
947
  </View>
857
948
  </TouchableWithoutFeedback>
949
+
950
+ {/* Data Request Modal for existing users */}
951
+ {showDataRequestModal && requestData && (
952
+ <DataRequestModal
953
+ visible={showDataRequestModal}
954
+ onClose={handleDataRequestDecline}
955
+ onAccept={handleDataRequestAccept}
956
+ requestData={{
957
+ // Convert DataTier format to expected format
958
+ Small: {
959
+ description: requestData.Small?.descriptions || 'Basic data access',
960
+ type: requestData.Small?.type || 'basic'
961
+ },
962
+ Medium: {
963
+ description: requestData.Medium?.descriptions || 'Standard data access',
964
+ type: requestData.Medium?.type || 'standard'
965
+ },
966
+ Large: {
967
+ description: requestData.Large?.descriptions || 'Full data access',
968
+ type: requestData.Large?.type || 'full'
969
+ }
970
+ }}
971
+ AppName={AppName}
972
+ />
973
+ )}
858
974
  </Modal>
859
975
  );
860
976
  };
@@ -1146,4 +1262,42 @@ const styles = StyleSheet.create({
1146
1262
  fontSize: 16,
1147
1263
  fontWeight: '600',
1148
1264
  },
1265
+ // Verification code styles
1266
+ developmentNote: {
1267
+ fontSize: 14,
1268
+ color: '#FF9800',
1269
+ textAlign: 'center',
1270
+ marginTop: 8,
1271
+ backgroundColor: '#FFF3E0',
1272
+ padding: 8,
1273
+ borderRadius: 4,
1274
+ },
1275
+ codeInputContainer: {
1276
+ flexDirection: 'row',
1277
+ justifyContent: 'space-between',
1278
+ marginBottom: 24,
1279
+ paddingHorizontal: 20,
1280
+ },
1281
+ codeDigit: {
1282
+ width: 45,
1283
+ height: 55,
1284
+ borderWidth: 2,
1285
+ borderColor: '#ddd',
1286
+ borderRadius: 8,
1287
+ fontSize: 24,
1288
+ fontWeight: '600',
1289
+ color: '#000',
1290
+ backgroundColor: '#fff',
1291
+ },
1292
+ codeDigitActive: {
1293
+ borderColor: '#4CAF50',
1294
+ },
1295
+ backButton: {
1296
+ paddingVertical: 12,
1297
+ alignItems: 'center',
1298
+ },
1299
+ backButtonText: {
1300
+ color: '#666',
1301
+ fontSize: 16,
1302
+ },
1149
1303
  });
@@ -220,23 +220,51 @@ export const initiateNativeAuth = async (platform: string, username?: string): P
220
220
  throw new Error('Google Sign-In SDK not installed. Please install @react-native-google-signin/google-signin');
221
221
  }
222
222
 
223
- // Configure Google Sign-In
224
- await GoogleSignin.configure({
225
- webClientId: '1030678346906-lovkuds2ouqmoc8eu5qpo98spa6edv4o.apps.googleusercontent.com', // Replace with your web client ID
226
- iosClientId: '1030678346906-lovkuds2ouqmoc8eu5qpo98spa6edv4o.apps.googleusercontent.com', // Replace with your iOS client ID
227
- scopes: ['https://www.googleapis.com/auth/youtube.readonly'],
228
- offlineAccess: true, // CRITICAL: This ensures we get refresh tokens
229
- hostedDomain: '',
230
- forceCodeForRefreshToken: true, // CRITICAL: Force refresh token on first sign-in
231
- accountName: '', // Clear to avoid conflicts
232
- });
223
+ // Configure Google Sign-In with better error handling
224
+ try {
225
+ await GoogleSignin.configure({
226
+ webClientId: '1030678346906-lovkuds2ouqmoc8eu5qpo98spa6edv4o.apps.googleusercontent.com',
227
+ iosClientId: '1030678346906-lovkuds2ouqmoc8eu5qpo98spa6edv4o.apps.googleusercontent.com',
228
+ scopes: ['https://www.googleapis.com/auth/youtube.readonly'],
229
+ offlineAccess: true,
230
+ hostedDomain: '',
231
+ forceCodeForRefreshToken: true,
232
+ accountName: '',
233
+ });
234
+ console.log('✅ Google Sign-In configured successfully');
235
+ } catch (configError) {
236
+ console.error('❌ Google Sign-In configuration failed:', configError);
237
+ throw new Error(`Google Sign-In configuration failed: ${configError.message || configError}`);
238
+ }
233
239
 
234
- // Check if Google Play Services are available
235
- await GoogleSignin.hasPlayServices();
240
+ // Check if Google Play Services are available (Android only)
241
+ try {
242
+ await GoogleSignin.hasPlayServices({ showPlayServicesUpdateDialog: true });
243
+ console.log('✅ Google Play Services available');
244
+ } catch (playServicesError) {
245
+ console.error('❌ Google Play Services error:', playServicesError);
246
+ throw new Error(`Google Play Services not available: ${playServicesError.message || playServicesError}`);
247
+ }
236
248
 
237
- // Sign in with Google
238
- const userInfo = await GoogleSignin.signIn();
239
- console.log('✅ Google Sign-In successful:', userInfo.user?.email);
249
+ // Sign in with Google
250
+ let userInfo;
251
+ try {
252
+ userInfo = await GoogleSignin.signIn();
253
+ console.log('✅ Google Sign-In successful:', userInfo.user?.email);
254
+ } catch (signInError) {
255
+ console.error('❌ Google Sign-In failed:', signInError);
256
+
257
+ // Handle specific error codes
258
+ if (signInError.code === statusCodes.SIGN_IN_CANCELLED) {
259
+ throw new Error('Google Sign-In was cancelled by user');
260
+ } else if (signInError.code === statusCodes.IN_PROGRESS) {
261
+ throw new Error('Google Sign-In already in progress');
262
+ } else if (signInError.code === statusCodes.PLAY_SERVICES_NOT_AVAILABLE) {
263
+ throw new Error('Google Play Services not available or outdated');
264
+ } else {
265
+ throw new Error(`Google Sign-In failed: ${signInError.message || signInError}`);
266
+ }
267
+ }
240
268
 
241
269
  // Get access token for API calls
242
270
  const tokens = await GoogleSignin.getTokens();