@oxyhq/services 5.13.1 → 5.13.3

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 (204) hide show
  1. package/README.md +71 -0
  2. package/lib/commonjs/core/HttpClient.js +238 -0
  3. package/lib/commonjs/core/HttpClient.js.map +1 -0
  4. package/lib/commonjs/core/OxyServices.js +538 -332
  5. package/lib/commonjs/core/OxyServices.js.map +1 -1
  6. package/lib/commonjs/core/RequestManager.js +199 -0
  7. package/lib/commonjs/core/RequestManager.js.map +1 -0
  8. package/lib/commonjs/core/index.js +38 -1
  9. package/lib/commonjs/core/index.js.map +1 -1
  10. package/lib/commonjs/index.js +36 -0
  11. package/lib/commonjs/index.js.map +1 -1
  12. package/lib/commonjs/ui/components/Avatar.js +94 -27
  13. package/lib/commonjs/ui/components/Avatar.js.map +1 -1
  14. package/lib/commonjs/ui/components/FollowButton.js +1 -0
  15. package/lib/commonjs/ui/components/FollowButton.js.map +1 -1
  16. package/lib/commonjs/ui/components/internal/TextField.js +13 -8
  17. package/lib/commonjs/ui/components/internal/TextField.js.map +1 -1
  18. package/lib/commonjs/ui/context/OxyContext.js +183 -224
  19. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  20. package/lib/commonjs/ui/hooks/useSessionSocket.js +80 -22
  21. package/lib/commonjs/ui/hooks/useSessionSocket.js.map +1 -1
  22. package/lib/commonjs/ui/index.js +4 -1
  23. package/lib/commonjs/ui/index.js.map +1 -1
  24. package/lib/commonjs/ui/screens/AccountSettingsScreen.js +32 -2
  25. package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
  26. package/lib/commonjs/ui/screens/AccountSwitcherScreen.js +101 -59
  27. package/lib/commonjs/ui/screens/AccountSwitcherScreen.js.map +1 -1
  28. package/lib/commonjs/ui/screens/FileManagementScreen.js +3 -2
  29. package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
  30. package/lib/commonjs/ui/screens/LanguageSelectorScreen.js +75 -117
  31. package/lib/commonjs/ui/screens/LanguageSelectorScreen.js.map +1 -1
  32. package/lib/commonjs/ui/screens/SignInScreen.js +0 -11
  33. package/lib/commonjs/ui/screens/SignInScreen.js.map +1 -1
  34. package/lib/commonjs/ui/screens/SignUpScreen.js +14 -16
  35. package/lib/commonjs/ui/screens/SignUpScreen.js.map +1 -1
  36. package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js +50 -18
  37. package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js.map +1 -1
  38. package/lib/commonjs/ui/screens/internal/SignInPasswordStep.js +10 -10
  39. package/lib/commonjs/ui/screens/internal/SignInPasswordStep.js.map +1 -1
  40. package/lib/commonjs/ui/screens/steps/SignInPasswordStep.js +16 -26
  41. package/lib/commonjs/ui/screens/steps/SignInPasswordStep.js.map +1 -1
  42. package/lib/commonjs/ui/screens/steps/SignInUsernameStep.js +104 -212
  43. package/lib/commonjs/ui/screens/steps/SignInUsernameStep.js.map +1 -1
  44. package/lib/commonjs/ui/stores/accountStore.js +237 -0
  45. package/lib/commonjs/ui/stores/accountStore.js.map +1 -0
  46. package/lib/commonjs/ui/stores/authStore.js +2 -1
  47. package/lib/commonjs/ui/stores/authStore.js.map +1 -1
  48. package/lib/commonjs/ui/styles/authStyles.js +14 -7
  49. package/lib/commonjs/ui/styles/authStyles.js.map +1 -1
  50. package/lib/commonjs/utils/asyncUtils.js +9 -22
  51. package/lib/commonjs/utils/asyncUtils.js.map +1 -1
  52. package/lib/commonjs/utils/cache.js +259 -0
  53. package/lib/commonjs/utils/cache.js.map +1 -0
  54. package/lib/commonjs/utils/index.js +99 -0
  55. package/lib/commonjs/utils/index.js.map +1 -1
  56. package/lib/commonjs/utils/languageUtils.js +159 -0
  57. package/lib/commonjs/utils/languageUtils.js.map +1 -0
  58. package/lib/commonjs/utils/requestUtils.js +217 -0
  59. package/lib/commonjs/utils/requestUtils.js.map +1 -0
  60. package/lib/commonjs/utils/sessionUtils.js +191 -0
  61. package/lib/commonjs/utils/sessionUtils.js.map +1 -0
  62. package/lib/module/core/HttpClient.js +232 -0
  63. package/lib/module/core/HttpClient.js.map +1 -0
  64. package/lib/module/core/OxyServices.js +536 -326
  65. package/lib/module/core/OxyServices.js.map +1 -1
  66. package/lib/module/core/RequestManager.js +194 -0
  67. package/lib/module/core/RequestManager.js.map +1 -0
  68. package/lib/module/core/index.js +2 -0
  69. package/lib/module/core/index.js.map +1 -1
  70. package/lib/module/index.js +2 -0
  71. package/lib/module/index.js.map +1 -1
  72. package/lib/module/ui/components/Avatar.js +94 -27
  73. package/lib/module/ui/components/Avatar.js.map +1 -1
  74. package/lib/module/ui/components/FollowButton.js +1 -0
  75. package/lib/module/ui/components/FollowButton.js.map +1 -1
  76. package/lib/module/ui/components/internal/TextField.js +13 -8
  77. package/lib/module/ui/components/internal/TextField.js.map +1 -1
  78. package/lib/module/ui/context/OxyContext.js +182 -223
  79. package/lib/module/ui/context/OxyContext.js.map +1 -1
  80. package/lib/module/ui/hooks/useSessionSocket.js +80 -22
  81. package/lib/module/ui/hooks/useSessionSocket.js.map +1 -1
  82. package/lib/module/ui/index.js +4 -2
  83. package/lib/module/ui/index.js.map +1 -1
  84. package/lib/module/ui/screens/AccountSettingsScreen.js +33 -2
  85. package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
  86. package/lib/module/ui/screens/AccountSwitcherScreen.js +102 -60
  87. package/lib/module/ui/screens/AccountSwitcherScreen.js.map +1 -1
  88. package/lib/module/ui/screens/FileManagementScreen.js +3 -2
  89. package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
  90. package/lib/module/ui/screens/LanguageSelectorScreen.js +73 -117
  91. package/lib/module/ui/screens/LanguageSelectorScreen.js.map +1 -1
  92. package/lib/module/ui/screens/SignInScreen.js +0 -11
  93. package/lib/module/ui/screens/SignInScreen.js.map +1 -1
  94. package/lib/module/ui/screens/SignUpScreen.js +14 -16
  95. package/lib/module/ui/screens/SignUpScreen.js.map +1 -1
  96. package/lib/module/ui/screens/WelcomeNewUserScreen.js +50 -18
  97. package/lib/module/ui/screens/WelcomeNewUserScreen.js.map +1 -1
  98. package/lib/module/ui/screens/internal/SignInPasswordStep.js +10 -10
  99. package/lib/module/ui/screens/internal/SignInPasswordStep.js.map +1 -1
  100. package/lib/module/ui/screens/steps/SignInPasswordStep.js +16 -26
  101. package/lib/module/ui/screens/steps/SignInPasswordStep.js.map +1 -1
  102. package/lib/module/ui/screens/steps/SignInUsernameStep.js +105 -214
  103. package/lib/module/ui/screens/steps/SignInUsernameStep.js.map +1 -1
  104. package/lib/module/ui/stores/accountStore.js +229 -0
  105. package/lib/module/ui/stores/accountStore.js.map +1 -0
  106. package/lib/module/ui/stores/authStore.js +2 -1
  107. package/lib/module/ui/stores/authStore.js.map +1 -1
  108. package/lib/module/ui/styles/authStyles.js +14 -7
  109. package/lib/module/ui/styles/authStyles.js.map +1 -1
  110. package/lib/module/utils/asyncUtils.js +10 -22
  111. package/lib/module/utils/asyncUtils.js.map +1 -1
  112. package/lib/module/utils/cache.js +250 -0
  113. package/lib/module/utils/cache.js.map +1 -0
  114. package/lib/module/utils/index.js +7 -0
  115. package/lib/module/utils/index.js.map +1 -1
  116. package/lib/module/utils/languageUtils.js +151 -0
  117. package/lib/module/utils/languageUtils.js.map +1 -0
  118. package/lib/module/utils/requestUtils.js +210 -0
  119. package/lib/module/utils/requestUtils.js.map +1 -0
  120. package/lib/module/utils/sessionUtils.js +180 -0
  121. package/lib/module/utils/sessionUtils.js.map +1 -0
  122. package/lib/typescript/core/HttpClient.d.ts +64 -0
  123. package/lib/typescript/core/HttpClient.d.ts.map +1 -0
  124. package/lib/typescript/core/OxyServices.d.ts +88 -71
  125. package/lib/typescript/core/OxyServices.d.ts.map +1 -1
  126. package/lib/typescript/core/RequestManager.d.ts +67 -0
  127. package/lib/typescript/core/RequestManager.d.ts.map +1 -0
  128. package/lib/typescript/core/index.d.ts +2 -0
  129. package/lib/typescript/core/index.d.ts.map +1 -1
  130. package/lib/typescript/index.d.ts +2 -0
  131. package/lib/typescript/index.d.ts.map +1 -1
  132. package/lib/typescript/models/interfaces.d.ts +15 -0
  133. package/lib/typescript/models/interfaces.d.ts.map +1 -1
  134. package/lib/typescript/models/session.d.ts +1 -0
  135. package/lib/typescript/models/session.d.ts.map +1 -1
  136. package/lib/typescript/ui/components/Avatar.d.ts +6 -7
  137. package/lib/typescript/ui/components/Avatar.d.ts.map +1 -1
  138. package/lib/typescript/ui/components/FollowButton.d.ts.map +1 -1
  139. package/lib/typescript/ui/components/internal/TextField.d.ts.map +1 -1
  140. package/lib/typescript/ui/context/OxyContext.d.ts +4 -0
  141. package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
  142. package/lib/typescript/ui/hooks/useSessionSocket.d.ts.map +1 -1
  143. package/lib/typescript/ui/index.d.ts +2 -2
  144. package/lib/typescript/ui/index.d.ts.map +1 -1
  145. package/lib/typescript/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
  146. package/lib/typescript/ui/screens/AccountSwitcherScreen.d.ts.map +1 -1
  147. package/lib/typescript/ui/screens/LanguageSelectorScreen.d.ts +3 -3
  148. package/lib/typescript/ui/screens/LanguageSelectorScreen.d.ts.map +1 -1
  149. package/lib/typescript/ui/screens/SignInScreen.d.ts.map +1 -1
  150. package/lib/typescript/ui/screens/SignUpScreen.d.ts.map +1 -1
  151. package/lib/typescript/ui/screens/WelcomeNewUserScreen.d.ts.map +1 -1
  152. package/lib/typescript/ui/screens/internal/SignInPasswordStep.d.ts.map +1 -1
  153. package/lib/typescript/ui/screens/steps/SignInPasswordStep.d.ts.map +1 -1
  154. package/lib/typescript/ui/screens/steps/SignInUsernameStep.d.ts.map +1 -1
  155. package/lib/typescript/ui/stores/accountStore.d.ts +34 -0
  156. package/lib/typescript/ui/stores/accountStore.d.ts.map +1 -0
  157. package/lib/typescript/ui/stores/authStore.d.ts.map +1 -1
  158. package/lib/typescript/ui/styles/authStyles.d.ts +18 -2
  159. package/lib/typescript/ui/styles/authStyles.d.ts.map +1 -1
  160. package/lib/typescript/utils/asyncUtils.d.ts +2 -0
  161. package/lib/typescript/utils/asyncUtils.d.ts.map +1 -1
  162. package/lib/typescript/utils/cache.d.ts +128 -0
  163. package/lib/typescript/utils/cache.d.ts.map +1 -0
  164. package/lib/typescript/utils/index.d.ts +4 -0
  165. package/lib/typescript/utils/index.d.ts.map +1 -1
  166. package/lib/typescript/utils/languageUtils.d.ts +38 -0
  167. package/lib/typescript/utils/languageUtils.d.ts.map +1 -0
  168. package/lib/typescript/utils/requestUtils.d.ts +122 -0
  169. package/lib/typescript/utils/requestUtils.d.ts.map +1 -0
  170. package/lib/typescript/utils/sessionUtils.d.ts +55 -0
  171. package/lib/typescript/utils/sessionUtils.d.ts.map +1 -0
  172. package/package.json +1 -1
  173. package/src/core/HttpClient.ts +277 -0
  174. package/src/core/OxyServices.ts +466 -351
  175. package/src/core/RequestManager.ts +240 -0
  176. package/src/core/index.ts +10 -0
  177. package/src/index.ts +10 -0
  178. package/src/models/interfaces.ts +19 -0
  179. package/src/models/session.ts +1 -1
  180. package/src/ui/components/Avatar.tsx +151 -35
  181. package/src/ui/components/FollowButton.tsx +1 -0
  182. package/src/ui/components/internal/TextField.tsx +7 -6
  183. package/src/ui/context/OxyContext.tsx +213 -217
  184. package/src/ui/hooks/useSessionSocket.ts +72 -18
  185. package/src/ui/index.ts +4 -1
  186. package/src/ui/screens/AccountSettingsScreen.tsx +34 -2
  187. package/src/ui/screens/AccountSwitcherScreen.tsx +102 -68
  188. package/src/ui/screens/FileManagementScreen.tsx +1 -1
  189. package/src/ui/screens/LanguageSelectorScreen.tsx +86 -143
  190. package/src/ui/screens/SignInScreen.tsx +0 -7
  191. package/src/ui/screens/SignUpScreen.tsx +14 -15
  192. package/src/ui/screens/WelcomeNewUserScreen.tsx +52 -15
  193. package/src/ui/screens/internal/SignInPasswordStep.tsx +4 -6
  194. package/src/ui/screens/steps/SignInPasswordStep.tsx +4 -8
  195. package/src/ui/screens/steps/SignInUsernameStep.tsx +110 -256
  196. package/src/ui/stores/accountStore.ts +285 -0
  197. package/src/ui/stores/authStore.ts +2 -1
  198. package/src/ui/styles/authStyles.ts +14 -7
  199. package/src/utils/asyncUtils.ts +10 -24
  200. package/src/utils/cache.ts +264 -0
  201. package/src/utils/index.ts +19 -0
  202. package/src/utils/languageUtils.ts +174 -0
  203. package/src/utils/requestUtils.ts +234 -0
  204. package/src/utils/sessionUtils.ts +206 -0
@@ -13,10 +13,35 @@ interface UseSessionSocketProps {
13
13
 
14
14
  export function useSessionSocket({ userId, activeSessionId, refreshSessions, logout, baseURL, onRemoteSignOut }: UseSessionSocketProps) {
15
15
  const socketRef = useRef<any>(null);
16
+ const joinedRoomRef = useRef<string | null>(null);
17
+
18
+ // Store callbacks in refs to avoid re-joining when they change
19
+ const refreshSessionsRef = useRef(refreshSessions);
20
+ const logoutRef = useRef(logout);
21
+ const onRemoteSignOutRef = useRef(onRemoteSignOut);
22
+ const activeSessionIdRef = useRef(activeSessionId);
16
23
 
24
+ // Update refs when callbacks change
17
25
  useEffect(() => {
18
- if (!userId || !baseURL) return;
26
+ refreshSessionsRef.current = refreshSessions;
27
+ logoutRef.current = logout;
28
+ onRemoteSignOutRef.current = onRemoteSignOut;
29
+ activeSessionIdRef.current = activeSessionId;
30
+ }, [refreshSessions, logout, onRemoteSignOut, activeSessionId]);
19
31
 
32
+ useEffect(() => {
33
+ if (!userId || !baseURL) {
34
+ // Clean up if userId or baseURL becomes invalid
35
+ if (socketRef.current && joinedRoomRef.current) {
36
+ socketRef.current.emit('leave', { userId: joinedRoomRef.current });
37
+ joinedRoomRef.current = null;
38
+ }
39
+ return;
40
+ }
41
+
42
+ const roomId = `user:${userId}`;
43
+
44
+ // Only create socket if it doesn't exist
20
45
  if (!socketRef.current) {
21
46
  socketRef.current = io(baseURL, {
22
47
  transports: ['websocket'],
@@ -24,30 +49,59 @@ export function useSessionSocket({ userId, activeSessionId, refreshSessions, log
24
49
  }
25
50
  const socket = socketRef.current;
26
51
 
27
- socket.on('connect', () => {
28
- console.log('Socket connected:', socket.id);
29
- });
52
+ // Only join if we haven't already joined this room
53
+ if (joinedRoomRef.current !== roomId) {
54
+ // Leave previous room if switching users
55
+ if (joinedRoomRef.current) {
56
+ socket.emit('leave', { userId: joinedRoomRef.current });
57
+ }
58
+
59
+ socket.emit('join', { userId: roomId });
60
+ joinedRoomRef.current = roomId;
61
+
62
+ if (__DEV__) {
63
+ console.log('Emitting join for room:', roomId);
64
+ }
65
+ }
30
66
 
31
- socket.emit('join', { userId: `user:${userId}` });
32
- console.log('Emitting join for room:', `user:${userId}`);
67
+ // Set up event handlers (only once per socket instance)
68
+ const handleConnect = () => {
69
+ if (__DEV__) {
70
+ console.log('Socket connected:', socket.id);
71
+ }
72
+ };
33
73
 
34
- socket.on('session_update', (data: { type: string; sessionId: string }) => {
35
- console.log('Received session_update:', data);
74
+ const handleSessionUpdate = (data: { type: string; sessionId: string }) => {
75
+ if (__DEV__) {
76
+ console.log('Received session_update:', data);
77
+ }
36
78
 
37
- // Always refresh sessions to get the latest state
38
- refreshSessions();
79
+ // Use refs to get latest callback versions
80
+ refreshSessionsRef.current();
39
81
 
40
82
  // If the current session was logged out, handle it specially
41
- if (data.sessionId === activeSessionId) {
42
- if (onRemoteSignOut) onRemoteSignOut();
43
- else toast.info('You have been signed out remotely.');
44
- logout();
83
+ if (data.sessionId === activeSessionIdRef.current) {
84
+ if (onRemoteSignOutRef.current) {
85
+ onRemoteSignOutRef.current();
86
+ } else {
87
+ toast.info('You have been signed out remotely.');
88
+ }
89
+ logoutRef.current();
45
90
  }
46
- });
91
+ };
92
+
93
+ socket.on('connect', handleConnect);
94
+ socket.on('session_update', handleSessionUpdate);
47
95
 
48
96
  return () => {
49
- socket.emit('leave', { userId: `user:${userId}` });
50
- socket.off('session_update');
97
+ socket.off('connect', handleConnect);
98
+ socket.off('session_update', handleSessionUpdate);
99
+
100
+ // Only leave on unmount if we're still in this room
101
+ if (joinedRoomRef.current === roomId) {
102
+ socket.emit('leave', { userId: roomId });
103
+ joinedRoomRef.current = null;
104
+ }
51
105
  };
52
- }, [userId, baseURL, activeSessionId, refreshSessions, logout, onRemoteSignOut]);
106
+ }, [userId, baseURL]); // Only depend on userId and baseURL - callbacks are in refs
53
107
  }
package/src/ui/index.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  import isFrontend from './isFrontend';
8
8
 
9
9
  // Real UI exports
10
- let OxyProvider, OxySignInButton, OxyLogo, Avatar, FollowButton, OxyPayButton, FontLoader, setupFonts, OxyIcon, useOxy, useOxyAuth, useOxyUser, useOxyKarma, useOxyPayments, useOxyDevices, useOxyNotifications, useOxySocket, useOxyQR, OxyContextProvider, OxyContextState, OxyContextProviderProps, useFollow, ProfileScreen, OxyRouter, useAuthStore, fontFamilies, fontStyles, toast;
10
+ let OxyProvider, OxySignInButton, OxyLogo, Avatar, FollowButton, OxyPayButton, FontLoader, setupFonts, OxyIcon, useOxy, useOxyAuth, useOxyUser, useOxyKarma, useOxyPayments, useOxyDevices, useOxyNotifications, useOxySocket, useOxyQR, OxyContextProvider, OxyContextState, OxyContextProviderProps, useFollow, ProfileScreen, OxyRouter, useAuthStore, useAccountStore, fontFamilies, fontStyles, toast;
11
11
 
12
12
  if (isFrontend) {
13
13
  OxyProvider = require('./components/OxyProvider').default;
@@ -27,6 +27,7 @@ if (isFrontend) {
27
27
  ProfileScreen = require('./screens/ProfileScreen').default;
28
28
  OxyRouter = require('./navigation/OxyRouter').default;
29
29
  useAuthStore = require('./stores/authStore').useAuthStore;
30
+ useAccountStore = require('./stores/accountStore').useAccountStore;
30
31
  fontFamilies = require('./styles/fonts').fontFamilies;
31
32
  fontStyles = require('./styles/fonts').fontStyles;
32
33
  toast = require('../lib/sonner').toast;
@@ -51,6 +52,7 @@ if (isFrontend) {
51
52
  ProfileScreen = noopComponent;
52
53
  OxyRouter = noopComponent;
53
54
  useAuthStore = noopHook;
55
+ useAccountStore = noopHook;
54
56
  fontFamilies = {};
55
57
  fontStyles = {};
56
58
  toast = () => {};
@@ -74,6 +76,7 @@ export {
74
76
  ProfileScreen,
75
77
  OxyRouter,
76
78
  useAuthStore,
79
+ useAccountStore,
77
80
  fontFamilies,
78
81
  fontStyles,
79
82
  toast
@@ -25,6 +25,13 @@ import { useAuthStore } from '../stores/authStore';
25
25
  import { Header, GroupedSection } from '../components';
26
26
  import { useI18n } from '../hooks/useI18n';
27
27
  import QRCode from 'react-native-qrcode-svg';
28
+ import { TTLCache, registerCacheForCleanup } from '../../utils/cache';
29
+
30
+ // Caches for link metadata and location searches
31
+ const linkMetadataCache = new TTLCache<any>(30 * 60 * 1000); // 30 minutes cache for link metadata
32
+ const locationSearchCache = new TTLCache<any[]>(60 * 60 * 1000); // 1 hour cache for location searches
33
+ registerCacheForCleanup(linkMetadataCache);
34
+ registerCacheForCleanup(locationSearchCache);
28
35
 
29
36
  const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
30
37
  onClose,
@@ -369,6 +376,13 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
369
376
 
370
377
 
371
378
  const fetchLinkMetadata = async (url: string) => {
379
+ // Check cache first
380
+ const cacheKey = url.toLowerCase().trim();
381
+ const cached = linkMetadataCache.get(cacheKey);
382
+ if (cached) {
383
+ return cached;
384
+ }
385
+
372
386
  try {
373
387
  setIsFetchingMetadata(true);
374
388
  console.log('Fetching metadata for URL:', url);
@@ -377,20 +391,27 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
377
391
  const metadata = await oxyServices.fetchLinkMetadata(url);
378
392
  console.log('Received metadata:', metadata);
379
393
 
380
- return {
394
+ const result = {
381
395
  ...metadata,
382
396
  id: Date.now().toString()
383
397
  };
398
+
399
+ // Cache the result
400
+ linkMetadataCache.set(cacheKey, result);
401
+ return result;
384
402
  } catch (error) {
385
403
  console.error('Error fetching metadata:', error);
386
404
  // Fallback to basic metadata
387
- return {
405
+ const fallback = {
388
406
  url: url.startsWith('http') ? url : 'https://' + url,
389
407
  title: url.replace(/^https?:\/\//, '').replace(/\/$/, ''),
390
408
  description: 'Link',
391
409
  image: undefined,
392
410
  id: Date.now().toString()
393
411
  };
412
+ // Cache fallback too (shorter TTL)
413
+ linkMetadataCache.set(cacheKey, fallback, 5 * 60 * 1000); // 5 minutes for fallbacks
414
+ return fallback;
394
415
  } finally {
395
416
  setIsFetchingMetadata(false);
396
417
  }
@@ -402,12 +423,23 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
402
423
  return;
403
424
  }
404
425
 
426
+ // Check cache first
427
+ const cacheKey = query.toLowerCase().trim();
428
+ const cached = locationSearchCache.get(cacheKey);
429
+ if (cached) {
430
+ setLocationSearchResults(cached);
431
+ return;
432
+ }
433
+
405
434
  try {
406
435
  setIsSearchingLocations(true);
407
436
  const response = await fetch(
408
437
  `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}&limit=5&addressdetails=1`
409
438
  );
410
439
  const data = await response.json();
440
+
441
+ // Cache the results
442
+ locationSearchCache.set(cacheKey, data);
411
443
  setLocationSearchResults(data);
412
444
  } catch (error) {
413
445
  console.error('Error searching locations:', error);
@@ -1,5 +1,5 @@
1
1
  import type React from 'react';
2
- import { useState, useEffect } from 'react';
2
+ import { useState, useEffect, useMemo, useCallback } from 'react';
3
3
  import {
4
4
  View,
5
5
  Text,
@@ -74,8 +74,8 @@ const ModernAccountSwitcherScreen: React.FC<BaseScreenProps> = ({
74
74
  const isDarkTheme = theme === 'dark';
75
75
  const { t } = useI18n();
76
76
 
77
- // Modern color scheme
78
- const colors = {
77
+ // Modern color scheme - memoized for performance
78
+ const colors = useMemo(() => ({
79
79
  background: isDarkTheme ? '#000000' : '#FFFFFF',
80
80
  surface: isDarkTheme ? '#1C1C1E' : '#F2F2F7',
81
81
  card: isDarkTheme ? '#2C2C2E' : '#FFFFFF',
@@ -87,7 +87,7 @@ const ModernAccountSwitcherScreen: React.FC<BaseScreenProps> = ({
87
87
  border: isDarkTheme ? '#38383A' : '#C6C6C8',
88
88
  activeCard: isDarkTheme ? '#0A84FF20' : '#007AFF15',
89
89
  shadow: isDarkTheme ? 'rgba(0,0,0,0.3)' : 'rgba(0,0,0,0.1)',
90
- };
90
+ }), [isDarkTheme]);
91
91
 
92
92
  // Refresh sessions when screen loads
93
93
  useEffect(() => {
@@ -96,54 +96,72 @@ const ModernAccountSwitcherScreen: React.FC<BaseScreenProps> = ({
96
96
  }
97
97
  }, [isAuthenticated, activeSessionId]);
98
98
 
99
+ // Memoize session IDs to prevent unnecessary re-renders
100
+ const sessionIds = useMemo(() => sessions.map(s => s.sessionId).join(','), [sessions]);
101
+
99
102
  // Load user profiles for sessions
103
+ // Production-ready: Optimized with batching, memoization, and error handling
100
104
  useEffect(() => {
101
- console.log('AccountSwitcherScreen - sessions changed:', sessions);
102
- console.log('AccountSwitcherScreen - sessions length:', sessions.length);
103
- console.log('AccountSwitcherScreen - activeSessionId:', activeSessionId);
104
- console.log('AccountSwitcherScreen - isAuthenticated:', isAuthenticated);
105
+ let cancelled = false;
105
106
 
106
107
  const loadUserProfiles = async () => {
107
- if (!sessions.length || !oxyServices) return;
108
+ if (!sessions.length || !oxyServices || cancelled) return;
109
+
110
+ // Sessions are already deduplicated by userId at the core level (OxyContext)
111
+ const uniqueSessions = sessions;
108
112
 
109
- const updatedSessions: SessionWithUser[] = sessions.map(session => ({
113
+ // Initialize loading state
114
+ setSessionsWithUsers(uniqueSessions.map(session => ({
110
115
  ...session,
111
116
  isLoadingProfile: true,
112
- }));
113
- setSessionsWithUsers(updatedSessions);
114
-
115
- // Load profiles for each session
116
- for (let i = 0; i < sessions.length; i++) {
117
- const session = sessions[i];
118
- try {
119
- // Try to get user profile using the session
120
- const userProfile = await oxyServices.getUserBySession(session.sessionId);
121
-
122
- setSessionsWithUsers(prev =>
123
- prev.map(s =>
124
- s.sessionId === session.sessionId
125
- ? { ...s, userProfile, isLoadingProfile: false }
126
- : s
127
- )
128
- );
129
- } catch (error) {
130
- console.error(`Failed to load profile for session ${session.sessionId}:`, error);
117
+ })));
118
+
119
+ // Batch load profiles for better performance using batch endpoint
120
+ try {
121
+ const sessionIds = uniqueSessions.map(s => s.sessionId);
122
+ const batchResults = await oxyServices.getUsersBySessions(sessionIds);
123
+
124
+ // Create a map for O(1) lookup
125
+ const userProfileMap = new Map<string, User | null>();
126
+ batchResults.forEach(({ sessionId, user }) => {
127
+ userProfileMap.set(sessionId, user);
128
+ });
129
+
130
+ if (cancelled) return;
131
+
132
+ // Update sessions with loaded profiles - optimized with Map for O(1) lookup
133
+ setSessionsWithUsers(prev => {
134
+ return prev.map(session => {
135
+ const userProfile = userProfileMap.get(session.sessionId);
136
+ return {
137
+ ...session,
138
+ userProfile: userProfile || undefined,
139
+ isLoadingProfile: false,
140
+ };
141
+ });
142
+ });
143
+ } catch (error) {
144
+ if (!cancelled && __DEV__) {
145
+ console.error('Failed to load user profiles:', error);
146
+ }
147
+ if (!cancelled) {
131
148
  setSessionsWithUsers(prev =>
132
- prev.map(s =>
133
- s.sessionId === session.sessionId
134
- ? { ...s, isLoadingProfile: false }
135
- : s
136
- )
149
+ prev.map(s => ({ ...s, isLoadingProfile: false }))
137
150
  );
138
151
  }
139
152
  }
140
153
  };
141
154
 
142
155
  loadUserProfiles();
143
- }, [sessions, oxyServices]);
144
156
 
145
- const handleSwitchSession = async (sessionId: string) => {
146
- if (sessionId === user?.sessionId) return; // Already active session
157
+ return () => {
158
+ cancelled = true;
159
+ };
160
+ }, [sessionIds, oxyServices, sessions]);
161
+
162
+ const handleSwitchSession = useCallback(async (sessionId: string) => {
163
+ if (sessionId === activeSessionId) return; // Already active session
164
+ if (switchingToUserId) return; // Already switching
147
165
 
148
166
  setSwitchingToUserId(sessionId);
149
167
  try {
@@ -153,14 +171,18 @@ const ModernAccountSwitcherScreen: React.FC<BaseScreenProps> = ({
153
171
  onClose();
154
172
  }
155
173
  } catch (error) {
156
- console.error('Switch session failed:', error);
174
+ if (__DEV__) {
175
+ console.error('Switch session failed:', error);
176
+ }
157
177
  toast.error(t('accountSwitcher.toasts.switchFailed') || 'There was a problem switching accounts. Please try again.');
158
178
  } finally {
159
179
  setSwitchingToUserId(null);
160
180
  }
161
- };
181
+ }, [activeSessionId, switchSession, onClose, t, switchingToUserId]);
182
+
183
+ const handleRemoveSession = useCallback(async (sessionId: string, displayName: string) => {
184
+ if (removingUserId) return; // Already removing
162
185
 
163
- const handleRemoveSession = async (sessionId: string, displayName: string) => {
164
186
  confirmAction(
165
187
  t('accountSwitcher.confirms.remove', { displayName }) || `Are you sure you want to remove ${displayName} from this device? You'll need to sign in again to access this account.`,
166
188
  async () => {
@@ -169,16 +191,18 @@ const ModernAccountSwitcherScreen: React.FC<BaseScreenProps> = ({
169
191
  await removeSession(sessionId);
170
192
  toast.success(t('accountSwitcher.toasts.removeSuccess') || 'Account removed successfully!');
171
193
  } catch (error) {
172
- console.error('Remove session failed:', error);
194
+ if (__DEV__) {
195
+ console.error('Remove session failed:', error);
196
+ }
173
197
  toast.error(t('accountSwitcher.toasts.removeFailed') || 'There was a problem removing the account. Please try again.');
174
198
  } finally {
175
199
  setRemovingUserId(null);
176
200
  }
177
201
  }
178
202
  );
179
- };
203
+ }, [removeSession, t, removingUserId]);
180
204
 
181
- const handleLogoutAll = async () => {
205
+ const handleLogoutAll = useCallback(() => {
182
206
  confirmAction(
183
207
  t('accountSwitcher.confirms.logoutAll') || 'Are you sure you want to sign out of all accounts? This will remove all saved accounts from this device.',
184
208
  async () => {
@@ -189,52 +213,58 @@ const ModernAccountSwitcherScreen: React.FC<BaseScreenProps> = ({
189
213
  onClose();
190
214
  }
191
215
  } catch (error) {
192
- console.error('Logout all failed:', error);
216
+ if (__DEV__) {
217
+ console.error('Logout all failed:', error);
218
+ }
193
219
  const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
194
220
  toast.error(t('accountSwitcher.toasts.signOutAllFailed', { error: errorMessage }) || `There was a problem signing out: ${errorMessage}`);
195
221
  }
196
222
  }
197
223
  );
198
- };
224
+ }, [logoutAll, onClose, t]);
199
225
 
200
- // Device session management functions
201
- const loadAllDeviceSessions = async () => {
226
+ // Device session management functions - optimized with useCallback
227
+ const loadAllDeviceSessions = useCallback(async () => {
202
228
  if (!oxyServices || !activeSessionId) return;
203
229
 
204
230
  setLoadingDeviceSessions(true);
205
231
  try {
206
- // This would call the API to get all device sessions for the current user
207
232
  const allSessions = await oxyServices.getDeviceSessions(activeSessionId);
208
233
  setDeviceSessions(allSessions || []);
209
234
  } catch (error) {
210
- console.error('Failed to load device sessions:', error);
235
+ if (__DEV__) {
236
+ console.error('Failed to load device sessions:', error);
237
+ }
211
238
  toast.error(t('accountSwitcher.toasts.deviceLoadFailed') || 'Failed to load device sessions. Please try again.');
212
239
  } finally {
213
240
  setLoadingDeviceSessions(false);
214
241
  }
215
- };
242
+ }, [oxyServices, activeSessionId, t]);
243
+
244
+ const handleRemoteSessionLogout = useCallback((sessionId: string, deviceName: string) => {
245
+ if (remotingLogoutSessionId) return; // Already processing
216
246
 
217
- const handleRemoteSessionLogout = async (sessionId: string, deviceName: string) => {
218
247
  confirmAction(
219
248
  t('accountSwitcher.confirms.remoteLogout', { deviceName }) || `Are you sure you want to sign out from "${deviceName}"? This will end the session on that device.`,
220
249
  async () => {
221
250
  setRemoteLogoutSessionId(sessionId);
222
251
  try {
223
252
  await oxyServices?.logoutSession(activeSessionId || '', sessionId);
224
- // Refresh device sessions list
225
253
  await loadAllDeviceSessions();
226
254
  toast.success(t('accountSwitcher.toasts.remoteSignOutSuccess', { deviceName }) || `Signed out from ${deviceName} successfully!`);
227
255
  } catch (error) {
228
- console.error('Remote logout failed:', error);
256
+ if (__DEV__) {
257
+ console.error('Remote logout failed:', error);
258
+ }
229
259
  toast.error(t('accountSwitcher.toasts.remoteSignOutFailed') || 'There was a problem signing out from the device. Please try again.');
230
260
  } finally {
231
261
  setRemoteLogoutSessionId(null);
232
262
  }
233
263
  }
234
264
  );
235
- };
265
+ }, [activeSessionId, oxyServices, loadAllDeviceSessions, t, remotingLogoutSessionId]);
236
266
 
237
- const handleLogoutAllDevices = async () => {
267
+ const handleLogoutAllDevices = useCallback(() => {
238
268
  const otherDevicesCount = deviceSessions.filter(session => !session.isCurrent).length;
239
269
 
240
270
  if (otherDevicesCount === 0) {
@@ -242,24 +272,33 @@ const ModernAccountSwitcherScreen: React.FC<BaseScreenProps> = ({
242
272
  return;
243
273
  }
244
274
 
275
+ if (loggingOutAllDevices) return; // Already processing
276
+
245
277
  confirmAction(
246
278
  t('accountSwitcher.confirms.logoutOthers', { count: otherDevicesCount }) || `Are you sure you want to sign out from all ${otherDevicesCount} other device(s)? This will end sessions on all other devices except this one.`,
247
279
  async () => {
248
280
  setLoggingOutAllDevices(true);
249
281
  try {
250
282
  await oxyServices?.logoutAllDeviceSessions(activeSessionId || '');
251
- // Refresh device sessions list
252
283
  await loadAllDeviceSessions();
253
284
  toast.success(t('accountSwitcher.toasts.signOutOthersSuccess') || 'Signed out from all other devices successfully!');
254
285
  } catch (error) {
255
- console.error('Logout all devices failed:', error);
286
+ if (__DEV__) {
287
+ console.error('Logout all devices failed:', error);
288
+ }
256
289
  toast.error(t('accountSwitcher.toasts.signOutOthersFailed') || 'There was a problem signing out from other devices. Please try again.');
257
290
  } finally {
258
291
  setLoggingOutAllDevices(false);
259
292
  }
260
293
  }
261
294
  );
262
- };
295
+ }, [deviceSessions, activeSessionId, oxyServices, loadAllDeviceSessions, t, loggingOutAllDevices]);
296
+
297
+ // Memoize filtered sessions for performance
298
+ const otherSessions = useMemo(
299
+ () => sessionsWithUsers.filter(s => s.sessionId !== activeSessionId),
300
+ [sessionsWithUsers, activeSessionId]
301
+ );
263
302
 
264
303
  return (
265
304
  <View style={[styles.container, { backgroundColor: '#f2f2f2' }]}>
@@ -274,10 +313,7 @@ const ModernAccountSwitcherScreen: React.FC<BaseScreenProps> = ({
274
313
  elevation="subtle"
275
314
  rightAction={{
276
315
  icon: "refresh",
277
- onPress: () => {
278
- console.log('Manual refresh triggered');
279
- refreshSessions();
280
- }
316
+ onPress: refreshSessions
281
317
  }}
282
318
  />
283
319
 
@@ -325,15 +361,13 @@ const ModernAccountSwitcherScreen: React.FC<BaseScreenProps> = ({
325
361
  )}
326
362
 
327
363
  {/* Other Accounts */}
328
- {sessionsWithUsers.filter(s => s.sessionId !== activeSessionId).length > 0 && (
364
+ {otherSessions.length > 0 && (
329
365
  <View style={styles.section}>
330
366
  <Text style={styles.sectionTitle}>
331
- {t('accountSwitcher.sections.otherWithCount', { count: sessionsWithUsers.filter(s => s.sessionId !== activeSessionId).length }) || `Other Accounts (${sessionsWithUsers.filter(s => s.sessionId !== activeSessionId).length})`}
367
+ {t('accountSwitcher.sections.otherWithCount', { count: otherSessions.length }) || `Other Accounts (${otherSessions.length})`}
332
368
  </Text>
333
369
 
334
- {sessionsWithUsers
335
- .filter(s => s.sessionId !== activeSessionId)
336
- .map((sessionWithUser, index, filteredArray) => {
370
+ {otherSessions.map((sessionWithUser, index, filteredArray) => {
337
371
  const isFirst = index === 0;
338
372
  const isLast = index === filteredArray.length - 1;
339
373
  const isSwitching = switchingToUserId === sessionWithUser.sessionId;
@@ -346,7 +380,7 @@ const ModernAccountSwitcherScreen: React.FC<BaseScreenProps> = ({
346
380
 
347
381
  return (
348
382
  <View
349
- key={sessionWithUser.sessionId}
383
+ key={`session-${sessionWithUser.sessionId}-${index}`}
350
384
  style={[
351
385
  styles.settingItem,
352
386
  isFirst && styles.firstSettingItem,
@@ -1974,7 +1974,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1974
1974
 
1975
1975
  {/* Uploading banner overlay */}
1976
1976
  {!selectMode && uploading && (
1977
- <View pointerEvents="none" style={styles.uploadBannerContainer}>
1977
+ <View style={[styles.uploadBannerContainer, { pointerEvents: 'none' }]}>
1978
1978
  <View style={[styles.uploadBanner, { backgroundColor: themeStyles.isDarkTheme ? '#222831EE' : '#FFFFFFEE', borderColor: themeStyles.borderColor }]}>
1979
1979
  <Ionicons name="cloud-upload" size={18} color={themeStyles.primaryColor} />
1980
1980
  <Text style={[styles.uploadBannerText, { color: themeStyles.textColor }]}>Uploading{uploadProgress ? ` ${uploadProgress.current}/${uploadProgress.total}` : '...'}</Text>