@oxyhq/services 5.13.4 → 5.13.10

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 (170) hide show
  1. package/lib/commonjs/core/HttpClient.js +1 -1
  2. package/lib/commonjs/core/HttpClient.js.map +1 -1
  3. package/lib/commonjs/core/OxyServices.js +83 -30
  4. package/lib/commonjs/core/OxyServices.js.map +1 -1
  5. package/lib/commonjs/core/index.js +0 -7
  6. package/lib/commonjs/core/index.js.map +1 -1
  7. package/lib/commonjs/i18n/locales/en-US.json +222 -6
  8. package/lib/commonjs/index.js +0 -7
  9. package/lib/commonjs/index.js.map +1 -1
  10. package/lib/commonjs/lib/sonner.js.map +1 -1
  11. package/lib/commonjs/ui/components/GroupedItem.js +24 -22
  12. package/lib/commonjs/ui/components/GroupedItem.js.map +1 -1
  13. package/lib/commonjs/ui/components/OxyProvider.js +35 -14
  14. package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
  15. package/lib/commonjs/ui/navigation/routes.js +36 -1
  16. package/lib/commonjs/ui/navigation/routes.js.map +1 -1
  17. package/lib/commonjs/ui/screens/AccountOverviewScreen.js +150 -5
  18. package/lib/commonjs/ui/screens/AccountOverviewScreen.js.map +1 -1
  19. package/lib/commonjs/ui/screens/AccountSettingsScreen.js +475 -319
  20. package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
  21. package/lib/commonjs/ui/screens/AccountVerificationScreen.js +217 -0
  22. package/lib/commonjs/ui/screens/AccountVerificationScreen.js.map +1 -0
  23. package/lib/commonjs/ui/screens/FileManagementScreen.js +911 -213
  24. package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
  25. package/lib/commonjs/ui/screens/HelpSupportScreen.js +131 -0
  26. package/lib/commonjs/ui/screens/HelpSupportScreen.js.map +1 -0
  27. package/lib/commonjs/ui/screens/HistoryViewScreen.js +258 -0
  28. package/lib/commonjs/ui/screens/HistoryViewScreen.js.map +1 -0
  29. package/lib/commonjs/ui/screens/LegalDocumentsScreen.js +211 -0
  30. package/lib/commonjs/ui/screens/LegalDocumentsScreen.js.map +1 -0
  31. package/lib/commonjs/ui/screens/PremiumSubscriptionScreen.js +0 -1
  32. package/lib/commonjs/ui/screens/PremiumSubscriptionScreen.js.map +1 -1
  33. package/lib/commonjs/ui/screens/PrivacySettingsScreen.js +307 -0
  34. package/lib/commonjs/ui/screens/PrivacySettingsScreen.js.map +1 -0
  35. package/lib/commonjs/ui/screens/ProfileScreen.js +1 -7
  36. package/lib/commonjs/ui/screens/ProfileScreen.js.map +1 -1
  37. package/lib/commonjs/ui/screens/SavesCollectionsScreen.js +205 -0
  38. package/lib/commonjs/ui/screens/SavesCollectionsScreen.js.map +1 -0
  39. package/lib/commonjs/ui/screens/SearchSettingsScreen.js +239 -0
  40. package/lib/commonjs/ui/screens/SearchSettingsScreen.js.map +1 -0
  41. package/lib/commonjs/ui/screens/SignInScreen.js +14 -29
  42. package/lib/commonjs/ui/screens/SignInScreen.js.map +1 -1
  43. package/lib/commonjs/utils/asyncUtils.js +1 -0
  44. package/lib/commonjs/utils/asyncUtils.js.map +1 -1
  45. package/lib/commonjs/utils/cache.js +4 -4
  46. package/lib/commonjs/utils/cache.js.map +1 -1
  47. package/lib/commonjs/utils/index.js +0 -6
  48. package/lib/commonjs/utils/index.js.map +1 -1
  49. package/lib/module/core/HttpClient.js +1 -1
  50. package/lib/module/core/HttpClient.js.map +1 -1
  51. package/lib/module/core/OxyServices.js +82 -29
  52. package/lib/module/core/OxyServices.js.map +1 -1
  53. package/lib/module/core/index.js +1 -1
  54. package/lib/module/core/index.js.map +1 -1
  55. package/lib/module/i18n/locales/en-US.json +222 -6
  56. package/lib/module/index.js +1 -1
  57. package/lib/module/index.js.map +1 -1
  58. package/lib/module/lib/sonner.js.map +1 -1
  59. package/lib/module/ui/components/GroupedItem.js +24 -22
  60. package/lib/module/ui/components/GroupedItem.js.map +1 -1
  61. package/lib/module/ui/components/OxyProvider.js +40 -17
  62. package/lib/module/ui/components/OxyProvider.js.map +1 -1
  63. package/lib/module/ui/navigation/routes.js +36 -1
  64. package/lib/module/ui/navigation/routes.js.map +1 -1
  65. package/lib/module/ui/screens/AccountOverviewScreen.js +151 -6
  66. package/lib/module/ui/screens/AccountOverviewScreen.js.map +1 -1
  67. package/lib/module/ui/screens/AccountSettingsScreen.js +475 -319
  68. package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
  69. package/lib/module/ui/screens/AccountVerificationScreen.js +212 -0
  70. package/lib/module/ui/screens/AccountVerificationScreen.js.map +1 -0
  71. package/lib/module/ui/screens/FileManagementScreen.js +913 -212
  72. package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
  73. package/lib/module/ui/screens/HelpSupportScreen.js +126 -0
  74. package/lib/module/ui/screens/HelpSupportScreen.js.map +1 -0
  75. package/lib/module/ui/screens/HistoryViewScreen.js +253 -0
  76. package/lib/module/ui/screens/HistoryViewScreen.js.map +1 -0
  77. package/lib/module/ui/screens/LegalDocumentsScreen.js +206 -0
  78. package/lib/module/ui/screens/LegalDocumentsScreen.js.map +1 -0
  79. package/lib/module/ui/screens/PremiumSubscriptionScreen.js +0 -1
  80. package/lib/module/ui/screens/PremiumSubscriptionScreen.js.map +1 -1
  81. package/lib/module/ui/screens/PrivacySettingsScreen.js +302 -0
  82. package/lib/module/ui/screens/PrivacySettingsScreen.js.map +1 -0
  83. package/lib/module/ui/screens/ProfileScreen.js +1 -7
  84. package/lib/module/ui/screens/ProfileScreen.js.map +1 -1
  85. package/lib/module/ui/screens/SavesCollectionsScreen.js +200 -0
  86. package/lib/module/ui/screens/SavesCollectionsScreen.js.map +1 -0
  87. package/lib/module/ui/screens/SearchSettingsScreen.js +234 -0
  88. package/lib/module/ui/screens/SearchSettingsScreen.js.map +1 -0
  89. package/lib/module/ui/screens/SignInScreen.js +14 -29
  90. package/lib/module/ui/screens/SignInScreen.js.map +1 -1
  91. package/lib/module/utils/asyncUtils.js +1 -0
  92. package/lib/module/utils/asyncUtils.js.map +1 -1
  93. package/lib/module/utils/cache.js +3 -3
  94. package/lib/module/utils/cache.js.map +1 -1
  95. package/lib/module/utils/index.js +1 -1
  96. package/lib/module/utils/index.js.map +1 -1
  97. package/lib/typescript/core/OxyServices.d.ts +30 -24
  98. package/lib/typescript/core/OxyServices.d.ts.map +1 -1
  99. package/lib/typescript/core/index.d.ts +1 -1
  100. package/lib/typescript/core/index.d.ts.map +1 -1
  101. package/lib/typescript/index.d.ts +1 -1
  102. package/lib/typescript/index.d.ts.map +1 -1
  103. package/lib/typescript/lib/sonner.d.ts +1 -0
  104. package/lib/typescript/lib/sonner.d.ts.map +1 -1
  105. package/lib/typescript/types/expo-document-picker.d.ts +36 -0
  106. package/lib/typescript/ui/components/GroupedItem.d.ts.map +1 -1
  107. package/lib/typescript/ui/components/OxyProvider.d.ts.map +1 -1
  108. package/lib/typescript/ui/navigation/routes.d.ts +1 -1
  109. package/lib/typescript/ui/navigation/routes.d.ts.map +1 -1
  110. package/lib/typescript/ui/screens/AccountOverviewScreen.d.ts.map +1 -1
  111. package/lib/typescript/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
  112. package/lib/typescript/ui/screens/AccountVerificationScreen.d.ts +5 -0
  113. package/lib/typescript/ui/screens/AccountVerificationScreen.d.ts.map +1 -0
  114. package/lib/typescript/ui/screens/FileManagementScreen.d.ts.map +1 -1
  115. package/lib/typescript/ui/screens/HelpSupportScreen.d.ts +5 -0
  116. package/lib/typescript/ui/screens/HelpSupportScreen.d.ts.map +1 -0
  117. package/lib/typescript/ui/screens/HistoryViewScreen.d.ts +5 -0
  118. package/lib/typescript/ui/screens/HistoryViewScreen.d.ts.map +1 -0
  119. package/lib/typescript/ui/screens/LegalDocumentsScreen.d.ts +5 -0
  120. package/lib/typescript/ui/screens/LegalDocumentsScreen.d.ts.map +1 -0
  121. package/lib/typescript/ui/screens/PremiumSubscriptionScreen.d.ts.map +1 -1
  122. package/lib/typescript/ui/screens/PrivacySettingsScreen.d.ts +5 -0
  123. package/lib/typescript/ui/screens/PrivacySettingsScreen.d.ts.map +1 -0
  124. package/lib/typescript/ui/screens/ProfileScreen.d.ts.map +1 -1
  125. package/lib/typescript/ui/screens/SavesCollectionsScreen.d.ts +5 -0
  126. package/lib/typescript/ui/screens/SavesCollectionsScreen.d.ts.map +1 -0
  127. package/lib/typescript/ui/screens/SearchSettingsScreen.d.ts +5 -0
  128. package/lib/typescript/ui/screens/SearchSettingsScreen.d.ts.map +1 -0
  129. package/lib/typescript/ui/screens/SignInScreen.d.ts.map +1 -1
  130. package/lib/typescript/utils/asyncUtils.d.ts.map +1 -1
  131. package/lib/typescript/utils/cache.d.ts +3 -3
  132. package/lib/typescript/utils/cache.d.ts.map +1 -1
  133. package/lib/typescript/utils/index.d.ts +1 -1
  134. package/lib/typescript/utils/index.d.ts.map +1 -1
  135. package/package.json +1 -1
  136. package/src/core/HttpClient.ts +1 -1
  137. package/src/core/OxyServices.ts +80 -30
  138. package/src/core/index.ts +1 -1
  139. package/src/i18n/locales/en-US.json +222 -6
  140. package/src/index.ts +1 -1
  141. package/src/lib/sonner.ts +1 -0
  142. package/src/types/expo-document-picker.d.ts +36 -0
  143. package/src/ui/components/GroupedItem.tsx +23 -21
  144. package/src/ui/components/OxyProvider.tsx +33 -11
  145. package/src/ui/navigation/routes.ts +42 -0
  146. package/src/ui/screens/AccountOverviewScreen.tsx +175 -5
  147. package/src/ui/screens/AccountSettingsScreen.tsx +521 -360
  148. package/src/ui/screens/AccountVerificationScreen.tsx +235 -0
  149. package/src/ui/screens/FileManagementScreen.tsx +934 -208
  150. package/src/ui/screens/HelpSupportScreen.tsx +143 -0
  151. package/src/ui/screens/HistoryViewScreen.tsx +280 -0
  152. package/src/ui/screens/LegalDocumentsScreen.tsx +220 -0
  153. package/src/ui/screens/PremiumSubscriptionScreen.tsx +0 -1
  154. package/src/ui/screens/PrivacySettingsScreen.tsx +332 -0
  155. package/src/ui/screens/ProfileScreen.tsx +1 -8
  156. package/src/ui/screens/SavesCollectionsScreen.tsx +222 -0
  157. package/src/ui/screens/SearchSettingsScreen.tsx +219 -0
  158. package/src/ui/screens/SignInScreen.tsx +19 -35
  159. package/src/utils/asyncUtils.ts +1 -0
  160. package/src/utils/cache.ts +3 -3
  161. package/src/utils/index.ts +1 -1
  162. package/lib/commonjs/ui/components/StepBasedScreen.README.md +0 -337
  163. package/lib/commonjs/ui/components/internal/TextField.md +0 -436
  164. package/lib/commonjs/ui/styles/FONTS.md +0 -126
  165. package/lib/module/ui/components/StepBasedScreen.README.md +0 -337
  166. package/lib/module/ui/components/internal/TextField.md +0 -436
  167. package/lib/module/ui/styles/FONTS.md +0 -126
  168. package/src/ui/components/StepBasedScreen.README.md +0 -337
  169. package/src/ui/components/internal/TextField.md +0 -436
  170. package/src/ui/styles/FONTS.md +0 -126
@@ -39,11 +39,18 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
39
39
  goBack,
40
40
  navigate,
41
41
  }) => {
42
- const { user, oxyServices, isLoading: authLoading, isAuthenticated, showBottomSheet, activeSessionId } = useOxy();
42
+ const { user: userFromContext, oxyServices, isLoading: authLoading, isAuthenticated, showBottomSheet, activeSessionId } = useOxy();
43
43
  const { t } = useI18n();
44
44
  const updateUser = useAuthStore((state) => state.updateUser);
45
+ // Get user directly from store to ensure reactivity to avatar changes
46
+ const user = useAuthStore((state) => state.user) || userFromContext;
45
47
  const [isLoading, setIsLoading] = useState(false);
46
48
  const [isSaving, setIsSaving] = useState(false);
49
+ const [isUpdatingAvatar, setIsUpdatingAvatar] = useState(false);
50
+ const [optimisticAvatarId, setOptimisticAvatarId] = useState<string | null>(null);
51
+ const scrollViewRef = useRef<ScrollView>(null);
52
+ const avatarSectionRef = useRef<View>(null);
53
+ const [avatarSectionY, setAvatarSectionY] = useState<number | null>(null);
47
54
 
48
55
  // Two-Factor (TOTP) state
49
56
  const [totpSetupUrl, setTotpSetupUrl] = useState<string | null>(null);
@@ -69,7 +76,6 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
69
76
  // Editing states
70
77
  const [editingField, setEditingField] = useState<string | null>(null);
71
78
 
72
- // Temporary input states for inline editing
73
79
  const [tempDisplayName, setTempDisplayName] = useState('');
74
80
  const [tempLastName, setTempLastName] = useState('');
75
81
  const [tempUsername, setTempUsername] = useState('');
@@ -117,84 +123,123 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
117
123
  }, [theme]);
118
124
 
119
125
  // Memoize animation function to prevent recreation on every render
120
- const animateSaveButton = useCallback((toValue: number) => {
126
+ const animateSaveButton = useCallback((toValue: number, onComplete?: () => void) => {
121
127
  Animated.spring(saveButtonScale, {
122
128
  toValue,
123
129
  useNativeDriver: Platform.OS !== 'web',
124
130
  tension: 150,
125
131
  friction: 8,
126
- }).start();
132
+ }).start(onComplete ? (finished) => {
133
+ if (finished) {
134
+ onComplete();
135
+ }
136
+ } : undefined);
127
137
  }, [saveButtonScale]);
128
138
 
129
- // Load user data
139
+ // Track initialization to prevent unnecessary resets
140
+ const isInitializedRef = useRef(false);
141
+ const previousUserIdRef = useRef<string | null>(null);
142
+ const previousAvatarRef = useRef<string | null>(null);
143
+
144
+ // Load user data - only reset fields when user actually changes (not just avatar)
130
145
  useEffect(() => {
131
146
  if (user) {
132
- const userDisplayName = typeof user.name === 'string'
133
- ? user.name
134
- : user.name?.first || user.name?.full || '';
135
- const userLastName = typeof user.name === 'object' ? user.name?.last || '' : '';
136
- setDisplayName(userDisplayName);
137
- setLastName(userLastName);
138
- setUsername(user.username || '');
139
- setEmail(user.email || '');
140
- setBio(user.bio || '');
141
- setLocation(user.location || '');
142
-
143
- // Handle locations - convert single location to array format
144
- if (user.locations && Array.isArray(user.locations)) {
145
- setTempLocations(user.locations.map((loc, index) => ({
146
- id: loc.id || `existing-${index}`,
147
- name: loc.name,
148
- label: loc.label,
149
- coordinates: loc.coordinates
150
- })));
151
- } else if (user.location) {
152
- // Convert single location string to array format
153
- setTempLocations([{
154
- id: 'existing-0',
155
- name: user.location,
156
- label: 'Location'
157
- }]);
158
- } else {
159
- setTempLocations([]);
147
+ const currentUserId = user.id;
148
+ const currentAvatar = typeof user.avatar === 'string' ? user.avatar : '';
149
+ const isNewUser = previousUserIdRef.current !== currentUserId;
150
+ const isAvatarOnlyUpdate = !isNewUser && previousUserIdRef.current === currentUserId &&
151
+ previousAvatarRef.current !== currentAvatar &&
152
+ previousAvatarRef.current !== null;
153
+ const shouldInitialize = !isInitializedRef.current || isNewUser;
154
+
155
+ // Only reset all fields if it's a new user or first load
156
+ // Skip reset if it's just an avatar update
157
+ if (shouldInitialize && !isAvatarOnlyUpdate) {
158
+ const userDisplayName = typeof user.name === 'string'
159
+ ? user.name
160
+ : user.name?.first || user.name?.full || '';
161
+ const userLastName = typeof user.name === 'object' ? user.name?.last || '' : '';
162
+ setDisplayName(userDisplayName);
163
+ setLastName(userLastName);
164
+ setUsername(user.username || '');
165
+ setEmail(user.email || '');
166
+ setBio(user.bio || '');
167
+ setLocation(user.location || '');
168
+
169
+ // Handle locations - convert single location to array format
170
+ if (user.locations && Array.isArray(user.locations)) {
171
+ setTempLocations(user.locations.map((loc, index) => ({
172
+ id: loc.id || `existing-${index}`,
173
+ name: loc.name,
174
+ label: loc.label,
175
+ coordinates: loc.coordinates
176
+ })));
177
+ } else if (user.location) {
178
+ // Convert single location string to array format
179
+ setTempLocations([{
180
+ id: 'existing-0',
181
+ name: user.location,
182
+ label: 'Location'
183
+ }]);
184
+ } else {
185
+ setTempLocations([]);
186
+ }
187
+
188
+ // Handle links - simple and direct like other fields
189
+ if (user.linksMetadata && Array.isArray(user.linksMetadata)) {
190
+ const urls = user.linksMetadata.map(l => l.url);
191
+ setLinks(urls);
192
+ const metadataWithIds = user.linksMetadata.map((link, index) => ({
193
+ ...link,
194
+ id: link.id || `existing-${index}`
195
+ }));
196
+ setTempLinksWithMetadata(metadataWithIds);
197
+ } else if (Array.isArray(user.links)) {
198
+ const simpleLinks = user.links.map(l => typeof l === 'string' ? l : l.link).filter(Boolean);
199
+ setLinks(simpleLinks);
200
+ const linksWithMetadata = simpleLinks.map((url, index) => ({
201
+ url,
202
+ title: url.replace(/^https?:\/\//, '').replace(/\/$/, ''),
203
+ description: `Link to ${url}`,
204
+ image: undefined,
205
+ id: `existing-${index}`
206
+ }));
207
+ setTempLinksWithMetadata(linksWithMetadata);
208
+ } else if (user.website) {
209
+ setLinks([user.website]);
210
+ setTempLinksWithMetadata([{
211
+ url: user.website,
212
+ title: user.website.replace(/^https?:\/\//, '').replace(/\/$/, ''),
213
+ description: `Link to ${user.website}`,
214
+ image: undefined,
215
+ id: 'existing-0'
216
+ }]);
217
+ } else {
218
+ setLinks([]);
219
+ setTempLinksWithMetadata([]);
220
+ }
221
+ isInitializedRef.current = true;
160
222
  }
161
223
 
162
- // Handle links - simple and direct like other fields
163
- if (user.linksMetadata && Array.isArray(user.linksMetadata)) {
164
- const urls = user.linksMetadata.map(l => l.url);
165
- setLinks(urls);
166
- const metadataWithIds = user.linksMetadata.map((link, index) => ({
167
- ...link,
168
- id: link.id || `existing-${index}`
169
- }));
170
- setTempLinksWithMetadata(metadataWithIds);
171
- } else if (Array.isArray(user.links)) {
172
- const simpleLinks = user.links.map(l => typeof l === 'string' ? l : l.link).filter(Boolean);
173
- setLinks(simpleLinks);
174
- const linksWithMetadata = simpleLinks.map((url, index) => ({
175
- url,
176
- title: url.replace(/^https?:\/\//, '').replace(/\/$/, ''),
177
- description: `Link to ${url}`,
178
- image: undefined,
179
- id: `existing-${index}`
180
- }));
181
- setTempLinksWithMetadata(linksWithMetadata);
182
- } else if (user.website) {
183
- setLinks([user.website]);
184
- setTempLinksWithMetadata([{
185
- url: user.website,
186
- title: user.website.replace(/^https?:\/\//, '').replace(/\/$/, ''),
187
- description: `Link to ${user.website}`,
188
- image: undefined,
189
- id: 'existing-0'
190
- }]);
191
- } else {
192
- setLinks([]);
193
- setTempLinksWithMetadata([]);
224
+ // Update avatar only if it changed and we're not in optimistic/updating state
225
+ // This allows the server response to update the avatar without resetting other fields
226
+ // But don't override if we have a pending optimistic update
227
+ if (currentAvatar !== avatarFileId && !isUpdatingAvatar && !optimisticAvatarId) {
228
+ setAvatarFileId(currentAvatar);
194
229
  }
195
- setAvatarFileId(typeof user.avatar === 'string' ? user.avatar : '');
230
+
231
+ // If we just finished updating and the server avatar matches our optimistic one, clear optimistic state
232
+ // Also clear if the server avatar matches our current avatarFileId (update completed)
233
+ if (isUpdatingAvatar === false && optimisticAvatarId) {
234
+ if (currentAvatar === optimisticAvatarId || currentAvatar === avatarFileId) {
235
+ setOptimisticAvatarId(null);
236
+ }
237
+ }
238
+
239
+ previousUserIdRef.current = currentUserId;
240
+ previousAvatarRef.current = currentAvatar;
196
241
  }
197
- }, [user]);
242
+ }, [user, avatarFileId, isUpdatingAvatar, optimisticAvatarId]);
198
243
 
199
244
  const handleSave = async () => {
200
245
  if (!user) return;
@@ -213,9 +258,6 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
213
258
  linksMetadata: tempLinksWithMetadata.length > 0 ? tempLinksWithMetadata : undefined,
214
259
  };
215
260
 
216
- console.log('Saving updates:', updates);
217
- console.log('Links metadata being saved:', tempLinksWithMetadata);
218
-
219
261
  // Handle name field
220
262
  if (displayName || lastName) {
221
263
  updates.name = { first: displayName, last: lastName };
@@ -268,32 +310,68 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
268
310
  toast.info?.(t('editProfile.toasts.avatarUnchanged') || 'Avatar unchanged');
269
311
  return;
270
312
  }
313
+
314
+ // Optimistically update UI immediately
315
+ setOptimisticAvatarId(file.id);
271
316
  setAvatarFileId(file.id);
272
- toast.success(t('editProfile.toasts.avatarSelected') || 'Avatar selected');
317
+
273
318
  // Auto-save avatar immediately (does not close edit profile screen)
274
319
  (async () => {
275
320
  try {
276
- console.log('[AccountSettings] Auto-saving avatar', file.id);
277
- setIsSaving(true);
321
+ setIsUpdatingAvatar(true);
278
322
 
279
323
  // Update file visibility to public for avatar
280
324
  try {
281
325
  await oxyServices.assetUpdateVisibility(file.id, 'public');
282
- console.log('[AccountSettings] Avatar visibility updated to public');
283
326
  } catch (visError) {
284
- console.warn('[AccountSettings] Failed to update avatar visibility, continuing anyway:', visError);
285
327
  // Continue with avatar update even if visibility update fails
286
328
  }
287
329
 
288
- await updateUser({ avatar: file.id }, oxyServices);
289
- // Force refresh current user cache (updateUser already does a fetch with force=true internally)
290
- // Extra safeguard: ensure avatarFileId reflects saved id (already set) and trigger any dependent UI.
330
+ // Update on server directly without using updateUser (which triggers fetchUser)
331
+ // This prevents the entire component from re-rendering
332
+ await oxyServices.updateProfile({ avatar: file.id });
333
+
334
+ // Update the user object in store directly without triggering fetchUser
335
+ // This prevents isLoading from being set to true, which would show loading screen
336
+ const currentUser = useAuthStore.getState().user;
337
+ if (currentUser) {
338
+ useAuthStore.setState({
339
+ user: { ...currentUser, avatar: file.id },
340
+ // Don't update lastUserFetch to avoid cache issues
341
+ });
342
+ }
343
+
344
+ // Update local state - keep avatarFileId set to the new value
345
+ // Don't clear optimisticAvatarId yet - let it persist until user object updates
346
+ // This ensures the avatar displays correctly
347
+ setAvatarFileId(file.id);
348
+
291
349
  toast.success(t('editProfile.toasts.avatarUpdated') || 'Avatar updated');
350
+
351
+ // Scroll to avatar section after a brief delay to ensure UI is updated
352
+ requestAnimationFrame(() => {
353
+ requestAnimationFrame(() => {
354
+ if (avatarSectionY !== null) {
355
+ scrollViewRef.current?.scrollTo({
356
+ y: Math.max(0, avatarSectionY - 100), // Offset to show section near top
357
+ animated: true,
358
+ });
359
+ } else {
360
+ // Fallback: scroll to approximate position
361
+ scrollViewRef.current?.scrollTo({
362
+ y: 200, // Approximate position of avatar section
363
+ animated: true,
364
+ });
365
+ }
366
+ });
367
+ });
292
368
  } catch (e: any) {
293
- console.error('[AccountSettings] Failed to auto-save avatar', e);
369
+ // Revert optimistic update on error
370
+ setAvatarFileId(typeof user?.avatar === 'string' ? user.avatar : '');
371
+ setOptimisticAvatarId(null);
294
372
  toast.error(e.message || t('editProfile.toasts.updateAvatarFailed') || 'Failed to update avatar');
295
373
  } finally {
296
- setIsSaving(false);
374
+ setIsUpdatingAvatar(false);
297
375
  }
298
376
  })();
299
377
  },
@@ -301,7 +379,7 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
301
379
  disabledMimeTypes: ['video/', 'audio/', 'application/pdf']
302
380
  }
303
381
  });
304
- }, [showBottomSheet, oxyServices, avatarFileId, updateUser]);
382
+ }, [showBottomSheet, oxyServices, avatarFileId, updateUser, user]);
305
383
 
306
384
  const startEditing = (type: string, currentValue: string) => {
307
385
  switch (type) {
@@ -362,11 +440,10 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
362
440
  break;
363
441
  }
364
442
 
365
- // Brief delay for animation, then reset and close editing
366
- setTimeout(() => {
367
- animateSaveButton(1);
443
+ // Complete animation, then reset and close editing
444
+ animateSaveButton(1, () => {
368
445
  setEditingField(null);
369
- }, 150);
446
+ });
370
447
  };
371
448
 
372
449
  const cancelEditing = () => {
@@ -385,11 +462,9 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
385
462
 
386
463
  try {
387
464
  setIsFetchingMetadata(true);
388
- console.log('Fetching metadata for URL:', url);
389
465
 
390
466
  // Use the backend API to fetch metadata
391
467
  const metadata = await oxyServices.fetchLinkMetadata(url);
392
- console.log('Received metadata:', metadata);
393
468
 
394
469
  const result = {
395
470
  ...metadata,
@@ -400,7 +475,6 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
400
475
  linkMetadataCache.set(cacheKey, result);
401
476
  return result;
402
477
  } catch (error) {
403
- console.error('Error fetching metadata:', error);
404
478
  // Fallback to basic metadata
405
479
  const fallback = {
406
480
  url: url.startsWith('http') ? url : 'https://' + url,
@@ -437,12 +511,11 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
437
511
  `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}&limit=5&addressdetails=1`
438
512
  );
439
513
  const data = await response.json();
440
-
514
+
441
515
  // Cache the results
442
516
  locationSearchCache.set(cacheKey, data);
443
517
  setLocationSearchResults(data);
444
518
  } catch (error) {
445
- console.error('Error searching locations:', error);
446
519
  setLocationSearchResults([]);
447
520
  } finally {
448
521
  setIsSearchingLocations(false);
@@ -491,10 +564,7 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
491
564
  if (!newLinkUrl.trim()) return;
492
565
 
493
566
  const url = newLinkUrl.trim();
494
- console.log('Adding link:', url);
495
-
496
567
  const metadata = await fetchLinkMetadata(url);
497
- console.log('Final metadata for adding:', metadata);
498
568
 
499
569
  setTempLinksWithMetadata(prev => [...prev, metadata]);
500
570
  setNewLinkUrl('');
@@ -552,7 +622,7 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
552
622
  </TouchableOpacity>
553
623
  ) : (
554
624
  <View style={{ alignItems: 'center', gap: 16 }}>
555
- <View style={{ padding: 16, backgroundColor: '#fff', borderRadius: 12 }}>
625
+ <View style={{ padding: 16, backgroundColor: '#fff', borderRadius: 16 }}>
556
626
  <QRCode value={totpSetupUrl} size={180} />
557
627
  </View>
558
628
  <View>
@@ -646,7 +716,7 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
646
716
  <View style={styles.editingFieldContent}>
647
717
  <View style={styles.newValueSection}>
648
718
  <View style={styles.editingFieldHeader}>
649
- <Text style={[styles.editingFieldLabel, { color: themeStyles.isDarkTheme ? '#FFFFFF' : '#1A1A1A' }]}>Edit Display Name</Text>
719
+ <Text style={[styles.editingFieldLabel, { color: themeStyles.isDarkTheme ? '#FFFFFF' : '#1A1A1A' }]}>Edit Full Name</Text>
650
720
  </View>
651
721
  <View style={{ flexDirection: 'row', gap: 12 }}>
652
722
  <View style={{ flex: 1 }}>
@@ -972,27 +1042,27 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
972
1042
  };
973
1043
 
974
1044
  return (
975
- <View style={[styles.editingFieldContainer, { backgroundColor: themeStyles.backgroundColor }]}>
1045
+ <View style={[styles.editingFieldContainer, { backgroundColor: themeStyles.isDarkTheme ? '#000000' : '#FFFFFF' }]}>
976
1046
  <View style={styles.editingFieldContent}>
977
1047
  <View style={styles.newValueSection}>
978
1048
  <View style={styles.editingFieldHeader}>
979
- <Text style={[styles.editingFieldLabel, { color: themeStyles.isDarkTheme ? '#FFFFFF' : '#1A1A1A' }]}>
980
- {`Enter ${config.label.toLowerCase()}:`}
1049
+ <Text style={[styles.editingFieldLabel, { color: themeStyles.isDarkTheme ? '#FFFFFF' : '#000000' }]}>
1050
+ {config.label}
981
1051
  </Text>
982
1052
  </View>
983
1053
  <TextInput
984
1054
  style={[
985
1055
  config.multiline ? styles.editingFieldTextArea : styles.editingFieldInput,
986
1056
  {
987
- backgroundColor: themeStyles.isDarkTheme ? '#333' : '#fff',
988
- color: themeStyles.isDarkTheme ? '#fff' : '#000',
989
- borderColor: themeStyles.primaryColor
1057
+ backgroundColor: themeStyles.isDarkTheme ? '#1C1C1E' : '#F2F2F7',
1058
+ color: themeStyles.isDarkTheme ? '#FFFFFF' : '#000000',
1059
+ borderColor: themeStyles.isDarkTheme ? '#38383A' : '#E5E5EA',
990
1060
  }
991
1061
  ]}
992
1062
  value={tempValue}
993
1063
  onChangeText={setTempValue}
994
1064
  placeholder={config.placeholder}
995
- placeholderTextColor={themeStyles.isDarkTheme ? '#aaa' : '#999'}
1065
+ placeholderTextColor={themeStyles.isDarkTheme ? '#636366' : '#8E8E93'}
996
1066
  multiline={config.multiline}
997
1067
  numberOfLines={config.multiline ? 6 : 1}
998
1068
  keyboardType={config.keyboardType}
@@ -1009,25 +1079,42 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
1009
1079
 
1010
1080
  if (authLoading || !isAuthenticated) {
1011
1081
  return (
1012
- <View style={[styles.container, { backgroundColor: themeStyles.backgroundColor, justifyContent: 'center' }]}>
1082
+ <View style={[styles.container, {
1083
+ backgroundColor: themeStyles.isDarkTheme ? '#000000' : '#F5F5F7',
1084
+ justifyContent: 'center'
1085
+ }]}>
1013
1086
  <ActivityIndicator size="large" color={themeStyles.primaryColor} />
1014
1087
  </View>
1015
1088
  );
1016
1089
  }
1017
1090
 
1018
1091
  return (
1019
- <View style={[styles.container, { backgroundColor: themeStyles.backgroundColor }]}>
1092
+ <View style={[styles.container, { backgroundColor: themeStyles.isDarkTheme ? '#000000' : '#F5F5F7' }]}>
1020
1093
  {/* Header */}
1021
1094
  {editingField ? (
1022
- <View style={[styles.editingHeader, { backgroundColor: '#FFFFFF', borderBottomColor: themeStyles.isDarkTheme ? '#38383A' : '#E9ECEF' }]}>
1095
+ <View style={[styles.editingHeader, {
1096
+ backgroundColor: themeStyles.isDarkTheme ? '#000000' : '#FFFFFF',
1097
+ borderBottomColor: themeStyles.isDarkTheme ? '#38383A' : '#E5E5EA'
1098
+ }]}>
1023
1099
  <View style={styles.editingHeaderContent}>
1024
- <TouchableOpacity style={styles.editingBackButton} onPress={cancelEditing}>
1025
- <OxyIcon name="chevron-back" size={20} color={themeStyles.primaryColor} />
1100
+ <TouchableOpacity
1101
+ style={[styles.editingBackButton, {
1102
+ backgroundColor: themeStyles.isDarkTheme ? '#1C1C1E' : '#F2F2F7'
1103
+ }]}
1104
+ onPress={cancelEditing}
1105
+ >
1106
+ <Ionicons name="chevron-back" size={20} color={themeStyles.primaryColor} />
1026
1107
  </TouchableOpacity>
1027
1108
  <View style={styles.editingTitleContainer}>
1028
1109
  </View>
1029
1110
  <TouchableOpacity
1030
- style={[styles.editingSaveButton, { opacity: isSaving ? 0.5 : 1 }]}
1111
+ style={[
1112
+ styles.editingSaveButton,
1113
+ {
1114
+ opacity: isSaving ? 0.5 : 1,
1115
+ backgroundColor: themeStyles.isDarkTheme ? '#1C1C1E' : '#F2F2F7'
1116
+ }
1117
+ ]}
1031
1118
  onPress={() => saveField(editingField)}
1032
1119
  disabled={isSaving}
1033
1120
  >
@@ -1039,27 +1126,35 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
1039
1126
  </TouchableOpacity>
1040
1127
  </View>
1041
1128
  <View style={styles.editingHeaderBottom}>
1042
- <OxyIcon
1043
- name={
1044
- editingField === 'displayName' ? 'person' :
1045
- editingField === 'username' ? 'at' :
1046
- editingField === 'email' ? 'mail' :
1047
- editingField === 'bio' ? 'document-text' :
1048
- editingField === 'location' ? 'location' :
1049
- editingField === 'links' ? 'link' : 'person'
1050
- }
1051
- size={56}
1052
- color={
1053
- editingField === 'displayName' ? '#007AFF' :
1054
- editingField === 'username' ? '#5856D6' :
1055
- editingField === 'email' ? '#FF9500' :
1056
- editingField === 'bio' ? '#34C759' :
1057
- editingField === 'location' ? '#FF3B30' :
1058
- editingField === 'links' ? '#32D74B' : '#007AFF'
1059
- }
1060
- style={styles.editingBottomIcon}
1061
- />
1062
- <Text style={[styles.editingBottomTitle, { color: themeStyles.isDarkTheme ? '#FFFFFF' : '#1A1A1A' }]}>
1129
+ <View style={[styles.editingIconContainer, {
1130
+ backgroundColor: editingField === 'displayName' ? '#007AFF20' :
1131
+ editingField === 'username' ? '#5856D620' :
1132
+ editingField === 'email' ? '#FF950020' :
1133
+ editingField === 'bio' ? '#34C75920' :
1134
+ editingField === 'location' ? '#FF3B3020' :
1135
+ editingField === 'links' ? '#32D74B20' : '#007AFF20'
1136
+ }]}>
1137
+ <Ionicons
1138
+ name={
1139
+ editingField === 'displayName' ? 'person' as any :
1140
+ editingField === 'username' ? 'at' as any :
1141
+ editingField === 'email' ? 'mail' as any :
1142
+ editingField === 'bio' ? 'document-text' as any :
1143
+ editingField === 'location' ? 'location' as any :
1144
+ editingField === 'links' ? 'link' as any : 'person' as any
1145
+ }
1146
+ size={28}
1147
+ color={
1148
+ editingField === 'displayName' ? '#007AFF' :
1149
+ editingField === 'username' ? '#5856D6' :
1150
+ editingField === 'email' ? '#FF9500' :
1151
+ editingField === 'bio' ? '#34C759' :
1152
+ editingField === 'location' ? '#FF3B30' :
1153
+ editingField === 'links' ? '#32D74B' : '#007AFF'
1154
+ }
1155
+ />
1156
+ </View>
1157
+ <Text style={[styles.editingBottomTitle, { color: themeStyles.isDarkTheme ? '#FFFFFF' : '#000000' }]}>
1063
1158
  {editingField === 'displayName' ? (t('editProfile.items.displayName.title') || 'Display Name') :
1064
1159
  editingField === 'username' ? (t('editProfile.items.username.title') || 'Username') :
1065
1160
  editingField === 'email' ? (t('editProfile.items.email.title') || 'Email') :
@@ -1084,7 +1179,10 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
1084
1179
  />
1085
1180
  )}
1086
1181
 
1087
- <ScrollView style={editingField ? styles.contentEditing : styles.content}>
1182
+ <ScrollView
1183
+ ref={scrollViewRef}
1184
+ style={editingField ? styles.contentEditing : styles.content}
1185
+ >
1088
1186
  {editingField ? (
1089
1187
  // Show only the editing interface when editing
1090
1188
  <View style={styles.editingOnlyContainer}>
@@ -1095,7 +1193,7 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
1095
1193
  <>
1096
1194
  {showRecoveryModal && (
1097
1195
  <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' }}>
1098
- <View style={{ backgroundColor: '#fff', borderRadius: 16, padding: 20, maxHeight: '80%' }}>
1196
+ <View style={{ backgroundColor: '#fff', borderRadius: 20, padding: 20, maxHeight: '80%' }}>
1099
1197
  <Text style={{ fontSize: 18, fontWeight: '700', marginBottom: 12 }}>Save These Codes Now</Text>
1100
1198
  <Text style={{ fontSize: 14, color: '#444', marginBottom: 12 }}>
1101
1199
  Backup codes and your Recovery Key are shown only once. Store them securely (paper or password manager).
@@ -1103,7 +1201,7 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
1103
1201
  {generatedBackupCodes && generatedBackupCodes.length > 0 && (
1104
1202
  <View style={{ marginBottom: 12 }}>
1105
1203
  <Text style={{ fontSize: 16, fontWeight: '600', marginBottom: 8 }}>Backup Codes</Text>
1106
- <View style={{ backgroundColor: '#F8F9FA', borderRadius: 8, padding: 12 }}>
1204
+ <View style={{ backgroundColor: '#F8F9FA', borderRadius: 12, padding: 12 }}>
1107
1205
  {generatedBackupCodes.map((c, idx) => (
1108
1206
  <Text key={idx} style={{ fontFamily: Platform.OS === 'web' ? 'monospace' as any : 'monospace', fontSize: 14, marginBottom: 4 }}>{c}</Text>
1109
1207
  ))}
@@ -1113,7 +1211,7 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
1113
1211
  {generatedRecoveryKey && (
1114
1212
  <View style={{ marginBottom: 12 }}>
1115
1213
  <Text style={{ fontSize: 16, fontWeight: '600', marginBottom: 8 }}>Recovery Key</Text>
1116
- <View style={{ backgroundColor: '#F8F9FA', borderRadius: 8, padding: 12 }}>
1214
+ <View style={{ backgroundColor: '#F8F9FA', borderRadius: 12, padding: 12 }}>
1117
1215
  <Text style={{ fontFamily: Platform.OS === 'web' ? 'monospace' as any : 'monospace', fontSize: 14 }}>{generatedRecoveryKey}</Text>
1118
1216
  </View>
1119
1217
  </View>
@@ -1129,221 +1227,276 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
1129
1227
  </View>
1130
1228
  )}
1131
1229
  {/* Profile Picture Section */}
1132
- <View style={styles.section}>
1133
- <Text style={styles.sectionTitle}>{t('editProfile.sections.profilePicture') || 'Profile Picture'}</Text>
1134
- <GroupedSection
1135
- items={[
1136
- {
1137
- id: 'profile-photo',
1138
- icon: avatarFileId ? undefined : 'person',
1139
- iconColor: '#007AFF',
1140
- // Use download URL (includes token + fallback) instead of raw stream for reliability
1141
- image: avatarFileId ? oxyServices.getFileDownloadUrl(avatarFileId, 'thumb') : undefined,
1142
- imageSize: 40,
1143
- title: 'Profile Photo',
1144
- subtitle: avatarFileId ? 'Tap to change your profile picture' : 'Tap to add a profile picture',
1145
- onPress: openAvatarPicker,
1146
- },
1147
- ...(avatarFileId ? [
1230
+ <View
1231
+ ref={avatarSectionRef}
1232
+ style={styles.section}
1233
+ onLayout={(event) => {
1234
+ const { y } = event.nativeEvent.layout;
1235
+ setAvatarSectionY(y);
1236
+ }}
1237
+ >
1238
+ <Text style={[styles.sectionTitle, { color: themeStyles.isDarkTheme ? '#8E8E93' : '#8E8E93' }]}>
1239
+ {t('editProfile.sections.profilePicture') || 'PROFILE PICTURE'}
1240
+ </Text>
1241
+ <View style={styles.groupedSectionWrapper}>
1242
+ <GroupedSection
1243
+ items={[
1148
1244
  {
1149
- id: 'remove-profile-photo',
1150
- icon: 'trash',
1151
- iconColor: '#FF3B30',
1152
- title: 'Remove Photo',
1153
- subtitle: 'Delete current profile picture',
1154
- onPress: handleAvatarRemove,
1155
- }
1156
- ] : []),
1157
- ]}
1158
- theme={theme}
1159
- />
1245
+ id: 'profile-photo',
1246
+ icon: avatarFileId ? undefined : 'person',
1247
+ iconColor: '#007AFF',
1248
+ // Use optimistic avatar ID if available, otherwise use saved one
1249
+ image: (optimisticAvatarId || avatarFileId) ? oxyServices.getFileDownloadUrl(optimisticAvatarId || avatarFileId, 'thumb') : undefined,
1250
+ imageSize: 40,
1251
+ title: 'Profile Photo',
1252
+ subtitle: isUpdatingAvatar
1253
+ ? 'Updating profile picture...'
1254
+ : (avatarFileId ? 'Tap to change your profile picture' : 'Tap to add a profile picture'),
1255
+ onPress: isUpdatingAvatar ? undefined : openAvatarPicker,
1256
+ disabled: isUpdatingAvatar,
1257
+ customIcon: isUpdatingAvatar ? (
1258
+ <Animated.View style={{ position: 'relative', width: 40, height: 40 }}>
1259
+ {(optimisticAvatarId || avatarFileId) && (
1260
+ <Animated.Image
1261
+ source={{ uri: oxyServices.getFileDownloadUrl(optimisticAvatarId || avatarFileId, 'thumb') }}
1262
+ style={{
1263
+ width: 40,
1264
+ height: 40,
1265
+ borderRadius: 22,
1266
+ opacity: 0.6
1267
+ }}
1268
+ />
1269
+ )}
1270
+ <View style={{
1271
+ position: 'absolute',
1272
+ top: 0,
1273
+ left: 0,
1274
+ right: 0,
1275
+ bottom: 0,
1276
+ justifyContent: 'center',
1277
+ alignItems: 'center',
1278
+ backgroundColor: themeStyles.isDarkTheme ? 'rgba(0, 0, 0, 0.4)' : 'rgba(255, 255, 255, 0.7)',
1279
+ borderRadius: 22,
1280
+ }}>
1281
+ <ActivityIndicator size="small" color={themeStyles.primaryColor} />
1282
+ </View>
1283
+ </Animated.View>
1284
+ ) : undefined,
1285
+ },
1286
+ ...(avatarFileId && !isUpdatingAvatar ? [
1287
+ {
1288
+ id: 'remove-profile-photo',
1289
+ icon: 'trash',
1290
+ iconColor: '#FF3B30',
1291
+ title: 'Remove Photo',
1292
+ subtitle: 'Delete current profile picture',
1293
+ onPress: handleAvatarRemove,
1294
+ }
1295
+ ] : []),
1296
+ ]}
1297
+ theme={theme}
1298
+ />
1299
+ </View>
1160
1300
  </View>
1161
1301
 
1162
1302
  {/* Basic Information */}
1163
1303
  <View style={styles.section}>
1164
- <Text style={styles.sectionTitle}>{t('editProfile.sections.basicInfo') || 'Basic Information'}</Text>
1165
-
1166
- <GroupedSection
1167
- items={[
1168
- {
1169
- id: 'display-name',
1170
- icon: 'person',
1171
- iconColor: '#007AFF',
1172
- title: t('editProfile.items.displayName.title') || 'Display Name',
1173
- subtitle: [displayName, lastName].filter(Boolean).join(' ') || (t('editProfile.items.displayName.add') || 'Add your display name'),
1174
- onPress: () => startEditing('displayName', ''),
1175
- },
1176
- {
1177
- id: 'username',
1178
- icon: 'at',
1179
- iconColor: '#5856D6',
1180
- title: t('editProfile.items.username.title') || 'Username',
1181
- subtitle: username || (t('editProfile.items.username.choose') || 'Choose a username'),
1182
- onPress: () => startEditing('username', username),
1183
- },
1184
- {
1185
- id: 'email',
1186
- icon: 'mail',
1187
- iconColor: '#FF9500',
1188
- title: t('editProfile.items.email.title') || 'Email',
1189
- subtitle: email || (t('editProfile.items.email.add') || 'Add your email address'),
1190
- onPress: () => startEditing('email', email),
1191
- },
1192
- ]}
1193
- theme={theme}
1194
- />
1304
+ <Text style={[styles.sectionTitle, { color: themeStyles.isDarkTheme ? '#8E8E93' : '#8E8E93' }]}>
1305
+ {t('editProfile.sections.basicInfo') || 'BASIC INFORMATION'}
1306
+ </Text>
1307
+ <View style={styles.groupedSectionWrapper}>
1308
+ <GroupedSection
1309
+ items={[
1310
+ {
1311
+ id: 'display-name',
1312
+ icon: 'person',
1313
+ iconColor: '#007AFF',
1314
+ title: t('editProfile.items.displayName.title') || 'Display Name',
1315
+ subtitle: [displayName, lastName].filter(Boolean).join(' ') || (t('editProfile.items.displayName.add') || 'Add your display name'),
1316
+ onPress: () => startEditing('displayName', ''),
1317
+ },
1318
+ {
1319
+ id: 'username',
1320
+ icon: 'at',
1321
+ iconColor: '#5856D6',
1322
+ title: t('editProfile.items.username.title') || 'Username',
1323
+ subtitle: username || (t('editProfile.items.username.choose') || 'Choose a username'),
1324
+ onPress: () => startEditing('username', username),
1325
+ },
1326
+ {
1327
+ id: 'email',
1328
+ icon: 'mail',
1329
+ iconColor: '#FF9500',
1330
+ title: t('editProfile.items.email.title') || 'Email',
1331
+ subtitle: email || (t('editProfile.items.email.add') || 'Add your email address'),
1332
+ onPress: () => startEditing('email', email),
1333
+ },
1334
+ ]}
1335
+ theme={theme}
1336
+ />
1337
+ </View>
1195
1338
  </View>
1196
1339
 
1197
1340
  {/* About You */}
1198
1341
  <View style={styles.section}>
1199
- <Text style={styles.sectionTitle}>{t('editProfile.sections.about') || 'About You'}</Text>
1200
-
1201
- <GroupedSection
1202
- items={[
1203
- {
1204
- id: 'bio',
1205
- icon: 'document-text',
1206
- iconColor: '#34C759',
1207
- title: t('editProfile.items.bio.title') || 'Bio',
1208
- subtitle: bio || (t('editProfile.items.bio.placeholder') || 'Tell people about yourself'),
1209
- onPress: () => startEditing('bio', bio),
1210
- },
1211
- {
1212
- id: 'locations',
1213
- icon: 'location',
1214
- iconColor: '#FF3B30',
1215
- title: t('editProfile.items.locations.title') || 'Locations',
1216
- subtitle: tempLocations.length > 0
1217
- ? (tempLocations.length === 1
1218
- ? (t('editProfile.items.locations.count', { count: tempLocations.length }) || `${tempLocations.length} location added`)
1219
- : (t('editProfile.items.locations.count_plural', { count: tempLocations.length }) || `${tempLocations.length} locations added`))
1220
- : (t('editProfile.items.locations.add') || 'Add your locations'),
1221
- onPress: () => startEditing('location', ''),
1222
- customContentBelow: tempLocations.length > 0 && (
1223
- <View style={styles.linksPreviewContainer}>
1224
- {tempLocations.slice(0, 2).map((location, index) => (
1225
- <View key={location.id || index} style={styles.linkPreviewItem}>
1226
- <View style={styles.linkPreviewImage}>
1227
- <Text style={styles.linkPreviewImageText}>
1228
- {location.name.charAt(0).toUpperCase()}
1229
- </Text>
1230
- </View>
1231
- <View style={styles.linkPreviewContent}>
1232
- <Text style={styles.linkPreviewTitle} numberOfLines={1}>
1233
- {location.name}
1234
- </Text>
1235
- {location.label && (
1236
- <Text style={styles.linkPreviewSubtitle}>
1237
- {location.label}
1238
- </Text>
1239
- )}
1240
- </View>
1241
- </View>
1242
- ))}
1243
- {tempLocations.length > 2 && (
1244
- <Text style={styles.linkPreviewMore}>
1245
- +{tempLocations.length - 2} more
1246
- </Text>
1247
- )}
1248
- </View>
1249
- ),
1250
- },
1251
- {
1252
- id: 'links',
1253
- icon: 'link',
1254
- iconColor: '#32D74B',
1255
- title: t('editProfile.items.links.title') || 'Links',
1256
- subtitle: tempLinksWithMetadata.length > 0
1257
- ? (tempLinksWithMetadata.length === 1
1258
- ? (t('editProfile.items.links.count', { count: tempLinksWithMetadata.length }) || `${tempLinksWithMetadata.length} link added`)
1259
- : (t('editProfile.items.links.count_plural', { count: tempLinksWithMetadata.length }) || `${tempLinksWithMetadata.length} links added`))
1260
- : (t('editProfile.items.links.add') || 'Add your links'),
1261
- onPress: () => startEditing('links', ''),
1262
- multiRow: true,
1263
- customContentBelow: tempLinksWithMetadata.length > 0 && (
1264
- <View style={styles.linksPreviewContainer}>
1265
- {tempLinksWithMetadata.slice(0, 2).map((link, index) => (
1266
- <View key={link.id || index} style={styles.linkPreviewItem}>
1267
- {link.image ? (
1268
- <Image source={{ uri: link.image }} style={styles.linkPreviewImage} />
1269
- ) : (
1342
+ <Text style={[styles.sectionTitle, { color: themeStyles.isDarkTheme ? '#8E8E93' : '#8E8E93' }]}>
1343
+ {t('editProfile.sections.about') || 'ABOUT YOU'}
1344
+ </Text>
1345
+ <View style={styles.groupedSectionWrapper}>
1346
+ <GroupedSection
1347
+ items={[
1348
+ {
1349
+ id: 'bio',
1350
+ icon: 'document-text',
1351
+ iconColor: '#34C759',
1352
+ title: t('editProfile.items.bio.title') || 'Bio',
1353
+ subtitle: bio || (t('editProfile.items.bio.placeholder') || 'Tell people about yourself'),
1354
+ onPress: () => startEditing('bio', bio),
1355
+ },
1356
+ {
1357
+ id: 'locations',
1358
+ icon: 'location',
1359
+ iconColor: '#FF3B30',
1360
+ title: t('editProfile.items.locations.title') || 'Locations',
1361
+ subtitle: tempLocations.length > 0
1362
+ ? (tempLocations.length === 1
1363
+ ? (t('editProfile.items.locations.count', { count: tempLocations.length }) || `${tempLocations.length} location added`)
1364
+ : (t('editProfile.items.locations.count_plural', { count: tempLocations.length }) || `${tempLocations.length} locations added`))
1365
+ : (t('editProfile.items.locations.add') || 'Add your locations'),
1366
+ onPress: () => startEditing('location', ''),
1367
+ customContentBelow: tempLocations.length > 0 && (
1368
+ <View style={styles.linksPreviewContainer}>
1369
+ {tempLocations.slice(0, 2).map((location, index) => (
1370
+ <View key={location.id || index} style={styles.linkPreviewItem}>
1270
1371
  <View style={styles.linkPreviewImage}>
1271
1372
  <Text style={styles.linkPreviewImageText}>
1272
- {link.title?.charAt(0).toUpperCase() || link.url.charAt(0).toUpperCase()}
1373
+ {location.name.charAt(0).toUpperCase()}
1273
1374
  </Text>
1274
1375
  </View>
1275
- )}
1276
- <Text style={styles.linkPreviewTitle} numberOfLines={1}>
1277
- {link.title || link.url}
1376
+ <View style={styles.linkPreviewContent}>
1377
+ <Text style={styles.linkPreviewTitle} numberOfLines={1}>
1378
+ {location.name}
1379
+ </Text>
1380
+ {location.label && (
1381
+ <Text style={styles.linkPreviewSubtitle}>
1382
+ {location.label}
1383
+ </Text>
1384
+ )}
1385
+ </View>
1386
+ </View>
1387
+ ))}
1388
+ {tempLocations.length > 2 && (
1389
+ <Text style={styles.linkPreviewMore}>
1390
+ +{tempLocations.length - 2} more
1278
1391
  </Text>
1279
- </View>
1280
- ))}
1281
- {tempLinksWithMetadata.length > 2 && (
1282
- <Text style={styles.linkPreviewMore}>
1283
- +{tempLinksWithMetadata.length - 2} more
1284
- </Text>
1285
- )}
1286
- </View>
1287
- ),
1288
- },
1289
- ]}
1290
- theme={theme}
1291
- />
1392
+ )}
1393
+ </View>
1394
+ ),
1395
+ },
1396
+ {
1397
+ id: 'links',
1398
+ icon: 'link',
1399
+ iconColor: '#32D74B',
1400
+ title: t('editProfile.items.links.title') || 'Links',
1401
+ subtitle: tempLinksWithMetadata.length > 0
1402
+ ? (tempLinksWithMetadata.length === 1
1403
+ ? (t('editProfile.items.links.count', { count: tempLinksWithMetadata.length }) || `${tempLinksWithMetadata.length} link added`)
1404
+ : (t('editProfile.items.links.count_plural', { count: tempLinksWithMetadata.length }) || `${tempLinksWithMetadata.length} links added`))
1405
+ : (t('editProfile.items.links.add') || 'Add your links'),
1406
+ onPress: () => startEditing('links', ''),
1407
+ multiRow: true,
1408
+ customContentBelow: tempLinksWithMetadata.length > 0 && (
1409
+ <View style={styles.linksPreviewContainer}>
1410
+ {tempLinksWithMetadata.slice(0, 2).map((link, index) => (
1411
+ <View key={link.id || index} style={styles.linkPreviewItem}>
1412
+ {link.image ? (
1413
+ <Image source={{ uri: link.image }} style={styles.linkPreviewImage} />
1414
+ ) : (
1415
+ <View style={styles.linkPreviewImage}>
1416
+ <Text style={styles.linkPreviewImageText}>
1417
+ {link.title?.charAt(0).toUpperCase() || link.url.charAt(0).toUpperCase()}
1418
+ </Text>
1419
+ </View>
1420
+ )}
1421
+ <Text style={styles.linkPreviewTitle} numberOfLines={1}>
1422
+ {link.title || link.url}
1423
+ </Text>
1424
+ </View>
1425
+ ))}
1426
+ {tempLinksWithMetadata.length > 2 && (
1427
+ <Text style={styles.linkPreviewMore}>
1428
+ +{tempLinksWithMetadata.length - 2} more
1429
+ </Text>
1430
+ )}
1431
+ </View>
1432
+ ),
1433
+ },
1434
+ ]}
1435
+ theme={theme}
1436
+ />
1437
+ </View>
1292
1438
  </View>
1293
1439
 
1294
1440
  {/* Quick Actions */}
1295
1441
  <View style={styles.section}>
1296
- <Text style={styles.sectionTitle}>{t('editProfile.sections.quickActions') || 'Quick Actions'}</Text>
1297
-
1298
- <GroupedSection
1299
- items={[
1300
- {
1301
- id: 'preview-profile',
1302
- icon: 'eye',
1303
- iconColor: '#007AFF',
1304
- title: t('editProfile.items.previewProfile.title') || 'Preview Profile',
1305
- subtitle: t('editProfile.items.previewProfile.subtitle') || 'See how your profile looks to others',
1306
- onPress: () => navigate?.('Profile', { userId: user?.id }),
1307
- },
1308
- {
1309
- id: 'privacy-settings',
1310
- icon: 'shield-checkmark',
1311
- iconColor: '#8E8E93',
1312
- title: t('editProfile.items.privacySettings.title') || 'Privacy Settings',
1313
- subtitle: t('editProfile.items.privacySettings.subtitle') || 'Control who can see your profile',
1314
- onPress: () => toast.info(t('editProfile.items.privacySettings.coming') || 'Privacy settings coming soon!'),
1315
- },
1316
- {
1317
- id: 'verify-account',
1318
- icon: 'checkmark-circle',
1319
- iconColor: '#30D158',
1320
- title: t('editProfile.items.verifyAccount.title') || 'Verify Account',
1321
- subtitle: t('editProfile.items.verifyAccount.subtitle') || 'Get a verified badge',
1322
- onPress: () => toast.info(t('editProfile.items.verifyAccount.coming') || 'Account verification coming soon!'),
1323
- },
1324
- ]}
1325
- theme={theme}
1326
- />
1442
+ <Text style={[styles.sectionTitle, { color: themeStyles.isDarkTheme ? '#8E8E93' : '#8E8E93' }]}>
1443
+ {t('editProfile.sections.quickActions') || 'QUICK ACTIONS'}
1444
+ </Text>
1445
+ <View style={styles.groupedSectionWrapper}>
1446
+ <GroupedSection
1447
+ items={[
1448
+ {
1449
+ id: 'preview-profile',
1450
+ icon: 'eye',
1451
+ iconColor: '#007AFF',
1452
+ title: t('editProfile.items.previewProfile.title') || 'Preview Profile',
1453
+ subtitle: t('editProfile.items.previewProfile.subtitle') || 'See how your profile looks to others',
1454
+ onPress: () => navigate?.('Profile', { userId: user?.id }),
1455
+ },
1456
+ {
1457
+ id: 'privacy-settings',
1458
+ icon: 'shield-checkmark',
1459
+ iconColor: '#8E8E93',
1460
+ title: t('editProfile.items.privacySettings.title') || 'Privacy Settings',
1461
+ subtitle: t('editProfile.items.privacySettings.subtitle') || 'Control who can see your profile',
1462
+ onPress: () => navigate?.('PrivacySettings'),
1463
+ },
1464
+ {
1465
+ id: 'verify-account',
1466
+ icon: 'checkmark-circle',
1467
+ iconColor: '#30D158',
1468
+ title: t('editProfile.items.verifyAccount.title') || 'Verify Account',
1469
+ subtitle: t('editProfile.items.verifyAccount.subtitle') || 'Get a verified badge',
1470
+ onPress: () => navigate?.('AccountVerification'),
1471
+ },
1472
+ ]}
1473
+ theme={theme}
1474
+ />
1475
+ </View>
1327
1476
  </View>
1328
1477
 
1329
1478
  {/* Security */}
1330
1479
  <View style={styles.section}>
1331
- <Text style={styles.sectionTitle}>{t('editProfile.sections.security') || 'Security'}</Text>
1332
- <GroupedSection
1333
- items={[
1334
- {
1335
- id: 'two-factor',
1336
- icon: 'shield-checkmark',
1337
- iconColor: '#007AFF',
1338
- title: t('editProfile.items.twoFactor.title') || 'Two‑Factor Authentication',
1339
- subtitle: user?.privacySettings?.twoFactorEnabled
1340
- ? (t('editProfile.items.twoFactor.enabled') || 'Enabled')
1341
- : (t('editProfile.items.twoFactor.disabled') || 'Disabled (recommended)'),
1342
- onPress: () => startEditing('twoFactor', ''),
1343
- },
1344
- ]}
1345
- theme={theme}
1346
- />
1480
+ <Text style={[styles.sectionTitle, { color: themeStyles.isDarkTheme ? '#8E8E93' : '#8E8E93' }]}>
1481
+ {t('editProfile.sections.security') || 'SECURITY'}
1482
+ </Text>
1483
+ <View style={styles.groupedSectionWrapper}>
1484
+ <GroupedSection
1485
+ items={[
1486
+ {
1487
+ id: 'two-factor',
1488
+ icon: 'shield-checkmark',
1489
+ iconColor: '#007AFF',
1490
+ title: t('editProfile.items.twoFactor.title') || 'Two‑Factor Authentication',
1491
+ subtitle: user?.privacySettings?.twoFactorEnabled
1492
+ ? (t('editProfile.items.twoFactor.enabled') || 'Enabled')
1493
+ : (t('editProfile.items.twoFactor.disabled') || 'Disabled (recommended)'),
1494
+ onPress: () => startEditing('twoFactor', ''),
1495
+ },
1496
+ ]}
1497
+ theme={theme}
1498
+ />
1499
+ </View>
1347
1500
  </View>
1348
1501
  </>
1349
1502
  )}
@@ -1355,26 +1508,34 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
1355
1508
  const styles = StyleSheet.create({
1356
1509
  container: {
1357
1510
  flex: 1,
1358
- backgroundColor: '#f2f2f2',
1359
1511
  },
1360
1512
  content: {
1361
1513
  flex: 1,
1362
- padding: 16,
1514
+ paddingTop: 8,
1515
+ paddingBottom: 24,
1363
1516
  },
1364
1517
  contentEditing: {
1365
1518
  flex: 1,
1366
1519
  padding: 0,
1367
1520
  },
1368
1521
  section: {
1369
- marginBottom: 24,
1522
+ marginBottom: 32,
1370
1523
  },
1371
1524
  sectionTitle: {
1372
- fontSize: 16,
1525
+ fontSize: 13,
1373
1526
  fontWeight: '600',
1374
- color: '#333',
1375
- marginBottom: 12,
1527
+ color: '#8E8E93',
1528
+ marginBottom: 8,
1529
+ marginTop: 4,
1530
+ marginHorizontal: 16,
1531
+ textTransform: 'uppercase',
1532
+ letterSpacing: 0.5,
1376
1533
  fontFamily: fontFamilies.phuduSemiBold,
1377
1534
  },
1535
+ groupedSectionWrapper: {
1536
+ marginHorizontal: 16,
1537
+ backgroundColor: 'transparent',
1538
+ },
1378
1539
 
1379
1540
  userIcon: {
1380
1541
  marginRight: 12,
@@ -1413,21 +1574,21 @@ const styles = StyleSheet.create({
1413
1574
  flex: 1,
1414
1575
  },
1415
1576
  editingFieldLabel: {
1416
- fontSize: 16,
1577
+ fontSize: 13,
1417
1578
  fontWeight: '600',
1418
- color: '#333',
1419
- marginBottom: 12,
1579
+ marginBottom: 8,
1420
1580
  fontFamily: fontFamilies.phuduSemiBold,
1581
+ textTransform: 'uppercase',
1582
+ letterSpacing: 0.5,
1421
1583
  },
1422
1584
  editingFieldInput: {
1423
- backgroundColor: '#fff',
1424
- borderWidth: 2,
1425
- borderColor: '#e0e0e0',
1426
- borderRadius: 12,
1585
+ borderWidth: StyleSheet.hairlineWidth,
1586
+ borderRadius: 14,
1427
1587
  padding: 16,
1428
1588
  fontSize: 17,
1429
1589
  minHeight: 52,
1430
1590
  fontWeight: '400',
1591
+ letterSpacing: -0.2,
1431
1592
  },
1432
1593
  editingFieldDescription: {
1433
1594
  fontSize: 14,
@@ -1450,22 +1611,20 @@ const styles = StyleSheet.create({
1450
1611
  fontWeight: '600',
1451
1612
  },
1452
1613
  editingFieldTextArea: {
1453
- backgroundColor: '#fff',
1454
- borderWidth: 2,
1455
- borderColor: '#e0e0e0',
1456
- borderRadius: 12,
1614
+ borderWidth: StyleSheet.hairlineWidth,
1615
+ borderRadius: 14,
1457
1616
  padding: 16,
1458
1617
  fontSize: 17,
1459
1618
  minHeight: 120,
1460
1619
  textAlignVertical: 'top',
1461
1620
  fontWeight: '400',
1621
+ letterSpacing: -0.2,
1462
1622
  },
1463
1623
  // Custom editing header styles
1464
1624
  editingHeader: {
1465
1625
  paddingTop: Platform.OS === 'ios' ? 50 : 16,
1466
1626
  paddingBottom: 0,
1467
- borderBottomWidth: 1,
1468
- backgroundColor: '#fff',
1627
+ borderBottomWidth: StyleSheet.hairlineWidth,
1469
1628
  },
1470
1629
  editingHeaderContent: {
1471
1630
  flexDirection: 'row',
@@ -1474,10 +1633,9 @@ const styles = StyleSheet.create({
1474
1633
  minHeight: 44,
1475
1634
  },
1476
1635
  editingBackButton: {
1477
- width: 32,
1478
- height: 32,
1479
- borderRadius: 16,
1480
- backgroundColor: '#F8F9FA',
1636
+ width: 36,
1637
+ height: 36,
1638
+ borderRadius: 20,
1481
1639
  alignItems: 'center',
1482
1640
  justifyContent: 'center',
1483
1641
  marginRight: 12,
@@ -1505,8 +1663,7 @@ const styles = StyleSheet.create({
1505
1663
  editingSaveButton: {
1506
1664
  paddingHorizontal: 16,
1507
1665
  paddingVertical: 8,
1508
- borderRadius: 18,
1509
- backgroundColor: '#F8F9FA',
1666
+ borderRadius: 20,
1510
1667
  minWidth: 60,
1511
1668
  alignItems: 'center',
1512
1669
  justifyContent: 'center',
@@ -1519,20 +1676,24 @@ const styles = StyleSheet.create({
1519
1676
  editingHeaderBottom: {
1520
1677
  flexDirection: 'column',
1521
1678
  alignItems: 'flex-start',
1522
- paddingHorizontal: 16,
1523
- paddingBottom: 8,
1524
- paddingTop: 8,
1525
- },
1526
- editingBottomIcon: {
1527
- marginBottom: 8,
1528
- alignSelf: 'flex-start',
1679
+ paddingHorizontal: 20,
1680
+ paddingBottom: 20,
1681
+ paddingTop: 24,
1682
+ },
1683
+ editingIconContainer: {
1684
+ width: 64,
1685
+ height: 64,
1686
+ borderRadius: 20,
1687
+ alignItems: 'center',
1688
+ justifyContent: 'center',
1689
+ marginBottom: 16,
1529
1690
  },
1530
1691
  editingBottomTitle: {
1531
- fontSize: 32,
1692
+ fontSize: 28,
1532
1693
  fontWeight: '700',
1533
1694
  fontFamily: fontFamilies.phuduBold,
1534
1695
  letterSpacing: -0.5,
1535
- lineHeight: 36,
1696
+ lineHeight: 34,
1536
1697
  textAlign: 'left',
1537
1698
  alignSelf: 'flex-start',
1538
1699
  },