@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.
- package/lib/commonjs/core/OxyServices.js +0 -1
- package/lib/commonjs/core/OxyServices.js.map +1 -1
- package/lib/commonjs/core/mixins/OxyServices.auth.js +3 -6
- package/lib/commonjs/core/mixins/OxyServices.auth.js.map +1 -1
- package/lib/commonjs/core/mixins/OxyServices.devices.js +1 -1
- package/lib/commonjs/core/mixins/OxyServices.devices.js.map +1 -1
- package/lib/commonjs/core/mixins/index.js +11 -12
- package/lib/commonjs/core/mixins/index.js.map +1 -1
- package/lib/commonjs/crypto/signatureService.js +3 -2
- package/lib/commonjs/crypto/signatureService.js.map +1 -1
- package/lib/commonjs/i18n/locales/ar-SA.json +1 -9
- package/lib/commonjs/i18n/locales/ca-ES.json +1 -9
- package/lib/commonjs/i18n/locales/de-DE.json +1 -9
- package/lib/commonjs/i18n/locales/en-US.json +3 -21
- package/lib/commonjs/i18n/locales/es-ES.json +3 -21
- package/lib/commonjs/i18n/locales/fr-FR.json +1 -9
- package/lib/commonjs/i18n/locales/it-IT.json +1 -9
- package/lib/commonjs/i18n/locales/ja-JP.json +1 -9
- package/lib/commonjs/i18n/locales/ko-KR.json +1 -9
- package/lib/commonjs/i18n/locales/pt-PT.json +1 -9
- package/lib/commonjs/i18n/locales/zh-CN.json +1 -9
- package/lib/commonjs/ui/context/OxyContext.js +24 -4
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js +217 -100
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountSettingsScreen.js +2 -319
- package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/OxyAuthScreen.js +178 -77
- package/lib/commonjs/ui/screens/OxyAuthScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/PrivacySettingsScreen.js +0 -1
- package/lib/commonjs/ui/screens/PrivacySettingsScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/SessionManagementScreen.js +43 -29
- package/lib/commonjs/ui/screens/SessionManagementScreen.js.map +1 -1
- package/lib/commonjs/ui/stores/authStore.js +14 -1
- package/lib/commonjs/ui/stores/authStore.js.map +1 -1
- package/lib/module/core/OxyServices.js +0 -1
- package/lib/module/core/OxyServices.js.map +1 -1
- package/lib/module/core/mixins/OxyServices.auth.js +3 -6
- package/lib/module/core/mixins/OxyServices.auth.js.map +1 -1
- package/lib/module/core/mixins/OxyServices.devices.js +1 -1
- package/lib/module/core/mixins/OxyServices.devices.js.map +1 -1
- package/lib/module/core/mixins/index.js +1 -2
- package/lib/module/core/mixins/index.js.map +1 -1
- package/lib/module/crypto/signatureService.js +3 -2
- package/lib/module/crypto/signatureService.js.map +1 -1
- package/lib/module/i18n/locales/ar-SA.json +1 -9
- package/lib/module/i18n/locales/ca-ES.json +1 -9
- package/lib/module/i18n/locales/de-DE.json +1 -9
- package/lib/module/i18n/locales/en-US.json +3 -21
- package/lib/module/i18n/locales/es-ES.json +3 -21
- package/lib/module/i18n/locales/fr-FR.json +1 -9
- package/lib/module/i18n/locales/it-IT.json +1 -9
- package/lib/module/i18n/locales/ja-JP.json +1 -9
- package/lib/module/i18n/locales/ko-KR.json +1 -9
- package/lib/module/i18n/locales/pt-PT.json +1 -9
- package/lib/module/i18n/locales/zh-CN.json +1 -9
- package/lib/module/ui/context/OxyContext.js +24 -4
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/context/hooks/useAuthOperations.js +217 -100
- package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/module/ui/screens/AccountSettingsScreen.js +2 -319
- package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
- package/lib/module/ui/screens/OxyAuthScreen.js +179 -78
- package/lib/module/ui/screens/OxyAuthScreen.js.map +1 -1
- package/lib/module/ui/screens/PrivacySettingsScreen.js +0 -1
- package/lib/module/ui/screens/PrivacySettingsScreen.js.map +1 -1
- package/lib/module/ui/screens/SessionManagementScreen.js +44 -29
- package/lib/module/ui/screens/SessionManagementScreen.js.map +1 -1
- package/lib/module/ui/stores/authStore.js +14 -1
- package/lib/module/ui/stores/authStore.js.map +1 -1
- package/lib/typescript/core/OxyServices.d.ts +0 -1
- package/lib/typescript/core/OxyServices.d.ts.map +1 -1
- package/lib/typescript/core/mixins/OxyServices.auth.d.ts +3 -4
- package/lib/typescript/core/mixins/OxyServices.auth.d.ts.map +1 -1
- package/lib/typescript/core/mixins/OxyServices.devices.d.ts +1 -4
- package/lib/typescript/core/mixins/OxyServices.devices.d.ts.map +1 -1
- package/lib/typescript/core/mixins/index.d.ts +1 -64
- package/lib/typescript/core/mixins/index.d.ts.map +1 -1
- package/lib/typescript/crypto/signatureService.d.ts +2 -1
- package/lib/typescript/crypto/signatureService.d.ts.map +1 -1
- package/lib/typescript/models/interfaces.d.ts +1 -1
- package/lib/typescript/models/interfaces.d.ts.map +1 -1
- package/lib/typescript/types/bip39.d.ts +1 -0
- package/lib/typescript/types/buffer.d.ts +1 -0
- package/lib/typescript/types/color.d.ts +1 -0
- package/lib/typescript/types/elliptic.d.ts +1 -0
- package/lib/typescript/types/expo-crypto.d.ts +1 -0
- package/lib/typescript/types/expo-secure-store.d.ts +1 -0
- package/lib/typescript/ui/context/OxyContext.d.ts +11 -3
- package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/ui/context/hooks/useAuthOperations.d.ts +13 -5
- package/lib/typescript/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
- package/lib/typescript/ui/screens/OxyAuthScreen.d.ts +1 -0
- package/lib/typescript/ui/screens/OxyAuthScreen.d.ts.map +1 -1
- package/lib/typescript/ui/screens/PrivacySettingsScreen.d.ts.map +1 -1
- package/lib/typescript/ui/screens/SessionManagementScreen.d.ts.map +1 -1
- package/lib/typescript/ui/stores/authStore.d.ts +4 -0
- package/lib/typescript/ui/stores/authStore.d.ts.map +1 -1
- package/package.json +6 -5
- package/src/core/OxyServices.ts +0 -1
- package/src/core/mixins/OxyServices.auth.ts +3 -8
- package/src/core/mixins/OxyServices.devices.ts +1 -4
- package/src/core/mixins/index.ts +2 -5
- package/src/crypto/index.ts +1 -0
- package/src/crypto/keyManager.ts +1 -0
- package/src/crypto/polyfill.ts +1 -0
- package/src/crypto/recoveryPhrase.ts +1 -0
- package/src/crypto/signatureService.ts +4 -5
- package/src/i18n/locales/ar-SA.json +1 -9
- package/src/i18n/locales/ca-ES.json +1 -9
- package/src/i18n/locales/de-DE.json +1 -9
- package/src/i18n/locales/en-US.json +3 -21
- package/src/i18n/locales/es-ES.json +3 -21
- package/src/i18n/locales/fr-FR.json +1 -9
- package/src/i18n/locales/it-IT.json +1 -9
- package/src/i18n/locales/ja-JP.json +1 -9
- package/src/i18n/locales/ko-KR.json +1 -9
- package/src/i18n/locales/pt-PT.json +1 -9
- package/src/i18n/locales/zh-CN.json +1 -9
- package/src/models/interfaces.ts +1 -1
- package/src/types/bip39.d.ts +1 -0
- package/src/types/buffer.d.ts +1 -0
- package/src/types/color.d.ts +1 -0
- package/src/types/elliptic.d.ts +1 -0
- package/src/types/expo-crypto.d.ts +1 -0
- package/src/types/expo-secure-store.d.ts +1 -0
- package/src/ui/context/OxyContext.tsx +35 -3
- package/src/ui/context/hooks/useAuthOperations.ts +212 -98
- package/src/ui/screens/AccountSettingsScreen.tsx +1 -201
- package/src/ui/screens/OxyAuthScreen.tsx +193 -69
- package/src/ui/screens/PrivacySettingsScreen.tsx +0 -2
- package/src/ui/screens/SessionManagementScreen.tsx +43 -26
- package/src/ui/stores/authStore.ts +31 -2
- package/lib/commonjs/core/mixins/OxyServices.totp.js +0 -53
- package/lib/commonjs/core/mixins/OxyServices.totp.js.map +0 -1
- package/lib/commonjs/ui/components/profile/TwoFactorSetupModal.js +0 -467
- package/lib/commonjs/ui/components/profile/TwoFactorSetupModal.js.map +0 -1
- package/lib/module/core/mixins/OxyServices.totp.js +0 -49
- package/lib/module/core/mixins/OxyServices.totp.js.map +0 -1
- package/lib/module/ui/components/profile/TwoFactorSetupModal.js +0 -460
- package/lib/module/ui/components/profile/TwoFactorSetupModal.js.map +0 -1
- package/lib/typescript/core/mixins/OxyServices.totp.d.ts +0 -66
- package/lib/typescript/core/mixins/OxyServices.totp.d.ts.map +0 -1
- package/lib/typescript/ui/components/profile/TwoFactorSetupModal.d.ts +0 -11
- package/lib/typescript/ui/components/profile/TwoFactorSetupModal.d.ts.map +0 -1
- package/src/core/mixins/OxyServices.totp.ts +0 -36
- 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 (
|
|
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 [
|
|
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
|
-
|
|
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
|
-
|
|
258
|
+
cleanup();
|
|
141
259
|
};
|
|
142
|
-
}, [
|
|
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
|
-
|
|
270
|
+
cleanup();
|
|
153
271
|
setAuthSession(null);
|
|
154
272
|
setError('Session expired. Please try again.');
|
|
155
273
|
}
|
|
156
|
-
}, [authSession,
|
|
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
|
-
|
|
312
|
+
cleanup();
|
|
195
313
|
generateAuthSession();
|
|
196
|
-
}, [generateAuthSession,
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
{
|
|
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,
|