@oxyhq/services 10.2.10 → 10.2.11

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 (179) hide show
  1. package/lib/commonjs/ui/components/ActingAsBanner.js +2 -1
  2. package/lib/commonjs/ui/components/ActingAsBanner.js.map +1 -1
  3. package/lib/commonjs/ui/components/StepBasedScreen.js +19 -10
  4. package/lib/commonjs/ui/components/StepBasedScreen.js.map +1 -1
  5. package/lib/commonjs/ui/context/hooks/useAuthOperations.js +38 -122
  6. package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
  7. package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js +9 -7
  8. package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js.map +1 -1
  9. package/lib/commonjs/ui/hooks/useDeviceAccounts.js +20 -25
  10. package/lib/commonjs/ui/hooks/useDeviceAccounts.js.map +1 -1
  11. package/lib/commonjs/ui/hooks/useProfileEditing.js +19 -10
  12. package/lib/commonjs/ui/hooks/useProfileEditing.js.map +1 -1
  13. package/lib/commonjs/ui/screens/AppInfoScreen.js +1 -2
  14. package/lib/commonjs/ui/screens/AppInfoScreen.js.map +1 -1
  15. package/lib/commonjs/ui/screens/ConnectedAppsScreen.js +1 -2
  16. package/lib/commonjs/ui/screens/ConnectedAppsScreen.js.map +1 -1
  17. package/lib/commonjs/ui/screens/EditProfileFieldScreen.js +11 -13
  18. package/lib/commonjs/ui/screens/EditProfileFieldScreen.js.map +1 -1
  19. package/lib/commonjs/ui/screens/FeedbackScreen.js +2 -3
  20. package/lib/commonjs/ui/screens/FeedbackScreen.js.map +1 -1
  21. package/lib/commonjs/ui/screens/HelpSupportScreen.js +2 -2
  22. package/lib/commonjs/ui/screens/HelpSupportScreen.js.map +1 -1
  23. package/lib/commonjs/ui/screens/HistoryViewScreen.js +2 -2
  24. package/lib/commonjs/ui/screens/HistoryViewScreen.js.map +1 -1
  25. package/lib/commonjs/ui/screens/LanguageSelectorScreen.js +2 -3
  26. package/lib/commonjs/ui/screens/LanguageSelectorScreen.js.map +1 -1
  27. package/lib/commonjs/ui/screens/LegalDocumentsScreen.js +2 -2
  28. package/lib/commonjs/ui/screens/LegalDocumentsScreen.js.map +1 -1
  29. package/lib/commonjs/ui/screens/ManageAccountScreen.js +1 -2
  30. package/lib/commonjs/ui/screens/ManageAccountScreen.js.map +1 -1
  31. package/lib/commonjs/ui/screens/NotificationsScreen.js +2 -2
  32. package/lib/commonjs/ui/screens/NotificationsScreen.js.map +1 -1
  33. package/lib/commonjs/ui/screens/PaymentGatewayScreen.js +2 -2
  34. package/lib/commonjs/ui/screens/PaymentGatewayScreen.js.map +1 -1
  35. package/lib/commonjs/ui/screens/PreferencesScreen.js +2 -2
  36. package/lib/commonjs/ui/screens/PreferencesScreen.js.map +1 -1
  37. package/lib/commonjs/ui/screens/ProfileScreen.js +4 -3
  38. package/lib/commonjs/ui/screens/ProfileScreen.js.map +1 -1
  39. package/lib/commonjs/ui/screens/SavesCollectionsScreen.js +3 -4
  40. package/lib/commonjs/ui/screens/SavesCollectionsScreen.js.map +1 -1
  41. package/lib/commonjs/ui/screens/trust/TrustCenterScreen.js +2 -2
  42. package/lib/commonjs/ui/screens/trust/TrustCenterScreen.js.map +1 -1
  43. package/lib/commonjs/ui/screens/trust/TrustFAQScreen.js +2 -2
  44. package/lib/commonjs/ui/screens/trust/TrustFAQScreen.js.map +1 -1
  45. package/lib/commonjs/ui/screens/trust/TrustRewardsScreen.js +2 -2
  46. package/lib/commonjs/ui/screens/trust/TrustRewardsScreen.js.map +1 -1
  47. package/lib/commonjs/ui/stores/authStore.js.map +1 -1
  48. package/lib/commonjs/ui/utils/avatarUtils.js +1 -1
  49. package/lib/commonjs/ui/utils/avatarUtils.js.map +1 -1
  50. package/lib/module/ui/components/ActingAsBanner.js +2 -1
  51. package/lib/module/ui/components/ActingAsBanner.js.map +1 -1
  52. package/lib/module/ui/components/StepBasedScreen.js +19 -10
  53. package/lib/module/ui/components/StepBasedScreen.js.map +1 -1
  54. package/lib/module/ui/context/hooks/useAuthOperations.js +38 -122
  55. package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
  56. package/lib/module/ui/hooks/mutations/useAccountMutations.js +9 -7
  57. package/lib/module/ui/hooks/mutations/useAccountMutations.js.map +1 -1
  58. package/lib/module/ui/hooks/useDeviceAccounts.js +20 -25
  59. package/lib/module/ui/hooks/useDeviceAccounts.js.map +1 -1
  60. package/lib/module/ui/hooks/useProfileEditing.js +19 -10
  61. package/lib/module/ui/hooks/useProfileEditing.js.map +1 -1
  62. package/lib/module/ui/screens/AppInfoScreen.js +1 -1
  63. package/lib/module/ui/screens/AppInfoScreen.js.map +1 -1
  64. package/lib/module/ui/screens/ConnectedAppsScreen.js +1 -1
  65. package/lib/module/ui/screens/ConnectedAppsScreen.js.map +1 -1
  66. package/lib/module/ui/screens/EditProfileFieldScreen.js +10 -11
  67. package/lib/module/ui/screens/EditProfileFieldScreen.js.map +1 -1
  68. package/lib/module/ui/screens/FeedbackScreen.js +1 -1
  69. package/lib/module/ui/screens/FeedbackScreen.js.map +1 -1
  70. package/lib/module/ui/screens/HelpSupportScreen.js +1 -1
  71. package/lib/module/ui/screens/HelpSupportScreen.js.map +1 -1
  72. package/lib/module/ui/screens/HistoryViewScreen.js +1 -1
  73. package/lib/module/ui/screens/HistoryViewScreen.js.map +1 -1
  74. package/lib/module/ui/screens/LanguageSelectorScreen.js +1 -1
  75. package/lib/module/ui/screens/LanguageSelectorScreen.js.map +1 -1
  76. package/lib/module/ui/screens/LegalDocumentsScreen.js +1 -1
  77. package/lib/module/ui/screens/LegalDocumentsScreen.js.map +1 -1
  78. package/lib/module/ui/screens/ManageAccountScreen.js +1 -1
  79. package/lib/module/ui/screens/ManageAccountScreen.js.map +1 -1
  80. package/lib/module/ui/screens/NotificationsScreen.js +1 -1
  81. package/lib/module/ui/screens/NotificationsScreen.js.map +1 -1
  82. package/lib/module/ui/screens/PaymentGatewayScreen.js +1 -1
  83. package/lib/module/ui/screens/PaymentGatewayScreen.js.map +1 -1
  84. package/lib/module/ui/screens/PreferencesScreen.js +1 -1
  85. package/lib/module/ui/screens/PreferencesScreen.js.map +1 -1
  86. package/lib/module/ui/screens/ProfileScreen.js +5 -4
  87. package/lib/module/ui/screens/ProfileScreen.js.map +1 -1
  88. package/lib/module/ui/screens/SavesCollectionsScreen.js +2 -3
  89. package/lib/module/ui/screens/SavesCollectionsScreen.js.map +1 -1
  90. package/lib/module/ui/screens/trust/TrustCenterScreen.js +1 -1
  91. package/lib/module/ui/screens/trust/TrustCenterScreen.js.map +1 -1
  92. package/lib/module/ui/screens/trust/TrustFAQScreen.js +1 -1
  93. package/lib/module/ui/screens/trust/TrustFAQScreen.js.map +1 -1
  94. package/lib/module/ui/screens/trust/TrustRewardsScreen.js +1 -1
  95. package/lib/module/ui/screens/trust/TrustRewardsScreen.js.map +1 -1
  96. package/lib/module/ui/stores/authStore.js.map +1 -1
  97. package/lib/module/ui/utils/avatarUtils.js +1 -1
  98. package/lib/module/ui/utils/avatarUtils.js.map +1 -1
  99. package/lib/typescript/commonjs/ui/components/ActingAsBanner.d.ts.map +1 -1
  100. package/lib/typescript/commonjs/ui/components/StepBasedScreen.d.ts +5 -3
  101. package/lib/typescript/commonjs/ui/components/StepBasedScreen.d.ts.map +1 -1
  102. package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts +1 -1
  103. package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  104. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +52 -6
  105. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  106. package/lib/typescript/commonjs/ui/hooks/useDeviceAccounts.d.ts.map +1 -1
  107. package/lib/typescript/commonjs/ui/hooks/useProfileEditing.d.ts +22 -18
  108. package/lib/typescript/commonjs/ui/hooks/useProfileEditing.d.ts.map +1 -1
  109. package/lib/typescript/commonjs/ui/screens/EditProfileFieldScreen.d.ts.map +1 -1
  110. package/lib/typescript/commonjs/ui/screens/SavesCollectionsScreen.d.ts.map +1 -1
  111. package/lib/typescript/commonjs/ui/stores/authStore.d.ts +2 -10
  112. package/lib/typescript/commonjs/ui/stores/authStore.d.ts.map +1 -1
  113. package/lib/typescript/commonjs/ui/types/navigation.d.ts +0 -1
  114. package/lib/typescript/commonjs/ui/types/navigation.d.ts.map +1 -1
  115. package/lib/typescript/commonjs/ui/utils/avatarUtils.d.ts +2 -1
  116. package/lib/typescript/commonjs/ui/utils/avatarUtils.d.ts.map +1 -1
  117. package/lib/typescript/module/ui/components/ActingAsBanner.d.ts.map +1 -1
  118. package/lib/typescript/module/ui/components/StepBasedScreen.d.ts +5 -3
  119. package/lib/typescript/module/ui/components/StepBasedScreen.d.ts.map +1 -1
  120. package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts +1 -1
  121. package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  122. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +52 -6
  123. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  124. package/lib/typescript/module/ui/hooks/useDeviceAccounts.d.ts.map +1 -1
  125. package/lib/typescript/module/ui/hooks/useProfileEditing.d.ts +22 -18
  126. package/lib/typescript/module/ui/hooks/useProfileEditing.d.ts.map +1 -1
  127. package/lib/typescript/module/ui/screens/EditProfileFieldScreen.d.ts.map +1 -1
  128. package/lib/typescript/module/ui/screens/SavesCollectionsScreen.d.ts.map +1 -1
  129. package/lib/typescript/module/ui/stores/authStore.d.ts +2 -10
  130. package/lib/typescript/module/ui/stores/authStore.d.ts.map +1 -1
  131. package/lib/typescript/module/ui/types/navigation.d.ts +0 -1
  132. package/lib/typescript/module/ui/types/navigation.d.ts.map +1 -1
  133. package/lib/typescript/module/ui/utils/avatarUtils.d.ts +2 -1
  134. package/lib/typescript/module/ui/utils/avatarUtils.d.ts.map +1 -1
  135. package/package.json +3 -2
  136. package/src/ui/components/ActingAsBanner.tsx +2 -4
  137. package/src/ui/components/StepBasedScreen.tsx +37 -22
  138. package/src/ui/context/hooks/useAuthOperations.ts +51 -144
  139. package/src/ui/hooks/mutations/useAccountMutations.ts +9 -8
  140. package/src/ui/hooks/useDeviceAccounts.ts +20 -24
  141. package/src/ui/hooks/useProfileEditing.ts +43 -30
  142. package/src/ui/screens/AppInfoScreen.tsx +1 -1
  143. package/src/ui/screens/ConnectedAppsScreen.tsx +1 -1
  144. package/src/ui/screens/EditProfileFieldScreen.tsx +24 -14
  145. package/src/ui/screens/FeedbackScreen.tsx +1 -1
  146. package/src/ui/screens/HelpSupportScreen.tsx +1 -1
  147. package/src/ui/screens/HistoryViewScreen.tsx +1 -1
  148. package/src/ui/screens/LanguageSelectorScreen.tsx +1 -1
  149. package/src/ui/screens/LegalDocumentsScreen.tsx +1 -1
  150. package/src/ui/screens/ManageAccountScreen.tsx +1 -1
  151. package/src/ui/screens/NotificationsScreen.tsx +1 -1
  152. package/src/ui/screens/PaymentGatewayScreen.tsx +1 -1
  153. package/src/ui/screens/PreferencesScreen.tsx +1 -1
  154. package/src/ui/screens/ProfileScreen.tsx +4 -4
  155. package/src/ui/screens/SavesCollectionsScreen.tsx +3 -6
  156. package/src/ui/screens/trust/TrustCenterScreen.tsx +1 -1
  157. package/src/ui/screens/trust/TrustFAQScreen.tsx +1 -1
  158. package/src/ui/screens/trust/TrustRewardsScreen.tsx +1 -1
  159. package/src/ui/stores/authStore.ts +4 -13
  160. package/src/ui/types/navigation.ts +0 -4
  161. package/src/ui/utils/avatarUtils.ts +3 -2
  162. package/lib/commonjs/ui/styles/spacing.js +0 -68
  163. package/lib/commonjs/ui/styles/spacing.js.map +0 -1
  164. package/lib/commonjs/ui/utils/themeUtils.js +0 -37
  165. package/lib/commonjs/ui/utils/themeUtils.js.map +0 -1
  166. package/lib/module/ui/styles/spacing.js +0 -16
  167. package/lib/module/ui/styles/spacing.js.map +0 -1
  168. package/lib/module/ui/utils/themeUtils.js +0 -13
  169. package/lib/module/ui/utils/themeUtils.js.map +0 -1
  170. package/lib/typescript/commonjs/ui/styles/spacing.d.ts +0 -13
  171. package/lib/typescript/commonjs/ui/styles/spacing.d.ts.map +0 -1
  172. package/lib/typescript/commonjs/ui/utils/themeUtils.d.ts +0 -11
  173. package/lib/typescript/commonjs/ui/utils/themeUtils.d.ts.map +0 -1
  174. package/lib/typescript/module/ui/styles/spacing.d.ts +0 -13
  175. package/lib/typescript/module/ui/styles/spacing.d.ts.map +0 -1
  176. package/lib/typescript/module/ui/utils/themeUtils.d.ts +0 -11
  177. package/lib/typescript/module/ui/utils/themeUtils.d.ts.map +0 -1
  178. package/src/ui/styles/spacing.ts +0 -22
  179. package/src/ui/utils/themeUtils.ts +0 -18
@@ -11,11 +11,6 @@ import { SignatureService } from '@oxyhq/core';
11
11
  import { isWebBrowser } from '../../hooks/useWebSSO';
12
12
  import { clearActiveAuthuser, clearSsoBounceState } from '../../utils/activeAuthuser';
13
13
 
14
- /** Type guard for error objects with optional code and status properties */
15
- function isErrorWithCodeOrStatus(error: unknown): error is { code?: string; status?: number; message?: string } {
16
- return typeof error === 'object' && error !== null;
17
- }
18
-
19
14
  export interface UseAuthOperationsOptions {
20
15
  oxyServices: OxyServices;
21
16
  storage: StorageInterface | null;
@@ -55,7 +50,6 @@ const LOGOUT_ALL_ERROR_CODE = 'LOGOUT_ALL_ERROR';
55
50
  */
56
51
  export const useAuthOperations = ({
57
52
  oxyServices,
58
- storage,
59
53
  sessions,
60
54
  activeSessionId,
61
55
  setActiveSessionId,
@@ -77,7 +71,7 @@ export const useAuthOperations = ({
77
71
  sessionsRef.current = sessions;
78
72
 
79
73
  /**
80
- * Internal function to perform challenge-response sign in (works offline)
74
+ * Internal function to perform challenge-response sign in.
81
75
  */
82
76
  const performSignIn = useCallback(
83
77
  async (publicKey: string): Promise<User> => {
@@ -86,36 +80,8 @@ export const useAuthOperations = ({
86
80
  const deviceInfo = await DeviceManager.getDeviceInfo();
87
81
  const deviceName = deviceInfo.deviceName || DeviceManager.getDefaultDeviceName();
88
82
 
89
- let challenge: string;
90
- let isOffline = false;
91
-
92
- // Try to request challenge from server (online)
93
- try {
94
- const challengeResponse = await oxyServices.requestChallenge(publicKey);
95
- challenge = challengeResponse.challenge;
96
- } catch (error) {
97
- // Network error - generate challenge locally for offline sign-in
98
- const errorMessage = error instanceof Error ? error.message : String(error);
99
- const isNetworkError =
100
- errorMessage.includes('Network') ||
101
- errorMessage.includes('network') ||
102
- errorMessage.includes('Failed to fetch') ||
103
- errorMessage.includes('fetch failed') ||
104
- (isErrorWithCodeOrStatus(error) && error.code === 'NETWORK_ERROR') ||
105
- (isErrorWithCodeOrStatus(error) && error.status === 0);
106
-
107
- if (isNetworkError) {
108
- if (__DEV__ && logger) {
109
- logger('Network unavailable, performing offline sign-in');
110
- }
111
- // Generate challenge locally
112
- challenge = await SignatureService.generateChallenge();
113
- isOffline = true;
114
- } else {
115
- // Re-throw non-network errors
116
- throw error;
117
- }
118
- }
83
+ const challengeResponse = await oxyServices.requestChallenge(publicKey);
84
+ const challenge = challengeResponse.challenge;
119
85
 
120
86
  // Note: Biometric authentication check should be handled by the app layer
121
87
  // (e.g., accounts app) before calling signIn. The biometric preference is stored
@@ -127,123 +93,65 @@ export const useAuthOperations = ({
127
93
  let fullUser: User;
128
94
  let sessionResponse: SessionLoginResponse;
129
95
 
130
- if (isOffline) {
131
- // Offline sign-in: create local session and minimal user object
132
- if (__DEV__ && logger) {
133
- logger('Creating offline session');
134
- }
135
-
136
- // Generate a local session ID using cryptographically secure randomness
137
- const Crypto = await import('expo-crypto');
138
- const localSessionId = `offline_${Crypto.randomUUID()}`;
139
- const localDeviceId = `device_${Crypto.randomUUID()}`;
140
- const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(); // 7 days
141
-
142
- // Create minimal user object with publicKey as id
143
- fullUser = {
144
- id: publicKey, // Use publicKey as id (per migration document)
145
- publicKey,
146
- username: '',
147
- privacySettings: {},
148
- } as User;
149
-
150
- sessionResponse = {
151
- sessionId: localSessionId,
152
- deviceId: localDeviceId,
153
- expiresAt,
154
- user: {
155
- id: publicKey,
156
- username: '',
157
- },
158
- };
159
-
160
- // Store offline session locally
161
- const offlineSession: ClientSession = {
162
- sessionId: localSessionId,
163
- deviceId: localDeviceId,
164
- expiresAt,
165
- lastActive: new Date().toISOString(),
166
- userId: publicKey,
167
- isCurrent: true,
168
- };
169
-
170
- setActiveSessionId(localSessionId);
171
- await saveActiveSessionId(localSessionId);
172
- updateSessions([offlineSession], { merge: true });
173
-
174
- // Mark session as offline for later sync
175
- if (storage) {
176
- await storage.setItem(`oxy_session_${localSessionId}_offline`, 'true');
177
- }
178
-
96
+ // `verifyChallenge` plants the first access token internally, mirroring
97
+ // `claimSessionByToken`, so the client is authenticated as soon as this
98
+ // resolves.
99
+ sessionResponse = await oxyServices.verifyChallenge(
100
+ publicKey,
101
+ challenge,
102
+ signature,
103
+ timestamp,
104
+ deviceName,
105
+ deviceFingerprint,
106
+ );
107
+
108
+ // Get full user data
109
+ fullUser = await oxyServices.getUserBySession(sessionResponse.sessionId);
110
+
111
+ // Fetch device sessions
112
+ let allDeviceSessions: ClientSession[] = [];
113
+ try {
114
+ allDeviceSessions = await fetchSessionsWithFallback(oxyServices, sessionResponse.sessionId, {
115
+ fallbackDeviceId: sessionResponse.deviceId,
116
+ fallbackUserId: fullUser.id,
117
+ logger,
118
+ });
119
+ } catch (error) {
179
120
  if (__DEV__ && logger) {
180
- logger('Offline sign-in successful');
121
+ logger('Failed to fetch device sessions after login', error);
181
122
  }
182
- } else {
183
- // Online sign-in: use normal flow.
184
- // Verify and create session. `verifyChallenge` plants the first
185
- // access token (and refresh token) from the `/auth/verify` response
186
- // body internally — mirroring `claimSessionByToken` — so the client is
187
- // authenticated as soon as this resolves. Session IDs are not public
188
- // token-minting credentials; a token-less verify response simply leaves
189
- // the client without a bearer here.
190
- sessionResponse = await oxyServices.verifyChallenge(
191
- publicKey,
192
- challenge,
193
- signature,
194
- timestamp,
195
- deviceName,
196
- deviceFingerprint,
197
- );
123
+ }
198
124
 
199
- // Get full user data
200
- fullUser = await oxyServices.getUserBySession(sessionResponse.sessionId);
125
+ // Check for existing session for same user and switch to it to avoid duplicates
126
+ const existingSession = allDeviceSessions.find(
127
+ (session) =>
128
+ session.userId?.toString() === fullUser.id?.toString() &&
129
+ session.sessionId !== sessionResponse.sessionId,
130
+ );
201
131
 
202
- // Fetch device sessions
203
- let allDeviceSessions: ClientSession[] = [];
132
+ if (existingSession) {
133
+ // Switch to existing session instead of creating duplicate
204
134
  try {
205
- allDeviceSessions = await fetchSessionsWithFallback(oxyServices, sessionResponse.sessionId, {
206
- fallbackDeviceId: sessionResponse.deviceId,
207
- fallbackUserId: fullUser.id,
208
- logger,
209
- });
210
- } catch (error) {
135
+ await oxyServices.logoutSession(sessionResponse.sessionId, sessionResponse.sessionId);
136
+ } catch (logoutError) {
137
+ // Non-critical - continue to switch session even if logout fails
211
138
  if (__DEV__ && logger) {
212
- logger('Failed to fetch device sessions after login', error);
139
+ logger('Failed to logout duplicate session, continuing with switch', logoutError);
213
140
  }
214
141
  }
215
-
216
- // Check for existing session for same user and switch to it to avoid duplicates
217
- const existingSession = allDeviceSessions.find(
218
- (session) =>
219
- session.userId?.toString() === fullUser.id?.toString() &&
220
- session.sessionId !== sessionResponse.sessionId,
142
+ await switchSession(existingSession.sessionId);
143
+ updateSessions(
144
+ allDeviceSessions.filter((session) => session.sessionId !== sessionResponse.sessionId),
145
+ { merge: false },
221
146
  );
222
-
223
- if (existingSession) {
224
- // Switch to existing session instead of creating duplicate
225
- try {
226
- await oxyServices.logoutSession(sessionResponse.sessionId, sessionResponse.sessionId);
227
- } catch (logoutError) {
228
- // Non-critical - continue to switch session even if logout fails
229
- if (__DEV__ && logger) {
230
- logger('Failed to logout duplicate session, continuing with switch', logoutError);
231
- }
232
- }
233
- await switchSession(existingSession.sessionId);
234
- updateSessions(
235
- allDeviceSessions.filter((session) => session.sessionId !== sessionResponse.sessionId),
236
- { merge: false },
237
- );
238
- onAuthStateChange?.(fullUser);
239
- return fullUser;
240
- }
241
-
242
- setActiveSessionId(sessionResponse.sessionId);
243
- await saveActiveSessionId(sessionResponse.sessionId);
244
- updateSessions(allDeviceSessions, { merge: true });
147
+ onAuthStateChange?.(fullUser);
148
+ return fullUser;
245
149
  }
246
150
 
151
+ setActiveSessionId(sessionResponse.sessionId);
152
+ await saveActiveSessionId(sessionResponse.sessionId);
153
+ updateSessions(allDeviceSessions, { merge: true });
154
+
247
155
  await applyLanguagePreference(fullUser);
248
156
  loginSuccess(fullUser);
249
157
  onAuthStateChange?.(fullUser);
@@ -260,7 +168,6 @@ export const useAuthOperations = ({
260
168
  setActiveSessionId,
261
169
  switchSession,
262
170
  updateSessions,
263
- storage,
264
171
  ],
265
172
  );
266
173
 
@@ -7,6 +7,7 @@ import type {
7
7
  User,
8
8
  UserPreferences,
9
9
  } from '@oxyhq/core';
10
+ import type { UserProfileUpdate } from '@oxyhq/contracts';
10
11
  import {
11
12
  queryKeys,
12
13
  invalidateAccountQueries,
@@ -29,7 +30,7 @@ export const useUpdateProfile = () => {
29
30
 
30
31
  return useMutation({
31
32
  mutationKey: [...mutationKeys.account.updateProfile],
32
- mutationFn: async (updates: Partial<User>) => {
33
+ mutationFn: async (updates: UserProfileUpdate) => {
33
34
  return authenticatedApiCall<User>(
34
35
  oxyServices,
35
36
  activeSessionId,
@@ -46,17 +47,18 @@ export const useUpdateProfile = () => {
46
47
 
47
48
  // Optimistically update
48
49
  if (previousUser) {
49
- queryClient.setQueryData<User>(queryKeys.accounts.current(), {
50
+ const optimisticUser: User = {
50
51
  ...previousUser,
51
52
  ...updates,
52
- });
53
+ name: updates.name
54
+ ? { ...previousUser.name, ...updates.name }
55
+ : previousUser.name,
56
+ };
57
+ queryClient.setQueryData<User>(queryKeys.accounts.current(), optimisticUser);
53
58
 
54
59
  // Also update profile query if sessionId is available
55
60
  if (activeSessionId) {
56
- queryClient.setQueryData<User>(queryKeys.users.profile(activeSessionId), {
57
- ...previousUser,
58
- ...updates,
59
- });
61
+ queryClient.setQueryData<User>(queryKeys.users.profile(activeSessionId), optimisticUser);
60
62
  }
61
63
  }
62
64
 
@@ -684,4 +686,3 @@ export const useUploadFile = () => {
684
686
  },
685
687
  });
686
688
  };
687
-
@@ -252,19 +252,16 @@ export function useDeviceAccounts(): UseDeviceAccountsResult {
252
252
 
253
253
  if (fromSharedApex) {
254
254
  // Shared apex path: every entry carries a real per-account user.
255
- built = sharedAccounts.map((entry): DeviceAccount => {
256
- // `entry.user` is non-null on the refresh-all path; the core
257
- // mixin skips entries without a valid user. The fallback below
258
- // keeps rendering defensive if a server response is incomplete.
259
- const accountUser: DeviceAccountUser = entry.user ?? {
260
- id: '',
261
- username: '',
262
- };
255
+ built = sharedAccounts.flatMap((entry): DeviceAccount[] => {
256
+ if (!entry.user) {
257
+ return [];
258
+ }
259
+ const accountUser: DeviceAccountUser = entry.user;
263
260
  const displayName = getAccountDisplayName(accountUser, locale);
264
261
  const handle = getAccountFallbackHandle(accountUser);
265
- const email = entry.user?.email ?? null;
262
+ const email = entry.user.email ?? null;
266
263
  const secondaryHandle = handle ? `@${handle}` : null;
267
- return {
264
+ return [{
268
265
  sessionId: entry.sessionId,
269
266
  authuser: entry.authuser,
270
267
  // Provisional; finalised by `markCurrentAccount` below so the
@@ -274,37 +271,36 @@ export function useDeviceAccounts(): UseDeviceAccountsResult {
274
271
  // Real email, or null (NEVER synthesized). The UI uses the
275
272
  // `@handle` line only when email is genuinely absent.
276
273
  email: email ?? secondaryHandle,
277
- avatarUrl: resolveAvatarUrl(entry.user?.avatar),
278
- color: entry.user?.color ?? null,
274
+ avatarUrl: resolveAvatarUrl(entry.user.avatar),
275
+ color: entry.user.color ?? null,
279
276
  user: accountUser,
280
- };
277
+ }];
281
278
  });
282
279
  } else {
283
280
  // Local fallback path: build from the SDK's multi-session store. The
284
281
  // active session row gets the full loaded `user`; inactive fallback
285
282
  // rows carry only what the `ClientSession` exposes (no synthesized
286
283
  // identity — they show the active user's data only when active).
287
- built = (sessions ?? []).map((session: ClientSession): DeviceAccount => {
284
+ built = (sessions ?? []).flatMap((session: ClientSession): DeviceAccount[] => {
288
285
  const isCurrent = session.sessionId === activeSessionId;
289
- const accountUser: DeviceAccountUser = isCurrent && user
290
- ? user
291
- : { id: session.userId ?? '', username: '' };
286
+ if (!isCurrent || !user) {
287
+ return [];
288
+ }
289
+ const accountUser: DeviceAccountUser = user;
292
290
  const displayName = getAccountDisplayName(accountUser, locale);
293
291
  const handle = getAccountFallbackHandle(accountUser);
294
- const email = isCurrent && user?.email ? user.email : null;
292
+ const email = user.email ?? null;
295
293
  const secondaryHandle = handle ? `@${handle}` : null;
296
- const avatar = isCurrent && user ? user.avatar : undefined;
297
- const color = isCurrent && user?.color ? user.color : null;
298
- return {
294
+ return [{
299
295
  sessionId: session.sessionId,
300
296
  authuser: session.authuser,
301
297
  isCurrent,
302
298
  displayName,
303
299
  email: email ?? secondaryHandle,
304
- avatarUrl: resolveAvatarUrl(avatar),
305
- color,
300
+ avatarUrl: resolveAvatarUrl(user.avatar),
301
+ color: user.color ?? null,
306
302
  user: accountUser,
307
- };
303
+ }];
308
304
  });
309
305
  }
310
306
 
@@ -1,38 +1,51 @@
1
1
  import { useCallback } from 'react';
2
- import { useI18n } from './useI18n';
3
2
  import { useUpdateProfile } from './mutations/useAccountMutations';
4
3
  import { useAuthStore } from '../stores/authStore';
4
+ import type { UserProfileUpdate } from '@oxyhq/contracts';
5
+
6
+ interface ProfileLocation {
7
+ id: string;
8
+ name: string;
9
+ label?: string;
10
+ coordinates?: { lat: number; lon: number };
11
+ }
12
+
13
+ interface ProfileLinkMetadata {
14
+ url: string;
15
+ title?: string;
16
+ description?: string;
17
+ image?: string;
18
+ id: string;
19
+ }
5
20
 
6
21
  export interface ProfileUpdateData {
7
- displayName?: string;
22
+ firstName?: string;
8
23
  lastName?: string;
9
24
  username?: string;
10
25
  email?: string;
11
26
  bio?: string;
12
27
  location?: string;
13
- locations?: Array<{
14
- id: string;
15
- name: string;
16
- label?: string;
17
- coordinates?: { lat: number; lon: number };
18
- }>;
28
+ locations?: ProfileLocation[];
19
29
  links?: string[];
20
- linksMetadata?: Array<{
21
- url: string;
22
- title?: string;
23
- description?: string;
24
- image?: string;
25
- id: string;
26
- }>;
30
+ linksMetadata?: ProfileLinkMetadata[];
27
31
  avatar?: string;
28
32
  }
29
33
 
34
+ type ProfileFieldValue = string | ProfileLocation[] | ProfileLinkMetadata[];
35
+
36
+ function isProfileLocationArray(value: ProfileFieldValue): value is ProfileLocation[] {
37
+ return Array.isArray(value) && value.every((item) => typeof item === 'object' && item !== null && 'name' in item);
38
+ }
39
+
40
+ function isProfileLinkMetadataArray(value: ProfileFieldValue): value is ProfileLinkMetadata[] {
41
+ return Array.isArray(value) && value.every((item) => typeof item === 'object' && item !== null && 'url' in item);
42
+ }
43
+
30
44
  /**
31
45
  * Hook for managing profile editing operations
32
46
  * Provides functions to update profile fields and handle saving
33
47
  */
34
48
  export const useProfileEditing = () => {
35
- const { t } = useI18n();
36
49
  const updateProfileMutation = useUpdateProfile();
37
50
 
38
51
  /**
@@ -40,7 +53,7 @@ export const useProfileEditing = () => {
40
53
  */
41
54
  const saveProfile = useCallback(async (updates: ProfileUpdateData) => {
42
55
  // Prepare update object
43
- const updateData: Record<string, any> = {};
56
+ const updateData: UserProfileUpdate = {};
44
57
 
45
58
  if (updates.username !== undefined) {
46
59
  updateData.username = updates.username;
@@ -70,11 +83,11 @@ export const useProfileEditing = () => {
70
83
  }
71
84
 
72
85
  // Handle name field
73
- if (updates.displayName !== undefined || updates.lastName !== undefined) {
86
+ if (updates.firstName !== undefined || updates.lastName !== undefined) {
74
87
  const currentUser = useAuthStore.getState().user;
75
88
  const currentName = currentUser?.name;
76
89
  updateData.name = {
77
- first: updates.displayName ?? (typeof currentName === 'object' ? currentName?.first : '') ?? '',
90
+ first: updates.firstName ?? (typeof currentName === 'object' ? currentName?.first : '') ?? '',
78
91
  last: updates.lastName ?? (typeof currentName === 'object' ? currentName?.last : '') ?? '',
79
92
  };
80
93
  }
@@ -86,33 +99,39 @@ export const useProfileEditing = () => {
86
99
  // Error toast is handled by the mutation
87
100
  return false;
88
101
  }
89
- }, [updateProfileMutation, t]);
102
+ }, [updateProfileMutation]);
90
103
 
91
104
  /**
92
105
  * Update a single profile field
93
106
  */
94
- const updateField = useCallback(async (field: string, value: any) => {
107
+ const updateField = useCallback(async (field: string, value: ProfileFieldValue) => {
95
108
  const updates: ProfileUpdateData = {};
96
109
 
97
110
  switch (field) {
98
- case 'displayName':
99
- updates.displayName = value;
111
+ case 'firstName':
112
+ if (typeof value !== 'string') return false;
113
+ updates.firstName = value;
100
114
  break;
101
115
  case 'username':
116
+ if (typeof value !== 'string') return false;
102
117
  updates.username = value;
103
118
  break;
104
119
  case 'email':
120
+ if (typeof value !== 'string') return false;
105
121
  updates.email = value;
106
122
  break;
107
123
  case 'bio':
124
+ if (typeof value !== 'string') return false;
108
125
  updates.bio = value;
109
126
  break;
110
127
  case 'location':
128
+ if (!isProfileLocationArray(value)) return false;
111
129
  updates.locations = value;
112
130
  break;
113
131
  case 'links':
132
+ if (!isProfileLinkMetadataArray(value)) return false;
114
133
  updates.linksMetadata = value;
115
- updates.links = value.map((link: any) => link.url || link);
134
+ updates.links = value.map((link) => link.url);
116
135
  break;
117
136
  default:
118
137
  return false;
@@ -129,9 +148,3 @@ export const useProfileEditing = () => {
129
148
  };
130
149
 
131
150
 
132
-
133
-
134
-
135
-
136
-
137
-
@@ -20,7 +20,7 @@ import { SettingsIcon } from '../components/SettingsIcon';
20
20
  import { useTheme } from '@oxyhq/bloom/theme';
21
21
  import { useColorScheme } from '../hooks/useColorScheme';
22
22
  import { Colors } from '../constants/theme';
23
- import { normalizeColorScheme } from '../utils/themeUtils';
23
+ import { normalizeColorScheme } from '@oxyhq/core';
24
24
  import { useOxy } from '../context/OxyContext';
25
25
  import { useI18n } from '../hooks/useI18n';
26
26
  import { SettingsListGroup, SettingsListItem } from '@oxyhq/bloom/settings-list';
@@ -15,7 +15,7 @@ import { useAuthorizedApps } from '../hooks/queries/useAccountQueries';
15
15
  import { useRevokeAuthorizedApp } from '../hooks/mutations/useAccountMutations';
16
16
  import { useColorScheme } from '../hooks/useColorScheme';
17
17
  import { Colors } from '../constants/theme';
18
- import { normalizeColorScheme, normalizeTheme } from '../utils/themeUtils';
18
+ import { normalizeColorScheme, normalizeTheme } from '@oxyhq/core';
19
19
 
20
20
  /**
21
21
  * Format an ISO-8601 timestamp as a human-readable relative time. Mirrors the
@@ -14,7 +14,7 @@ import {
14
14
  import { Ionicons } from '@expo/vector-icons';
15
15
  import type { BaseScreenProps } from '../types/navigation';
16
16
  import { useTheme } from '@oxyhq/bloom/theme';
17
- import { normalizeTheme } from '../utils/themeUtils';
17
+ import { normalizeTheme } from '@oxyhq/core';
18
18
  import Header from '../components/Header';
19
19
  import { useI18n } from '../hooks/useI18n';
20
20
  import { useOxy } from '../context/OxyContext';
@@ -59,6 +59,17 @@ interface EditProfileFieldScreenProps extends BaseScreenProps {
59
59
  fieldType?: ProfileFieldType;
60
60
  }
61
61
 
62
+ type EditableListItem = {
63
+ id: string;
64
+ name?: string;
65
+ label?: string;
66
+ url?: string;
67
+ title?: string;
68
+ description?: string;
69
+ image?: string;
70
+ coordinates?: { lat: number; lon: number };
71
+ };
72
+
62
73
  /**
63
74
  * EditProfileFieldScreen - A dedicated screen for editing profile fields
64
75
  *
@@ -84,7 +95,7 @@ const EditProfileFieldScreen: React.FC<EditProfileFieldScreenProps> = ({
84
95
  const [fieldErrors, setFieldErrors] = useState<Record<string, string | undefined>>({});
85
96
 
86
97
  // State for list fields (locations, links)
87
- const [listItems, setListItems] = useState<Array<{ id: string; [key: string]: unknown }>>([]);
98
+ const [listItems, setListItems] = useState<EditableListItem[]>([]);
88
99
  const [newItemValue, setNewItemValue] = useState('');
89
100
 
90
101
  // Get field configuration based on fieldType
@@ -96,7 +107,7 @@ const EditProfileFieldScreen: React.FC<EditProfileFieldScreenProps> = ({
96
107
  subtitle: t('editProfile.items.displayName.subtitle') || 'This is how your name will appear to others',
97
108
  fields: [
98
109
  {
99
- key: 'displayName',
110
+ key: 'firstName',
100
111
  label: t('editProfile.items.displayName.firstName') || 'First Name',
101
112
  placeholder: t('editProfile.items.displayName.firstNamePlaceholder') || 'Enter first name',
102
113
  },
@@ -255,30 +266,29 @@ const EditProfileFieldScreen: React.FC<EditProfileFieldScreenProps> = ({
255
266
 
256
267
  if (fieldConfig.isList) {
257
268
  if (fieldType === 'locations') {
258
- const locations = Array.isArray(userData.locations) ? userData.locations as Array<Record<string, unknown>> : [];
269
+ const locations = Array.isArray(userData.locations) ? userData.locations : [];
259
270
  setListItems(locations.map((loc, i) => ({
260
271
  id: String(loc.id || `location-${i}`),
261
272
  name: String(loc.name || ''),
262
273
  ...loc,
263
274
  })));
264
275
  } else if (fieldType === 'links') {
265
- const linksMetadata = Array.isArray(userData.linksMetadata) ? userData.linksMetadata as Array<Record<string, unknown>> : [];
276
+ const linksMetadata = Array.isArray(userData.linksMetadata) ? userData.linksMetadata : [];
266
277
  const links = Array.isArray(userData.links) ? userData.links : [];
267
278
  // Use linksMetadata if available, otherwise convert links array
268
279
  if (linksMetadata.length > 0) {
269
280
  setListItems(linksMetadata.map((link, i) => ({
281
+ ...link,
270
282
  id: String(link.id || `link-${i}`),
271
- url: String(link.url || link.link || ''),
283
+ url: String(link.url || ''),
272
284
  title: String(link.title || ''),
273
- ...link,
274
285
  })));
275
286
  } else {
276
287
  setListItems(links.map((item, i) => {
277
- const url = typeof item === 'string' ? item : (item.link || '');
278
288
  return {
279
289
  id: `link-${i}`,
280
- url,
281
- title: url.replace(/^https?:\/\//, '').replace(/\/$/, ''),
290
+ url: item,
291
+ title: item.replace(/^https?:\/\//, '').replace(/\/$/, ''),
282
292
  };
283
293
  }));
284
294
  }
@@ -286,8 +296,8 @@ const EditProfileFieldScreen: React.FC<EditProfileFieldScreenProps> = ({
286
296
  } else {
287
297
  const initialValues: Record<string, string> = {};
288
298
  fieldConfig.fields.forEach(field => {
289
- if (field.key === 'displayName') {
290
- initialValues[field.key] = String(userData.displayName || userData.name?.first || '');
299
+ if (field.key === 'firstName') {
300
+ initialValues[field.key] = String(userData.name?.first || '');
291
301
  } else if (field.key === 'lastName') {
292
302
  initialValues[field.key] = String(userData.lastName || userData.name?.last || '');
293
303
  } else if (field.key === 'birthday') {
@@ -390,7 +400,7 @@ const EditProfileFieldScreen: React.FC<EditProfileFieldScreenProps> = ({
390
400
  let success = false;
391
401
  if (fieldType === 'displayName') {
392
402
  success = await saveProfile({
393
- displayName: fieldValues.displayName,
403
+ firstName: fieldValues.firstName,
394
404
  lastName: fieldValues.lastName,
395
405
  });
396
406
  } else {
@@ -496,7 +506,7 @@ const EditProfileFieldScreen: React.FC<EditProfileFieldScreenProps> = ({
496
506
  <Text style={[styles.listTitle, { color: bloomTheme.colors.text }]}>
497
507
  {listTitle} ({listItems.length})
498
508
  </Text>
499
- {listItems.map((item: any) => (
509
+ {listItems.map((item) => (
500
510
  <View
501
511
  key={item.id}
502
512
  style={[
@@ -13,7 +13,7 @@ import {
13
13
  } from 'react-native';
14
14
  import type { BaseScreenProps } from '../types/navigation';
15
15
  import { useThemeColors } from '../styles/theme';
16
- import { normalizeTheme } from '../utils/themeUtils';
16
+ import { normalizeTheme } from '@oxyhq/core';
17
17
  import { useTheme } from '@oxyhq/bloom/theme';
18
18
  import { Ionicons } from '@expo/vector-icons';
19
19
  import { toast } from '@oxyhq/bloom';
@@ -13,7 +13,7 @@ import { useI18n } from '../hooks/useI18n';
13
13
  import { useTheme } from '@oxyhq/bloom/theme';
14
14
  import { useColorScheme } from '../hooks/useColorScheme';
15
15
  import { Colors } from '../constants/theme';
16
- import { normalizeColorScheme } from '../utils/themeUtils';
16
+ import { normalizeColorScheme } from '@oxyhq/core';
17
17
  import { SettingsListGroup, SettingsListItem } from '@oxyhq/bloom/settings-list';
18
18
 
19
19
  const HelpSupportScreen: React.FC<BaseScreenProps> = ({