@oxyhq/services 5.15.9 → 5.16.1
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.assets.js +15 -0
- package/lib/commonjs/core/mixins/OxyServices.assets.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/index.js.map +1 -1
- 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 +16 -7
- 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.assets.js +15 -0
- package/lib/module/core/mixins/OxyServices.assets.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/index.js.map +1 -1
- 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 +16 -7
- 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.assets.d.ts +7 -1
- package/lib/typescript/core/mixins/OxyServices.assets.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 +2 -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/index.d.ts +1 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/models/interfaces.d.ts +22 -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.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 +5 -4
- package/src/core/OxyServices.ts +0 -1
- package/src/core/mixins/OxyServices.assets.ts +16 -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/index.ts +4 -1
- package/src/models/interfaces.ts +24 -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 +16 -8
- 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
|
@@ -26,14 +26,17 @@ export interface UseAuthOperationsOptions {
|
|
|
26
26
|
loginFailure: (message: string) => void;
|
|
27
27
|
logoutStore: () => void;
|
|
28
28
|
setAuthState: (state: Partial<AuthState>) => void;
|
|
29
|
+
// Identity sync store actions
|
|
30
|
+
setIdentitySynced: (synced: boolean) => void;
|
|
31
|
+
setSyncing: (syncing: boolean) => void;
|
|
29
32
|
logger?: (message: string, error?: unknown) => void;
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
export interface UseAuthOperationsResult {
|
|
33
|
-
/** Create a new identity and
|
|
34
|
-
createIdentity: (
|
|
36
|
+
/** Create a new identity locally (offline-first) and optionally sync with server */
|
|
37
|
+
createIdentity: () => Promise<{ recoveryPhrase: string[]; synced: boolean }>;
|
|
35
38
|
/** Import an existing identity from recovery phrase */
|
|
36
|
-
importIdentity: (phrase: string
|
|
39
|
+
importIdentity: (phrase: string) => Promise<{ synced: boolean }>;
|
|
37
40
|
/** Sign in with existing identity on device */
|
|
38
41
|
signIn: (deviceName?: string) => Promise<User>;
|
|
39
42
|
/** Logout from current session */
|
|
@@ -44,6 +47,10 @@ export interface UseAuthOperationsResult {
|
|
|
44
47
|
hasIdentity: () => Promise<boolean>;
|
|
45
48
|
/** Get the public key of the stored identity */
|
|
46
49
|
getPublicKey: () => Promise<string | null>;
|
|
50
|
+
/** Check if identity is synced with server */
|
|
51
|
+
isIdentitySynced: () => Promise<boolean>;
|
|
52
|
+
/** Sync local identity with server (when online) */
|
|
53
|
+
syncIdentity: () => Promise<User>;
|
|
47
54
|
}
|
|
48
55
|
|
|
49
56
|
const LOGIN_ERROR_CODE = 'LOGIN_ERROR';
|
|
@@ -71,103 +78,12 @@ export const useAuthOperations = ({
|
|
|
71
78
|
loginSuccess,
|
|
72
79
|
loginFailure,
|
|
73
80
|
logoutStore,
|
|
74
|
-
|
|
75
|
-
|
|
81
|
+
setAuthState,
|
|
82
|
+
setIdentitySynced,
|
|
83
|
+
setSyncing,
|
|
84
|
+
logger,
|
|
76
85
|
}: UseAuthOperationsOptions): UseAuthOperationsResult => {
|
|
77
86
|
|
|
78
|
-
/**
|
|
79
|
-
* Create a new identity with recovery phrase
|
|
80
|
-
*/
|
|
81
|
-
const createIdentity = useCallback(
|
|
82
|
-
async (username: string, email?: string): Promise<{ user: User; recoveryPhrase: string[] }> => {
|
|
83
|
-
if (!storage) throw new Error('Storage not initialized');
|
|
84
|
-
|
|
85
|
-
setAuthState({ isLoading: true, error: null });
|
|
86
|
-
|
|
87
|
-
try {
|
|
88
|
-
// Generate new identity with recovery phrase
|
|
89
|
-
const { phrase, words, publicKey } = await RecoveryPhraseService.generateIdentityWithRecovery();
|
|
90
|
-
|
|
91
|
-
// Create registration signature
|
|
92
|
-
const { signature, timestamp } = await SignatureService.createRegistrationSignature(username, email);
|
|
93
|
-
|
|
94
|
-
// Register with server
|
|
95
|
-
const { user } = await oxyServices.register(publicKey, username, signature, timestamp, email);
|
|
96
|
-
|
|
97
|
-
// Now sign in to create a session
|
|
98
|
-
const fullUser = await performSignIn(publicKey);
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
user: fullUser,
|
|
102
|
-
recoveryPhrase: words,
|
|
103
|
-
};
|
|
104
|
-
} catch (error) {
|
|
105
|
-
// Clean up identity if registration failed
|
|
106
|
-
await KeyManager.deleteIdentity().catch(() => {});
|
|
107
|
-
|
|
108
|
-
const message = handleAuthError(error, {
|
|
109
|
-
defaultMessage: 'Failed to create identity',
|
|
110
|
-
code: REGISTER_ERROR_CODE,
|
|
111
|
-
onError,
|
|
112
|
-
setAuthError: (msg) => setAuthState({ error: msg }),
|
|
113
|
-
logger,
|
|
114
|
-
});
|
|
115
|
-
loginFailure(message);
|
|
116
|
-
throw error;
|
|
117
|
-
} finally {
|
|
118
|
-
setAuthState({ isLoading: false });
|
|
119
|
-
}
|
|
120
|
-
},
|
|
121
|
-
[oxyServices, storage, setAuthState, loginFailure, onError, logger],
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Import identity from recovery phrase
|
|
126
|
-
*/
|
|
127
|
-
const importIdentity = useCallback(
|
|
128
|
-
async (phrase: string, username?: string, email?: string): Promise<User> => {
|
|
129
|
-
if (!storage) throw new Error('Storage not initialized');
|
|
130
|
-
|
|
131
|
-
setAuthState({ isLoading: true, error: null });
|
|
132
|
-
|
|
133
|
-
try {
|
|
134
|
-
// Restore identity from phrase
|
|
135
|
-
const publicKey = await RecoveryPhraseService.restoreFromPhrase(phrase);
|
|
136
|
-
|
|
137
|
-
// Check if this identity is already registered
|
|
138
|
-
const { registered } = await oxyServices.checkPublicKeyRegistered(publicKey);
|
|
139
|
-
|
|
140
|
-
if (registered) {
|
|
141
|
-
// Identity exists, just sign in
|
|
142
|
-
return await performSignIn(publicKey);
|
|
143
|
-
} else {
|
|
144
|
-
// Need to register this identity
|
|
145
|
-
if (!username) {
|
|
146
|
-
throw new Error('Username is required for new registration');
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const { signature, timestamp } = await SignatureService.createRegistrationSignature(username, email);
|
|
150
|
-
await oxyServices.register(publicKey, username, signature, timestamp, email);
|
|
151
|
-
|
|
152
|
-
return await performSignIn(publicKey);
|
|
153
|
-
}
|
|
154
|
-
} catch (error) {
|
|
155
|
-
const message = handleAuthError(error, {
|
|
156
|
-
defaultMessage: 'Failed to import identity',
|
|
157
|
-
code: REGISTER_ERROR_CODE,
|
|
158
|
-
onError,
|
|
159
|
-
setAuthError: (msg) => setAuthState({ error: msg }),
|
|
160
|
-
logger,
|
|
161
|
-
});
|
|
162
|
-
loginFailure(message);
|
|
163
|
-
throw error;
|
|
164
|
-
} finally {
|
|
165
|
-
setAuthState({ isLoading: false });
|
|
166
|
-
}
|
|
167
|
-
},
|
|
168
|
-
[oxyServices, storage, setAuthState, loginFailure, onError, logger],
|
|
169
|
-
);
|
|
170
|
-
|
|
171
87
|
/**
|
|
172
88
|
* Internal function to perform challenge-response sign in
|
|
173
89
|
*/
|
|
@@ -181,6 +97,10 @@ export const useAuthOperations = ({
|
|
|
181
97
|
// Request challenge
|
|
182
98
|
const { challenge } = await oxyServices.requestChallenge(publicKey);
|
|
183
99
|
|
|
100
|
+
// Note: Biometric authentication check should be handled by the app layer
|
|
101
|
+
// (e.g., accounts app) before calling signIn. The biometric preference is stored
|
|
102
|
+
// in local storage as 'oxy_biometric_enabled' and can be checked there.
|
|
103
|
+
|
|
184
104
|
// Sign the challenge
|
|
185
105
|
const { challenge: signature, timestamp } = await SignatureService.signChallenge(challenge);
|
|
186
106
|
|
|
@@ -261,6 +181,198 @@ export const useAuthOperations = ({
|
|
|
261
181
|
],
|
|
262
182
|
);
|
|
263
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Create a new identity with recovery phrase (offline-first)
|
|
186
|
+
* Identity is purely cryptographic - no username or email required
|
|
187
|
+
*/
|
|
188
|
+
const createIdentity = useCallback(
|
|
189
|
+
async (): Promise<{ recoveryPhrase: string[]; synced: boolean }> => {
|
|
190
|
+
if (!storage) throw new Error('Storage not initialized');
|
|
191
|
+
|
|
192
|
+
setAuthState({ isLoading: true, error: null });
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
// Generate new identity with recovery phrase (works offline)
|
|
196
|
+
const { phrase, words, publicKey } = await RecoveryPhraseService.generateIdentityWithRecovery();
|
|
197
|
+
|
|
198
|
+
// Mark as not synced
|
|
199
|
+
await storage.setItem('oxy_identity_synced', 'false');
|
|
200
|
+
setIdentitySynced(false);
|
|
201
|
+
|
|
202
|
+
// Try to sync with server (will succeed if online)
|
|
203
|
+
try {
|
|
204
|
+
const { signature, timestamp } = await SignatureService.createRegistrationSignature();
|
|
205
|
+
await oxyServices.register(publicKey, signature, timestamp);
|
|
206
|
+
|
|
207
|
+
// Mark as synced (Zustand store + storage)
|
|
208
|
+
await storage.setItem('oxy_identity_synced', 'true');
|
|
209
|
+
setIdentitySynced(true);
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
recoveryPhrase: words,
|
|
213
|
+
synced: true,
|
|
214
|
+
};
|
|
215
|
+
} catch (syncError) {
|
|
216
|
+
// Offline or server error - identity is created locally but not synced
|
|
217
|
+
if (__DEV__) {
|
|
218
|
+
console.log('[Auth] Identity created locally, will sync when online:', syncError);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
recoveryPhrase: words,
|
|
223
|
+
synced: false,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
} catch (error) {
|
|
227
|
+
// Clean up identity if generation failed
|
|
228
|
+
await KeyManager.deleteIdentity().catch(() => {});
|
|
229
|
+
await storage.removeItem('oxy_identity_synced').catch(() => {});
|
|
230
|
+
setIdentitySynced(true);
|
|
231
|
+
|
|
232
|
+
const message = handleAuthError(error, {
|
|
233
|
+
defaultMessage: 'Failed to create identity',
|
|
234
|
+
code: REGISTER_ERROR_CODE,
|
|
235
|
+
onError,
|
|
236
|
+
setAuthError: (msg) => setAuthState({ error: msg }),
|
|
237
|
+
logger,
|
|
238
|
+
});
|
|
239
|
+
loginFailure(message);
|
|
240
|
+
throw error;
|
|
241
|
+
} finally {
|
|
242
|
+
setAuthState({ isLoading: false });
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
[oxyServices, storage, setAuthState, loginFailure, onError, logger, setIdentitySynced],
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Check if identity is synced with server (reads from storage for persistence)
|
|
250
|
+
*/
|
|
251
|
+
const isIdentitySyncedFn = useCallback(async (): Promise<boolean> => {
|
|
252
|
+
if (!storage) return true;
|
|
253
|
+
const synced = await storage.getItem('oxy_identity_synced');
|
|
254
|
+
const isSynced = synced !== 'false';
|
|
255
|
+
setIdentitySynced(isSynced);
|
|
256
|
+
return isSynced;
|
|
257
|
+
}, [storage, setIdentitySynced]);
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Sync local identity with server (call when online)
|
|
261
|
+
*/
|
|
262
|
+
const syncIdentity = useCallback(
|
|
263
|
+
async (): Promise<User> => {
|
|
264
|
+
if (!storage) throw new Error('Storage not initialized');
|
|
265
|
+
|
|
266
|
+
setAuthState({ isLoading: true, error: null });
|
|
267
|
+
setSyncing(true);
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
const publicKey = await KeyManager.getPublicKey();
|
|
271
|
+
if (!publicKey) {
|
|
272
|
+
throw new Error('No identity found on this device');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Check if already synced
|
|
276
|
+
const alreadySynced = await storage.getItem('oxy_identity_synced');
|
|
277
|
+
if (alreadySynced === 'true') {
|
|
278
|
+
// Already synced, just sign in
|
|
279
|
+
setIdentitySynced(true);
|
|
280
|
+
return await performSignIn(publicKey);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Check if already registered on server
|
|
284
|
+
const { registered } = await oxyServices.checkPublicKeyRegistered(publicKey);
|
|
285
|
+
|
|
286
|
+
if (!registered) {
|
|
287
|
+
// Register with server (identity is just the publicKey)
|
|
288
|
+
const { signature, timestamp } = await SignatureService.createRegistrationSignature();
|
|
289
|
+
await oxyServices.register(publicKey, signature, timestamp);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Mark as synced (Zustand store + storage)
|
|
293
|
+
await storage.setItem('oxy_identity_synced', 'true');
|
|
294
|
+
setIdentitySynced(true);
|
|
295
|
+
|
|
296
|
+
// Sign in
|
|
297
|
+
return await performSignIn(publicKey);
|
|
298
|
+
} catch (error) {
|
|
299
|
+
const message = handleAuthError(error, {
|
|
300
|
+
defaultMessage: 'Failed to sync identity',
|
|
301
|
+
code: REGISTER_ERROR_CODE,
|
|
302
|
+
onError,
|
|
303
|
+
setAuthError: (msg) => setAuthState({ error: msg }),
|
|
304
|
+
logger,
|
|
305
|
+
});
|
|
306
|
+
loginFailure(message);
|
|
307
|
+
throw error;
|
|
308
|
+
} finally {
|
|
309
|
+
setAuthState({ isLoading: false });
|
|
310
|
+
setSyncing(false);
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
[oxyServices, storage, setAuthState, performSignIn, loginFailure, onError, logger, setSyncing, setIdentitySynced],
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Import identity from recovery phrase (offline-first)
|
|
318
|
+
*/
|
|
319
|
+
const importIdentity = useCallback(
|
|
320
|
+
async (phrase: string): Promise<{ synced: boolean }> => {
|
|
321
|
+
if (!storage) throw new Error('Storage not initialized');
|
|
322
|
+
|
|
323
|
+
setAuthState({ isLoading: true, error: null });
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
// Restore identity from phrase (works offline)
|
|
327
|
+
const publicKey = await RecoveryPhraseService.restoreFromPhrase(phrase);
|
|
328
|
+
|
|
329
|
+
// Mark as not synced
|
|
330
|
+
await storage.setItem('oxy_identity_synced', 'false');
|
|
331
|
+
setIdentitySynced(false);
|
|
332
|
+
|
|
333
|
+
// Try to sync with server
|
|
334
|
+
try {
|
|
335
|
+
// Check if this identity is already registered
|
|
336
|
+
const { registered } = await oxyServices.checkPublicKeyRegistered(publicKey);
|
|
337
|
+
|
|
338
|
+
if (registered) {
|
|
339
|
+
// Identity exists, mark as synced
|
|
340
|
+
await storage.setItem('oxy_identity_synced', 'true');
|
|
341
|
+
setIdentitySynced(true);
|
|
342
|
+
return { synced: true };
|
|
343
|
+
} else {
|
|
344
|
+
// Need to register this identity (identity is just the publicKey)
|
|
345
|
+
const { signature, timestamp } = await SignatureService.createRegistrationSignature();
|
|
346
|
+
await oxyServices.register(publicKey, signature, timestamp);
|
|
347
|
+
|
|
348
|
+
await storage.setItem('oxy_identity_synced', 'true');
|
|
349
|
+
setIdentitySynced(true);
|
|
350
|
+
return { synced: true };
|
|
351
|
+
}
|
|
352
|
+
} catch (syncError) {
|
|
353
|
+
// Offline - identity restored locally but not synced
|
|
354
|
+
if (__DEV__) {
|
|
355
|
+
console.log('[Auth] Identity imported locally, will sync when online:', syncError);
|
|
356
|
+
}
|
|
357
|
+
return { synced: false };
|
|
358
|
+
}
|
|
359
|
+
} catch (error) {
|
|
360
|
+
const message = handleAuthError(error, {
|
|
361
|
+
defaultMessage: 'Failed to import identity',
|
|
362
|
+
code: REGISTER_ERROR_CODE,
|
|
363
|
+
onError,
|
|
364
|
+
setAuthError: (msg) => setAuthState({ error: msg }),
|
|
365
|
+
logger,
|
|
366
|
+
});
|
|
367
|
+
loginFailure(message);
|
|
368
|
+
throw error;
|
|
369
|
+
} finally {
|
|
370
|
+
setAuthState({ isLoading: false });
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
[oxyServices, storage, setAuthState, loginFailure, onError, logger, setIdentitySynced],
|
|
374
|
+
);
|
|
375
|
+
|
|
264
376
|
/**
|
|
265
377
|
* Sign in with existing identity on device
|
|
266
378
|
*/
|
|
@@ -396,5 +508,7 @@ export const useAuthOperations = ({
|
|
|
396
508
|
logoutAll,
|
|
397
509
|
hasIdentity,
|
|
398
510
|
getPublicKey,
|
|
511
|
+
isIdentitySynced: isIdentitySyncedFn,
|
|
512
|
+
syncIdentity,
|
|
399
513
|
};
|
|
400
514
|
};
|
|
@@ -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
|
};
|