@oxyhq/services 5.15.8 → 5.16.0

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 (148) hide show
  1. package/lib/commonjs/core/OxyServices.js +0 -1
  2. package/lib/commonjs/core/OxyServices.js.map +1 -1
  3. package/lib/commonjs/core/mixins/OxyServices.auth.js +3 -6
  4. package/lib/commonjs/core/mixins/OxyServices.auth.js.map +1 -1
  5. package/lib/commonjs/core/mixins/OxyServices.devices.js +1 -1
  6. package/lib/commonjs/core/mixins/OxyServices.devices.js.map +1 -1
  7. package/lib/commonjs/core/mixins/index.js +11 -12
  8. package/lib/commonjs/core/mixins/index.js.map +1 -1
  9. package/lib/commonjs/crypto/signatureService.js +3 -2
  10. package/lib/commonjs/crypto/signatureService.js.map +1 -1
  11. package/lib/commonjs/i18n/locales/ar-SA.json +1 -9
  12. package/lib/commonjs/i18n/locales/ca-ES.json +1 -9
  13. package/lib/commonjs/i18n/locales/de-DE.json +1 -9
  14. package/lib/commonjs/i18n/locales/en-US.json +3 -21
  15. package/lib/commonjs/i18n/locales/es-ES.json +3 -21
  16. package/lib/commonjs/i18n/locales/fr-FR.json +1 -9
  17. package/lib/commonjs/i18n/locales/it-IT.json +1 -9
  18. package/lib/commonjs/i18n/locales/ja-JP.json +1 -9
  19. package/lib/commonjs/i18n/locales/ko-KR.json +1 -9
  20. package/lib/commonjs/i18n/locales/pt-PT.json +1 -9
  21. package/lib/commonjs/i18n/locales/zh-CN.json +1 -9
  22. package/lib/commonjs/ui/context/OxyContext.js +24 -4
  23. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  24. package/lib/commonjs/ui/context/hooks/useAuthOperations.js +217 -100
  25. package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
  26. package/lib/commonjs/ui/screens/AccountSettingsScreen.js +2 -319
  27. package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
  28. package/lib/commonjs/ui/screens/OxyAuthScreen.js +178 -77
  29. package/lib/commonjs/ui/screens/OxyAuthScreen.js.map +1 -1
  30. package/lib/commonjs/ui/screens/PrivacySettingsScreen.js +0 -1
  31. package/lib/commonjs/ui/screens/PrivacySettingsScreen.js.map +1 -1
  32. package/lib/commonjs/ui/screens/SessionManagementScreen.js +43 -29
  33. package/lib/commonjs/ui/screens/SessionManagementScreen.js.map +1 -1
  34. package/lib/commonjs/ui/stores/authStore.js +14 -1
  35. package/lib/commonjs/ui/stores/authStore.js.map +1 -1
  36. package/lib/module/core/OxyServices.js +0 -1
  37. package/lib/module/core/OxyServices.js.map +1 -1
  38. package/lib/module/core/mixins/OxyServices.auth.js +3 -6
  39. package/lib/module/core/mixins/OxyServices.auth.js.map +1 -1
  40. package/lib/module/core/mixins/OxyServices.devices.js +1 -1
  41. package/lib/module/core/mixins/OxyServices.devices.js.map +1 -1
  42. package/lib/module/core/mixins/index.js +1 -2
  43. package/lib/module/core/mixins/index.js.map +1 -1
  44. package/lib/module/crypto/signatureService.js +3 -2
  45. package/lib/module/crypto/signatureService.js.map +1 -1
  46. package/lib/module/i18n/locales/ar-SA.json +1 -9
  47. package/lib/module/i18n/locales/ca-ES.json +1 -9
  48. package/lib/module/i18n/locales/de-DE.json +1 -9
  49. package/lib/module/i18n/locales/en-US.json +3 -21
  50. package/lib/module/i18n/locales/es-ES.json +3 -21
  51. package/lib/module/i18n/locales/fr-FR.json +1 -9
  52. package/lib/module/i18n/locales/it-IT.json +1 -9
  53. package/lib/module/i18n/locales/ja-JP.json +1 -9
  54. package/lib/module/i18n/locales/ko-KR.json +1 -9
  55. package/lib/module/i18n/locales/pt-PT.json +1 -9
  56. package/lib/module/i18n/locales/zh-CN.json +1 -9
  57. package/lib/module/ui/context/OxyContext.js +24 -4
  58. package/lib/module/ui/context/OxyContext.js.map +1 -1
  59. package/lib/module/ui/context/hooks/useAuthOperations.js +217 -100
  60. package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
  61. package/lib/module/ui/screens/AccountSettingsScreen.js +2 -319
  62. package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
  63. package/lib/module/ui/screens/OxyAuthScreen.js +179 -78
  64. package/lib/module/ui/screens/OxyAuthScreen.js.map +1 -1
  65. package/lib/module/ui/screens/PrivacySettingsScreen.js +0 -1
  66. package/lib/module/ui/screens/PrivacySettingsScreen.js.map +1 -1
  67. package/lib/module/ui/screens/SessionManagementScreen.js +44 -29
  68. package/lib/module/ui/screens/SessionManagementScreen.js.map +1 -1
  69. package/lib/module/ui/stores/authStore.js +14 -1
  70. package/lib/module/ui/stores/authStore.js.map +1 -1
  71. package/lib/typescript/core/OxyServices.d.ts +0 -1
  72. package/lib/typescript/core/OxyServices.d.ts.map +1 -1
  73. package/lib/typescript/core/mixins/OxyServices.auth.d.ts +3 -4
  74. package/lib/typescript/core/mixins/OxyServices.auth.d.ts.map +1 -1
  75. package/lib/typescript/core/mixins/OxyServices.devices.d.ts +1 -4
  76. package/lib/typescript/core/mixins/OxyServices.devices.d.ts.map +1 -1
  77. package/lib/typescript/core/mixins/index.d.ts +1 -64
  78. package/lib/typescript/core/mixins/index.d.ts.map +1 -1
  79. package/lib/typescript/crypto/signatureService.d.ts +2 -1
  80. package/lib/typescript/crypto/signatureService.d.ts.map +1 -1
  81. package/lib/typescript/models/interfaces.d.ts +1 -1
  82. package/lib/typescript/models/interfaces.d.ts.map +1 -1
  83. package/lib/typescript/types/bip39.d.ts +1 -0
  84. package/lib/typescript/types/buffer.d.ts +1 -0
  85. package/lib/typescript/types/color.d.ts +1 -0
  86. package/lib/typescript/types/elliptic.d.ts +1 -0
  87. package/lib/typescript/types/expo-crypto.d.ts +1 -0
  88. package/lib/typescript/types/expo-secure-store.d.ts +1 -0
  89. package/lib/typescript/ui/context/OxyContext.d.ts +11 -3
  90. package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
  91. package/lib/typescript/ui/context/hooks/useAuthOperations.d.ts +13 -5
  92. package/lib/typescript/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  93. package/lib/typescript/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
  94. package/lib/typescript/ui/screens/OxyAuthScreen.d.ts +1 -0
  95. package/lib/typescript/ui/screens/OxyAuthScreen.d.ts.map +1 -1
  96. package/lib/typescript/ui/screens/PrivacySettingsScreen.d.ts.map +1 -1
  97. package/lib/typescript/ui/screens/SessionManagementScreen.d.ts.map +1 -1
  98. package/lib/typescript/ui/stores/authStore.d.ts +4 -0
  99. package/lib/typescript/ui/stores/authStore.d.ts.map +1 -1
  100. package/package.json +6 -5
  101. package/src/core/OxyServices.ts +0 -1
  102. package/src/core/mixins/OxyServices.auth.ts +3 -8
  103. package/src/core/mixins/OxyServices.devices.ts +1 -4
  104. package/src/core/mixins/index.ts +2 -5
  105. package/src/crypto/index.ts +1 -0
  106. package/src/crypto/keyManager.ts +1 -0
  107. package/src/crypto/polyfill.ts +1 -0
  108. package/src/crypto/recoveryPhrase.ts +1 -0
  109. package/src/crypto/signatureService.ts +4 -5
  110. package/src/i18n/locales/ar-SA.json +1 -9
  111. package/src/i18n/locales/ca-ES.json +1 -9
  112. package/src/i18n/locales/de-DE.json +1 -9
  113. package/src/i18n/locales/en-US.json +3 -21
  114. package/src/i18n/locales/es-ES.json +3 -21
  115. package/src/i18n/locales/fr-FR.json +1 -9
  116. package/src/i18n/locales/it-IT.json +1 -9
  117. package/src/i18n/locales/ja-JP.json +1 -9
  118. package/src/i18n/locales/ko-KR.json +1 -9
  119. package/src/i18n/locales/pt-PT.json +1 -9
  120. package/src/i18n/locales/zh-CN.json +1 -9
  121. package/src/models/interfaces.ts +1 -1
  122. package/src/types/bip39.d.ts +1 -0
  123. package/src/types/buffer.d.ts +1 -0
  124. package/src/types/color.d.ts +1 -0
  125. package/src/types/elliptic.d.ts +1 -0
  126. package/src/types/expo-crypto.d.ts +1 -0
  127. package/src/types/expo-secure-store.d.ts +1 -0
  128. package/src/ui/context/OxyContext.tsx +35 -3
  129. package/src/ui/context/hooks/useAuthOperations.ts +212 -98
  130. package/src/ui/screens/AccountSettingsScreen.tsx +1 -201
  131. package/src/ui/screens/OxyAuthScreen.tsx +193 -69
  132. package/src/ui/screens/PrivacySettingsScreen.tsx +0 -2
  133. package/src/ui/screens/SessionManagementScreen.tsx +43 -26
  134. package/src/ui/stores/authStore.ts +31 -2
  135. package/lib/commonjs/core/mixins/OxyServices.totp.js +0 -53
  136. package/lib/commonjs/core/mixins/OxyServices.totp.js.map +0 -1
  137. package/lib/commonjs/ui/components/profile/TwoFactorSetupModal.js +0 -467
  138. package/lib/commonjs/ui/components/profile/TwoFactorSetupModal.js.map +0 -1
  139. package/lib/module/core/mixins/OxyServices.totp.js +0 -49
  140. package/lib/module/core/mixins/OxyServices.totp.js.map +0 -1
  141. package/lib/module/ui/components/profile/TwoFactorSetupModal.js +0 -460
  142. package/lib/module/ui/components/profile/TwoFactorSetupModal.js.map +0 -1
  143. package/lib/typescript/core/mixins/OxyServices.totp.d.ts +0 -66
  144. package/lib/typescript/core/mixins/OxyServices.totp.d.ts.map +0 -1
  145. package/lib/typescript/ui/components/profile/TwoFactorSetupModal.d.ts +0 -11
  146. package/lib/typescript/ui/components/profile/TwoFactorSetupModal.d.ts.map +0 -1
  147. package/src/core/mixins/OxyServices.totp.ts +0 -36
  148. package/src/ui/components/profile/TwoFactorSetupModal.tsx +0 -442
@@ -36,10 +36,8 @@ import { EditEmailModal } from '../components/profile/EditEmailModal';
36
36
  import { EditBioModal } from '../components/profile/EditBioModal';
37
37
  import { EditLocationModal } from '../components/profile/EditLocationModal';
38
38
  import { EditLinksModal } from '../components/profile/EditLinksModal';
39
- import { TwoFactorSetupModal } from '../components/profile/TwoFactorSetupModal';
40
39
  import { getDisplayName } from '../utils/user-utils';
41
40
  import { TTLCache, registerCacheForCleanup } from '../../utils/cache';
42
- import QRCode from 'react-native-qrcode-svg';
43
41
  import { useOxy } from '../context/OxyContext';
44
42
  import {
45
43
  SCREEN_PADDING_HORIZONTAL,
@@ -102,13 +100,6 @@ const AccountSettingsScreen: React.FC<BaseScreenProps & { initialField?: string;
102
100
  const [quickActionsSectionY, setQuickActionsSectionY] = useState<number | null>(null);
103
101
  const [securitySectionY, setSecuritySectionY] = useState<number | null>(null);
104
102
 
105
- // Two-Factor (TOTP) state
106
- const [totpSetupUrl, setTotpSetupUrl] = useState<string | null>(null);
107
- const [totpCode, setTotpCode] = useState('');
108
- const [isTotpBusy, setIsTotpBusy] = useState(false);
109
- const [showRecoveryModal, setShowRecoveryModal] = useState(false);
110
- const [generatedBackupCodes, setGeneratedBackupCodes] = useState<string[] | null>(null);
111
- const [generatedRecoveryKey, setGeneratedRecoveryKey] = useState<string | null>(null);
112
103
 
113
104
  // Animation refs
114
105
  const saveButtonScale = useRef(new Animated.Value(1)).current;
@@ -130,7 +121,6 @@ const AccountSettingsScreen: React.FC<BaseScreenProps & { initialField?: string;
130
121
  const [showEditBioModal, setShowEditBioModal] = useState(false);
131
122
  const [showEditLocationModal, setShowEditLocationModal] = useState(false);
132
123
  const [showEditLinksModal, setShowEditLinksModal] = useState(false);
133
- const [showTwoFactorModal, setShowTwoFactorModal] = useState(false);
134
124
 
135
125
  // Location and links state (for display only - modals handle editing)
136
126
  const [locations, setLocations] = useState<Array<{
@@ -342,9 +332,6 @@ const AccountSettingsScreen: React.FC<BaseScreenProps & { initialField?: string;
342
332
  case 'links':
343
333
  setTempLinksWithMetadata([...linksMetadata]);
344
334
  break;
345
- case 'twoFactor':
346
- // No temp state needed for twoFactor
347
- break;
348
335
  }
349
336
  }, [displayName, lastName, username, email, bio, locations, linksMetadata]);
350
337
 
@@ -423,8 +410,6 @@ const AccountSettingsScreen: React.FC<BaseScreenProps & { initialField?: string;
423
410
  return bio;
424
411
  case 'location':
425
412
  case 'links':
426
- case 'twoFactor':
427
- return '';
428
413
  default:
429
414
  return '';
430
415
  }
@@ -529,7 +514,6 @@ const AccountSettingsScreen: React.FC<BaseScreenProps & { initialField?: string;
529
514
  const handleOpenBioModal = useCallback(() => setShowEditBioModal(true), []);
530
515
  const handleOpenLocationModal = useCallback(() => setShowEditLocationModal(true), []);
531
516
  const handleOpenLinksModal = useCallback(() => setShowEditLinksModal(true), []);
532
- const handleOpenTwoFactorModal = useCallback(() => setShowTwoFactorModal(true), []);
533
517
 
534
518
  // Handler to refresh data after modal saves
535
519
  // Note: Access user directly from store when invoked to get latest value,
@@ -611,9 +595,6 @@ const AccountSettingsScreen: React.FC<BaseScreenProps & { initialField?: string;
611
595
  case 'links':
612
596
  setShowEditLinksModal(true);
613
597
  break;
614
- case 'twoFactor':
615
- setShowTwoFactorModal(true);
616
- break;
617
598
  }
618
599
  }, 300);
619
600
  }
@@ -760,135 +741,9 @@ const AccountSettingsScreen: React.FC<BaseScreenProps & { initialField?: string;
760
741
  // Memoize display name for avatar
761
742
  const displayNameForAvatar = useMemo(() => getDisplayName(user), [user]);
762
743
 
763
- // Legacy renderEditingField function (still used for twoFactor and fallback)
744
+ // Legacy renderEditingField function (fallback)
764
745
  const renderEditingField = (type: string | null) => {
765
746
  if (!type) return null;
766
-
767
- if (type === 'twoFactor') {
768
- const enabled = !!user?.privacySettings?.twoFactorEnabled;
769
- return (
770
- <View style={[styles.editingFieldContainer, { backgroundColor: colors.background }]}>
771
- <View style={styles.editingFieldContent}>
772
- <View style={styles.newValueSection}>
773
- <View style={styles.editingFieldHeader}>
774
- <Text style={[styles.editingFieldLabel, { color: colors.text }]}>Two‑Factor Authentication (TOTP)</Text>
775
- </View>
776
-
777
- {!enabled ? (
778
- <>
779
- <Text style={styles.editingFieldDescription}>
780
- Protect your account with a 6‑digit code from an authenticator app. Scan the QR code then enter the code to enable.
781
- </Text>
782
- {!totpSetupUrl ? (
783
- <TouchableOpacity
784
- style={[styles.primaryButton, { backgroundColor: colors.iconSecurity }]}
785
- disabled={isTotpBusy}
786
- onPress={async () => {
787
- if (!activeSessionId) { toast.error(t('editProfile.toasts.noActiveSession') || 'No active session'); return; }
788
- setIsTotpBusy(true);
789
- try {
790
- const { otpauthUrl } = await oxyServices.startTotpEnrollment(activeSessionId);
791
- setTotpSetupUrl(otpauthUrl);
792
- } catch (e: any) {
793
- toast.error(e?.message || (t('editProfile.toasts.totpStartFailed') || 'Failed to start TOTP enrollment'));
794
- } finally {
795
- setIsTotpBusy(false);
796
- }
797
- }}
798
- >
799
- <Ionicons name="shield-checkmark" size={18} color="#fff" />
800
- <Text style={styles.primaryButtonText}>Generate QR Code</Text>
801
- </TouchableOpacity>
802
- ) : (
803
- <View style={{ alignItems: 'center', gap: 16 }}>
804
- <View style={{ padding: 16, backgroundColor: '#fff', borderRadius: 16 }}>
805
- <QRCode value={totpSetupUrl} size={180} />
806
- </View>
807
- <View>
808
- <Text style={styles.editingFieldLabel}>Enter 6‑digit code</Text>
809
- <TextInput
810
- style={styles.editingFieldInput}
811
- keyboardType="number-pad"
812
- placeholder="123456"
813
- value={totpCode}
814
- onChangeText={setTotpCode}
815
- maxLength={6}
816
- />
817
- </View>
818
- <TouchableOpacity
819
- style={[styles.primaryButton, { backgroundColor: colors.iconSecurity }]}
820
- disabled={isTotpBusy || totpCode.length !== 6}
821
- onPress={async () => {
822
- if (!activeSessionId) { toast.error(t('editProfile.toasts.noActiveSession') || 'No active session'); return; }
823
- setIsTotpBusy(true);
824
- try {
825
- const result = await oxyServices.verifyTotpEnrollment(activeSessionId, totpCode);
826
- await updateUser({ privacySettings: { twoFactorEnabled: true } }, oxyServices);
827
- if (result?.backupCodes || result?.recoveryKey) {
828
- setGeneratedBackupCodes(result.backupCodes || null);
829
- setGeneratedRecoveryKey(result.recoveryKey || null);
830
- setShowRecoveryModal(true);
831
- } else {
832
- toast.success(t('editProfile.toasts.twoFactorEnabled') || 'Two‑Factor Authentication enabled');
833
- setEditingField(null);
834
- }
835
- } catch (e: any) {
836
- toast.error(e?.message || (t('editProfile.toasts.invalidCode') || 'Invalid code'));
837
- } finally {
838
- setIsTotpBusy(false);
839
- }
840
- }}
841
- >
842
- <Ionicons name="checkmark-circle" size={18} color="#fff" />
843
- <Text style={styles.primaryButtonText}>Verify & Enable</Text>
844
- </TouchableOpacity>
845
- </View>
846
- )}
847
- </>
848
- ) : (
849
- <>
850
- <Text style={styles.editingFieldDescription}>
851
- Two‑Factor Authentication is currently enabled. To disable, enter a code from your authenticator app.
852
- </Text>
853
- <View>
854
- <Text style={styles.editingFieldLabel}>Enter 6‑digit code</Text>
855
- <TextInput
856
- style={styles.editingFieldInput}
857
- keyboardType="number-pad"
858
- placeholder="123456"
859
- value={totpCode}
860
- onChangeText={setTotpCode}
861
- maxLength={6}
862
- />
863
- </View>
864
- <TouchableOpacity
865
- style={[styles.primaryButton, { backgroundColor: colors.sidebarIconSharing }]}
866
- disabled={isTotpBusy || totpCode.length !== 6}
867
- onPress={async () => {
868
- if (!activeSessionId) { toast.error(t('editProfile.toasts.noActiveSession') || 'No active session'); return; }
869
- setIsTotpBusy(true);
870
- try {
871
- await oxyServices.disableTotp(activeSessionId, totpCode);
872
- await updateUser({ privacySettings: { twoFactorEnabled: false } }, oxyServices);
873
- toast.success(t('editProfile.toasts.twoFactorDisabled') || 'Two‑Factor Authentication disabled');
874
- setEditingField(null);
875
- } catch (e: any) {
876
- toast.error(e?.message || t('editProfile.toasts.disableFailed') || 'Failed to disable');
877
- } finally {
878
- setIsTotpBusy(false);
879
- }
880
- }}
881
- >
882
- <Ionicons name="close-circle" size={18} color="#fff" />
883
- <Text style={styles.primaryButtonText}>Disable 2FA</Text>
884
- </TouchableOpacity>
885
- </>
886
- )}
887
- </View>
888
- </View>
889
- </View>
890
- );
891
- }
892
747
  if (type === 'displayName') {
893
748
  return (
894
749
  <View style={[styles.editingFieldContainer, { backgroundColor: colors.background }]}>
@@ -1368,41 +1223,6 @@ const AccountSettingsScreen: React.FC<BaseScreenProps & { initialField?: string;
1368
1223
  </Text>
1369
1224
  </View>
1370
1225
 
1371
- {showRecoveryModal && (
1372
- <View style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.6)', zIndex: 50, padding: 16, justifyContent: 'center' }}>
1373
- <View style={{ backgroundColor: '#fff', borderRadius: 20, padding: 20, maxHeight: '80%' }}>
1374
- <Text style={{ fontSize: 18, fontWeight: '700', marginBottom: 12 }}>Save These Codes Now</Text>
1375
- <Text style={{ fontSize: 14, color: '#444', marginBottom: 12 }}>
1376
- Backup codes and your Recovery Key are shown only once. Store them securely (paper or password manager).
1377
- </Text>
1378
- {generatedBackupCodes && generatedBackupCodes.length > 0 && (
1379
- <View style={{ marginBottom: 12 }}>
1380
- <Text style={{ fontSize: 16, fontWeight: '600', marginBottom: 8 }}>Backup Codes</Text>
1381
- <View style={{ backgroundColor: '#F8F9FA', borderRadius: 12, padding: 12 }}>
1382
- {generatedBackupCodes.map((c, idx) => (
1383
- <Text key={idx} style={{ fontFamily: Platform.OS === 'web' ? 'monospace' as any : 'monospace', fontSize: 14, marginBottom: 4 }}>{c}</Text>
1384
- ))}
1385
- </View>
1386
- </View>
1387
- )}
1388
- {generatedRecoveryKey && (
1389
- <View style={{ marginBottom: 12 }}>
1390
- <Text style={{ fontSize: 16, fontWeight: '600', marginBottom: 8 }}>Recovery Key</Text>
1391
- <View style={{ backgroundColor: '#F8F9FA', borderRadius: 12, padding: 12 }}>
1392
- <Text style={{ fontFamily: Platform.OS === 'web' ? 'monospace' as any : 'monospace', fontSize: 14 }}>{generatedRecoveryKey}</Text>
1393
- </View>
1394
- </View>
1395
- )}
1396
- <TouchableOpacity
1397
- style={[styles.primaryButton, { backgroundColor: colors.iconSecurity, alignSelf: 'flex-end', marginTop: 8 }]}
1398
- onPress={() => { setShowRecoveryModal(false); setEditingField(null); toast.success(t('editProfile.toasts.twoFactorEnabled') || 'Two‑Factor Authentication enabled'); }}
1399
- >
1400
- <Ionicons name="checkmark" size={18} color="#fff" />
1401
- <Text style={styles.primaryButtonText}>I saved them</Text>
1402
- </TouchableOpacity>
1403
- </View>
1404
- </View>
1405
- )}
1406
1226
  {/* Profile Picture Section */}
1407
1227
  <View
1408
1228
  ref={(ref) => {
@@ -1634,20 +1454,6 @@ const AccountSettingsScreen: React.FC<BaseScreenProps & { initialField?: string;
1634
1454
  {t('editProfile.sections.security') || 'SECURITY'}
1635
1455
  </Text>
1636
1456
  <View style={styles.groupedSectionWrapper}>
1637
- <GroupedSection
1638
- items={[
1639
- {
1640
- id: 'two-factor',
1641
- icon: 'shield-lock',
1642
- iconColor: colors.sidebarIconSecurity,
1643
- title: t('editProfile.items.twoFactor.title') || 'Two‑Factor Authentication',
1644
- subtitle: user?.privacySettings?.twoFactorEnabled
1645
- ? (t('editProfile.items.twoFactor.enabled') || 'Enabled')
1646
- : (t('editProfile.items.twoFactor.disabled') || 'Disabled (recommended)'),
1647
- onPress: handleOpenTwoFactorModal,
1648
- },
1649
- ]}
1650
- />
1651
1457
  </View>
1652
1458
  </View>
1653
1459
  </>
@@ -1698,12 +1504,6 @@ const AccountSettingsScreen: React.FC<BaseScreenProps & { initialField?: string;
1698
1504
  theme={normalizedTheme}
1699
1505
  onSave={handleModalSave}
1700
1506
  />
1701
- <TwoFactorSetupModal
1702
- visible={showTwoFactorModal}
1703
- onClose={() => setShowTwoFactorModal(false)}
1704
- isEnabled={!!user?.privacySettings?.twoFactorEnabled}
1705
- theme={normalizedTheme}
1706
- />
1707
1507
  </View>
1708
1508
  );
1709
1509
  };
@@ -6,6 +6,7 @@
6
6
  * 1. Scan QR code with Oxy Accounts app
7
7
  * 2. Open Oxy Accounts app directly (via deep link)
8
8
  *
9
+ * Uses WebSocket for real-time authorization updates (with polling fallback).
9
10
  * The Oxy Accounts app is where users manage their cryptographic identity.
10
11
  * This screen should NOT be used within the Accounts app itself.
11
12
  */
@@ -21,6 +22,7 @@ import {
21
22
  Platform,
22
23
  ActivityIndicator,
23
24
  } from 'react-native';
25
+ import io, { type Socket } from 'socket.io-client';
24
26
  import type { BaseScreenProps } from '../types/navigation';
25
27
  import { useThemeColors } from '../styles';
26
28
  import { useOxy } from '../context/OxyContext';
@@ -34,11 +36,22 @@ const OXY_ACCOUNTS_WEB_URL = 'https://accounts.oxy.so';
34
36
  // Auth session expiration (5 minutes)
35
37
  const AUTH_SESSION_EXPIRY_MS = 5 * 60 * 1000;
36
38
 
39
+ // Polling interval (fallback if socket fails)
40
+ const POLLING_INTERVAL_MS = 3000;
41
+
37
42
  interface AuthSession {
38
43
  sessionToken: string;
39
44
  expiresAt: number;
40
45
  }
41
46
 
47
+ interface AuthUpdatePayload {
48
+ status: 'authorized' | 'cancelled' | 'expired';
49
+ sessionId?: string;
50
+ publicKey?: string;
51
+ userId?: string;
52
+ username?: string;
53
+ }
54
+
42
55
  const OxyAuthScreen: React.FC<BaseScreenProps> = ({
43
56
  navigate,
44
57
  goBack,
@@ -47,18 +60,163 @@ const OxyAuthScreen: React.FC<BaseScreenProps> = ({
47
60
  }) => {
48
61
  const themeValue = (theme === 'light' || theme === 'dark') ? theme : 'light';
49
62
  const colors = useThemeColors(themeValue);
50
- const { oxyServices, signIn } = useOxy();
63
+ const { oxyServices, signIn, switchSession } = useOxy();
51
64
 
52
65
  const [authSession, setAuthSession] = useState<AuthSession | null>(null);
53
66
  const [isLoading, setIsLoading] = useState(true);
54
67
  const [error, setError] = useState<string | null>(null);
55
- const [isPolling, setIsPolling] = useState(false);
68
+ const [isWaiting, setIsWaiting] = useState(false);
69
+ const [connectionType, setConnectionType] = useState<'socket' | 'polling'>('socket');
70
+
71
+ const socketRef = useRef<Socket | null>(null);
56
72
  const pollingIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
73
+ const isProcessingRef = useRef(false);
74
+
75
+ // Handle successful authorization
76
+ const handleAuthSuccess = useCallback(async (sessionId: string) => {
77
+ if (isProcessingRef.current) return;
78
+ isProcessingRef.current = true;
79
+
80
+ try {
81
+ // Switch to the new session (this will get token, user data, and update state)
82
+ if (switchSession) {
83
+ const user = await switchSession(sessionId);
84
+ if (onAuthenticated) {
85
+ onAuthenticated(user);
86
+ }
87
+ } else {
88
+ // Fallback if switchSession not available (shouldn't happen, but for safety)
89
+ await oxyServices.getTokenBySession(sessionId);
90
+ const user = await oxyServices.getUserBySession(sessionId);
91
+ if (onAuthenticated) {
92
+ onAuthenticated(user);
93
+ }
94
+ }
95
+ } catch (err) {
96
+ if (__DEV__) {
97
+ console.error('Error completing auth:', err);
98
+ }
99
+ setError('Authorization successful but failed to complete sign in. Please try again.');
100
+ isProcessingRef.current = false;
101
+ }
102
+ }, [oxyServices, switchSession, onAuthenticated]);
103
+
104
+ // Connect to socket for real-time updates
105
+ const connectSocket = useCallback((sessionToken: string) => {
106
+ const baseURL = oxyServices.getBaseURL();
107
+
108
+ // Connect to the auth-session namespace (no authentication required)
109
+ const socket = io(`${baseURL}/auth-session`, {
110
+ transports: ['websocket', 'polling'],
111
+ reconnection: true,
112
+ reconnectionAttempts: 3,
113
+ reconnectionDelay: 1000,
114
+ });
115
+
116
+ socketRef.current = socket;
117
+
118
+ socket.on('connect', () => {
119
+ if (__DEV__) {
120
+ console.log('Auth socket connected');
121
+ }
122
+ // Join the room for this session token
123
+ socket.emit('join', sessionToken);
124
+ setConnectionType('socket');
125
+ });
126
+
127
+ socket.on('joined', () => {
128
+ if (__DEV__) {
129
+ console.log('Joined auth session room');
130
+ }
131
+ });
132
+
133
+ socket.on('auth_update', (payload: AuthUpdatePayload) => {
134
+ if (__DEV__) {
135
+ console.log('Auth update received:', payload);
136
+ }
137
+
138
+ if (payload.status === 'authorized' && payload.sessionId) {
139
+ cleanup();
140
+ handleAuthSuccess(payload.sessionId);
141
+ } else if (payload.status === 'cancelled') {
142
+ cleanup();
143
+ setError('Authorization was denied.');
144
+ } else if (payload.status === 'expired') {
145
+ cleanup();
146
+ setError('Session expired. Please try again.');
147
+ }
148
+ });
149
+
150
+ socket.on('connect_error', (err) => {
151
+ if (__DEV__) {
152
+ console.log('Socket connection error, falling back to polling:', err.message);
153
+ }
154
+ // Fall back to polling if socket fails
155
+ socket.disconnect();
156
+ startPolling(sessionToken);
157
+ });
158
+
159
+ socket.on('disconnect', () => {
160
+ if (__DEV__) {
161
+ console.log('Auth socket disconnected');
162
+ }
163
+ });
164
+ }, [oxyServices, handleAuthSuccess]);
165
+
166
+ // Start polling for authorization (fallback)
167
+ const startPolling = useCallback((sessionToken: string) => {
168
+ setConnectionType('polling');
169
+
170
+ pollingIntervalRef.current = setInterval(async () => {
171
+ if (isProcessingRef.current) return;
172
+
173
+ try {
174
+ const response = await oxyServices.makeRequest<{
175
+ authorized: boolean;
176
+ sessionId?: string;
177
+ publicKey?: string;
178
+ status?: string;
179
+ }>('GET', `/api/auth/session/status/${sessionToken}`, undefined, { cache: false });
180
+
181
+ if (response.authorized && response.sessionId) {
182
+ cleanup();
183
+ handleAuthSuccess(response.sessionId);
184
+ } else if (response.status === 'cancelled') {
185
+ cleanup();
186
+ setError('Authorization was denied.');
187
+ } else if (response.status === 'expired') {
188
+ cleanup();
189
+ setError('Session expired. Please try again.');
190
+ }
191
+ } catch (err) {
192
+ // Silent fail for polling - will retry
193
+ if (__DEV__) {
194
+ console.log('Auth polling error:', err);
195
+ }
196
+ }
197
+ }, POLLING_INTERVAL_MS);
198
+ }, [oxyServices, handleAuthSuccess]);
199
+
200
+ // Cleanup socket and polling
201
+ const cleanup = useCallback(() => {
202
+ setIsWaiting(false);
203
+
204
+ if (socketRef.current) {
205
+ socketRef.current.disconnect();
206
+ socketRef.current = null;
207
+ }
208
+
209
+ if (pollingIntervalRef.current) {
210
+ clearInterval(pollingIntervalRef.current);
211
+ pollingIntervalRef.current = null;
212
+ }
213
+ }, []);
57
214
 
58
215
  // Generate a new auth session
59
216
  const generateAuthSession = useCallback(async () => {
60
217
  setIsLoading(true);
61
218
  setError(null);
219
+ isProcessingRef.current = false;
62
220
 
63
221
  try {
64
222
  // Generate a unique session token for this auth request
@@ -66,7 +224,6 @@ const OxyAuthScreen: React.FC<BaseScreenProps> = ({
66
224
  const expiresAt = Date.now() + AUTH_SESSION_EXPIRY_MS;
67
225
 
68
226
  // Register the auth session with the server
69
- // The server will associate this token with the user when they authorize in Accounts
70
227
  await oxyServices.makeRequest('POST', '/api/auth/session/create', {
71
228
  sessionToken,
72
229
  expiresAt,
@@ -74,13 +231,16 @@ const OxyAuthScreen: React.FC<BaseScreenProps> = ({
74
231
  }, { cache: false });
75
232
 
76
233
  setAuthSession({ sessionToken, expiresAt });
77
- startPolling(sessionToken);
234
+ setIsWaiting(true);
235
+
236
+ // Try socket first, will fall back to polling if needed
237
+ connectSocket(sessionToken);
78
238
  } catch (err: any) {
79
239
  setError(err.message || 'Failed to create auth session');
80
240
  } finally {
81
241
  setIsLoading(false);
82
242
  }
83
- }, [oxyServices]);
243
+ }, [oxyServices, connectSocket]);
84
244
 
85
245
  // Generate a random session token
86
246
  const generateSessionToken = (): string => {
@@ -92,54 +252,12 @@ const OxyAuthScreen: React.FC<BaseScreenProps> = ({
92
252
  return result;
93
253
  };
94
254
 
95
- // Start polling for authorization
96
- const startPolling = useCallback((sessionToken: string) => {
97
- setIsPolling(true);
98
-
99
- pollingIntervalRef.current = setInterval(async () => {
100
- try {
101
- const response = await oxyServices.makeRequest<{
102
- authorized: boolean;
103
- sessionId?: string;
104
- publicKey?: string;
105
- }>('GET', `/api/auth/session/status/${sessionToken}`, undefined, { cache: false });
106
-
107
- if (response.authorized && response.sessionId) {
108
- // Authorization successful!
109
- stopPolling();
110
-
111
- // Get token and user data
112
- await oxyServices.getTokenBySession(response.sessionId);
113
- const user = await oxyServices.getUserBySession(response.sessionId);
114
-
115
- if (onAuthenticated) {
116
- onAuthenticated(user);
117
- }
118
- }
119
- } catch (err) {
120
- // Silent fail for polling - will retry
121
- if (__DEV__) {
122
- console.log('Auth polling error:', err);
123
- }
124
- }
125
- }, 2000); // Poll every 2 seconds
126
- }, [oxyServices, onAuthenticated]);
127
-
128
- // Stop polling
129
- const stopPolling = useCallback(() => {
130
- setIsPolling(false);
131
- if (pollingIntervalRef.current) {
132
- clearInterval(pollingIntervalRef.current);
133
- pollingIntervalRef.current = null;
134
- }
135
- }, []);
136
-
137
255
  // Clean up on unmount
138
256
  useEffect(() => {
139
257
  return () => {
140
- stopPolling();
258
+ cleanup();
141
259
  };
142
- }, [stopPolling]);
260
+ }, [cleanup]);
143
261
 
144
262
  // Initialize auth session
145
263
  useEffect(() => {
@@ -149,11 +267,11 @@ const OxyAuthScreen: React.FC<BaseScreenProps> = ({
149
267
  // Check if session expired
150
268
  useEffect(() => {
151
269
  if (authSession && Date.now() > authSession.expiresAt) {
152
- stopPolling();
270
+ cleanup();
153
271
  setAuthSession(null);
154
272
  setError('Session expired. Please try again.');
155
273
  }
156
- }, [authSession, stopPolling]);
274
+ }, [authSession, cleanup]);
157
275
 
158
276
  // Build the QR code data
159
277
  const getQRData = (): string => {
@@ -191,9 +309,9 @@ const OxyAuthScreen: React.FC<BaseScreenProps> = ({
191
309
 
192
310
  // Refresh session
193
311
  const handleRefresh = useCallback(() => {
194
- stopPolling();
312
+ cleanup();
195
313
  generateAuthSession();
196
- }, [generateAuthSession, stopPolling]);
314
+ }, [generateAuthSession, cleanup]);
197
315
 
198
316
  if (isLoading) {
199
317
  return (
@@ -246,24 +364,29 @@ const OxyAuthScreen: React.FC<BaseScreenProps> = ({
246
364
  </Text>
247
365
  </View>
248
366
 
249
- {/* Divider */}
250
- <View style={styles.dividerContainer}>
251
- <View style={[styles.divider, { backgroundColor: colors.border }]} />
252
- <Text style={[styles.dividerText, { color: colors.secondaryText }]}>or</Text>
253
- <View style={[styles.divider, { backgroundColor: colors.border }]} />
254
- </View>
255
-
256
- {/* Open Accounts Button */}
257
- <TouchableOpacity
258
- style={[styles.button, { backgroundColor: colors.primary }]}
259
- onPress={handleOpenAccounts}
260
- >
261
- <OxyLogo width={20} height={20} fillColor="white" style={styles.buttonIcon} />
262
- <Text style={styles.buttonText}>Open Oxy Accounts</Text>
263
- </TouchableOpacity>
367
+ {/* Divider and Open Accounts Button - Only show on native platforms */}
368
+ {Platform.OS !== 'web' && (
369
+ <>
370
+ {/* Divider */}
371
+ <View style={styles.dividerContainer}>
372
+ <View style={[styles.divider, { backgroundColor: colors.border }]} />
373
+ <Text style={[styles.dividerText, { color: colors.secondaryText }]}>or</Text>
374
+ <View style={[styles.divider, { backgroundColor: colors.border }]} />
375
+ </View>
376
+
377
+ {/* Open Accounts Button */}
378
+ <TouchableOpacity
379
+ style={[styles.button, { backgroundColor: colors.primary }]}
380
+ onPress={handleOpenAccounts}
381
+ >
382
+ <OxyLogo width={20} height={20} fillColor="white" style={styles.buttonIcon} />
383
+ <Text style={styles.buttonText}>Open Oxy Accounts</Text>
384
+ </TouchableOpacity>
385
+ </>
386
+ )}
264
387
 
265
388
  {/* Status */}
266
- {isPolling && (
389
+ {isWaiting && (
267
390
  <View style={styles.statusContainer}>
268
391
  <ActivityIndicator size="small" color={colors.primary} />
269
392
  <Text style={[styles.statusText, { color: colors.secondaryText }]}>
@@ -401,3 +524,4 @@ const styles = StyleSheet.create({
401
524
 
402
525
  export default OxyAuthScreen;
403
526
 
527
+
@@ -21,7 +21,6 @@ interface PrivacySettings {
21
21
  hideOnlineStatus: boolean;
22
22
  hideLastSeen: boolean;
23
23
  profileVisibility: boolean;
24
- twoFactorEnabled: boolean;
25
24
  loginAlerts: boolean;
26
25
  blockScreenshots: boolean;
27
26
  login: boolean;
@@ -52,7 +51,6 @@ const PrivacySettingsScreen: React.FC<BaseScreenProps> = ({
52
51
  hideOnlineStatus: false,
53
52
  hideLastSeen: false,
54
53
  profileVisibility: true,
55
- twoFactorEnabled: false,
56
54
  loginAlerts: true,
57
55
  blockScreenshots: false,
58
56
  login: true,