@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
@@ -1,5 +1,4 @@
1
- import type React from 'react';
2
- import { useState } from 'react';
1
+ import React, { useState, useMemo, useCallback } from 'react';
3
2
  import {
4
3
  View,
5
4
  Text,
@@ -15,101 +14,18 @@ import { Ionicons } from '@expo/vector-icons';
15
14
  import { toast } from '../../lib/sonner';
16
15
  import { Header, Section, GroupedSection } from '../components';
17
16
  import { useI18n } from '../hooks/useI18n';
18
-
19
- // Supported languages with their metadata
20
- const SUPPORTED_LANGUAGES = [
21
- {
22
- id: 'en-US',
23
- name: 'English',
24
- nativeName: 'English',
25
- flag: '🇺🇸',
26
- icon: 'language-outline',
27
- color: '#007AFF',
28
- },
29
- {
30
- id: 'es-ES',
31
- name: 'Spanish',
32
- nativeName: 'Español',
33
- flag: '🇪🇸',
34
- icon: 'language-outline',
35
- color: '#FF3B30',
36
- },
37
- {
38
- id: 'ca-ES',
39
- name: 'Catalan',
40
- nativeName: 'Català',
41
- flag: '🇪🇸',
42
- icon: 'language-outline',
43
- color: '#0CA678',
44
- },
45
- {
46
- id: 'fr-FR',
47
- name: 'French',
48
- nativeName: 'Français',
49
- flag: '🇫🇷',
50
- icon: 'language-outline',
51
- color: '#5856D6',
52
- },
53
- {
54
- id: 'de-DE',
55
- name: 'German',
56
- nativeName: 'Deutsch',
57
- flag: '🇩🇪',
58
- icon: 'language-outline',
59
- color: '#FF9500',
60
- },
61
- {
62
- id: 'it-IT',
63
- name: 'Italian',
64
- nativeName: 'Italiano',
65
- flag: '🇮🇹',
66
- icon: 'language-outline',
67
- color: '#34C759',
68
- },
69
- {
70
- id: 'pt-PT',
71
- name: 'Portuguese',
72
- nativeName: 'Português',
73
- flag: '🇵🇹',
74
- icon: 'language-outline',
75
- color: '#AF52DE',
76
- },
77
- {
78
- id: 'ja-JP',
79
- name: 'Japanese',
80
- nativeName: '日本語',
81
- flag: '🇯🇵',
82
- icon: 'language-outline',
83
- color: '#FF2D92',
84
- },
85
- {
86
- id: 'ko-KR',
87
- name: 'Korean',
88
- nativeName: '한국어',
89
- flag: '🇰🇷',
90
- icon: 'language-outline',
91
- color: '#32D74B',
92
- },
93
- {
94
- id: 'zh-CN',
95
- name: 'Chinese',
96
- nativeName: '中文',
97
- flag: '🇨🇳',
98
- icon: 'language-outline',
99
- color: '#FF9F0A',
100
- },
101
- {
102
- id: 'ar-SA',
103
- name: 'Arabic',
104
- nativeName: 'العربية',
105
- flag: '🇸🇦',
106
- icon: 'language-outline',
107
- color: '#30B0C7',
108
- },
109
- ];
17
+ import { SUPPORTED_LANGUAGES } from '../../utils/languageUtils';
110
18
 
111
19
  interface LanguageSelectorScreenProps extends BaseScreenProps { }
112
20
 
21
+ /**
22
+ * LanguageSelectorScreen - Optimized for performance
23
+ *
24
+ * Performance optimizations:
25
+ * - useMemo for language items to prevent recreation on every render
26
+ * - useCallback for handlers to prevent unnecessary re-renders
27
+ * - Memoized current language section
28
+ */
113
29
  const LanguageSelectorScreen: React.FC<LanguageSelectorScreenProps> = ({
114
30
  goBack,
115
31
  onClose,
@@ -121,20 +37,27 @@ const LanguageSelectorScreen: React.FC<LanguageSelectorScreenProps> = ({
121
37
  const colors = useThemeColors(theme);
122
38
  const [isLoading, setIsLoading] = useState(false);
123
39
 
124
- const handleLanguageSelect = async (languageId: string) => {
125
- if (languageId === currentLanguage) {
126
- return; // Already selected
40
+ // Memoize the language select handler to prevent recreation on every render
41
+ const handleLanguageSelect = useCallback(async (languageId: string) => {
42
+ if (languageId === currentLanguage || isLoading) {
43
+ return; // Already selected or loading
127
44
  }
128
45
 
129
46
  setIsLoading(true);
130
47
 
131
48
  try {
49
+ let serverSyncFailed = false;
50
+
132
51
  // If signed in, persist preference to backend user settings
133
52
  if (isAuthenticated && user?.id) {
134
53
  try {
135
54
  await oxyServices.updateProfile({ language: languageId });
136
55
  } catch (e: any) {
137
- console.warn('Failed to update language on server, falling back to local storage', e);
56
+ // Server sync failed, but we'll save locally anyway
57
+ serverSyncFailed = true;
58
+ if (__DEV__) {
59
+ console.warn('Failed to sync language to server (will save locally only):', e?.message || e);
60
+ }
138
61
  }
139
62
  }
140
63
 
@@ -142,34 +65,71 @@ const LanguageSelectorScreen: React.FC<LanguageSelectorScreenProps> = ({
142
65
  await setLanguage(languageId);
143
66
 
144
67
  const selectedLang = SUPPORTED_LANGUAGES.find(lang => lang.id === languageId);
68
+
69
+ // Show success message (language is saved locally regardless of server sync)
145
70
  toast.success(t('language.changed', { lang: selectedLang?.name || languageId }));
71
+
72
+ // Log server sync failure only in dev mode (user experience is still good - saved locally)
73
+ if (serverSyncFailed && __DEV__) {
74
+ console.warn('Language saved locally but server sync failed');
75
+ }
146
76
 
147
77
  setIsLoading(false);
148
78
  // Close the bottom sheet if possible; otherwise, go back
149
79
  if (onClose) onClose(); else goBack();
150
80
 
151
81
  } catch (error) {
82
+ // Only show error if local storage also failed
152
83
  console.error('Error saving language preference:', error);
153
84
  toast.error('Failed to save language preference');
154
85
  setIsLoading(false);
155
86
  }
156
- };
87
+ }, [currentLanguage, isLoading, isAuthenticated, user?.id, oxyServices, setLanguage, t, onClose, goBack]);
88
+
89
+ // Memoize language items to prevent recreation on every render
90
+ const languageItems = useMemo(() =>
91
+ SUPPORTED_LANGUAGES.map(language => ({
92
+ id: language.id,
93
+ title: language.name,
94
+ subtitle: language.nativeName,
95
+ customIcon: (
96
+ <View style={[styles.languageFlag, { backgroundColor: `${language.color}20` }]}>
97
+ <Text style={styles.flagEmoji}>{language.flag}</Text>
98
+ </View>
99
+ ),
100
+ iconColor: language.color,
101
+ selected: currentLanguage === language.id,
102
+ onPress: () => handleLanguageSelect(language.id),
103
+ dense: true,
104
+ })),
105
+ [currentLanguage, handleLanguageSelect]
106
+ );
157
107
 
158
- // Create grouped items for the language list
159
- const languageItems = SUPPORTED_LANGUAGES.map(language => ({
160
- id: language.id,
161
- title: language.name,
162
- subtitle: language.nativeName,
163
- customIcon: (
164
- <View style={[styles.languageFlag, { backgroundColor: `${language.color}20` }]}>
165
- <Text style={styles.flagEmoji}>{language.flag}</Text>
166
- </View>
167
- ),
168
- iconColor: language.color,
169
- selected: currentLanguage === language.id,
170
- onPress: () => handleLanguageSelect(language.id),
171
- dense: true,
172
- }));
108
+ // Memoize current language data to prevent recalculation
109
+ const currentLanguageData = useMemo(() => {
110
+ if (!currentLanguage) return null;
111
+ return SUPPORTED_LANGUAGES.find(lang => lang.id === currentLanguage);
112
+ }, [currentLanguage]);
113
+
114
+ // Memoize current language section items
115
+ const currentLanguageItems = useMemo(() => {
116
+ if (!currentLanguageData) return [];
117
+ return [{
118
+ id: `current-${currentLanguageData.id}`,
119
+ title: currentLanguageData.name,
120
+ subtitle: currentLanguageData.nativeName,
121
+ customIcon: (
122
+ <View style={[styles.languageFlag, { backgroundColor: `${currentLanguageData.color}20` }]}>
123
+ <Text style={styles.flagEmoji}>{currentLanguageData.flag}</Text>
124
+ </View>
125
+ ),
126
+ iconColor: currentLanguageData.color,
127
+ selected: false,
128
+ showChevron: false,
129
+ dense: true,
130
+ disabled: true,
131
+ }];
132
+ }, [currentLanguageData]);
173
133
 
174
134
  return (
175
135
  <View style={[styles.container, { backgroundColor: '#f2f2f2' }]}>
@@ -182,36 +142,18 @@ const LanguageSelectorScreen: React.FC<LanguageSelectorScreenProps> = ({
182
142
  elevation="subtle"
183
143
  />
184
144
 
185
- <ScrollView style={styles.content} showsVerticalScrollIndicator={false}>
145
+ <ScrollView
146
+ style={styles.content}
147
+ showsVerticalScrollIndicator={false}
148
+ removeClippedSubviews={true}
149
+ >
186
150
  {/* Current selection */}
187
- {currentLanguage && (
151
+ {currentLanguage && currentLanguageItems.length > 0 && (
188
152
  <Section title={t('language.current')} theme={theme} isFirst={true}>
189
- {(() => {
190
- const current = SUPPORTED_LANGUAGES.find(lang => lang.id === currentLanguage);
191
- if (!current) return null;
192
- return (
193
- <GroupedSection
194
- items={[
195
- {
196
- id: `current-${current.id}`,
197
- title: current.name,
198
- subtitle: current.nativeName,
199
- customIcon: (
200
- <View style={[styles.languageFlag, { backgroundColor: `${current.color}20` }]}>
201
- <Text style={styles.flagEmoji}>{current.flag}</Text>
202
- </View>
203
- ),
204
- iconColor: current.color,
205
- selected: false,
206
- showChevron: false,
207
- dense: true,
208
- disabled: true,
209
- },
210
- ]}
211
- theme={theme}
212
- />
213
- );
214
- })()}
153
+ <GroupedSection
154
+ items={currentLanguageItems}
155
+ theme={theme}
156
+ />
215
157
  </Section>
216
158
  )}
217
159
 
@@ -348,4 +290,5 @@ const styles = StyleSheet.create({
348
290
  },
349
291
  });
350
292
 
351
- export default LanguageSelectorScreen;
293
+ // Export memoized component to prevent unnecessary re-renders
294
+ export default React.memo(LanguageSelectorScreen);
@@ -32,13 +32,6 @@ const SignInScreen: React.FC<BaseScreenProps> = ({
32
32
  const [existingSession, setExistingSession] = useState<any>(null);
33
33
 
34
34
  const { login, completeMfaLogin, isLoading, user, isAuthenticated, sessions, oxyServices, switchSession } = useOxy();
35
-
36
- // Only log props in development mode to reduce console noise
37
- if (__DEV__) {
38
- console.log('SignInScreen props:', { initialStep, initialUsername, initialUserProfile });
39
- console.log('🔧 oxyServices available:', !!oxyServices);
40
- console.log('🔧 getProfileByUsername available:', typeof oxyServices?.getProfileByUsername);
41
- }
42
35
  const colors = useThemeColors(theme);
43
36
 
44
37
  // Check if this should be treated as "Add Account" mode
@@ -9,6 +9,7 @@ import SignUpWelcomeStep from './steps/SignUpWelcomeStep';
9
9
  import SignUpIdentityStep from './steps/SignUpIdentityStep';
10
10
  import SignUpSecurityStep from './steps/SignUpSecurityStep';
11
11
  import SignUpSummaryStep from './steps/SignUpSummaryStep';
12
+ import { TTLCache, registerCacheForCleanup } from '../../utils/cache';
12
13
 
13
14
  // Types for better type safety
14
15
  interface ValidationState {
@@ -51,8 +52,16 @@ const SignUpScreen: React.FC<BaseScreenProps> = ({
51
52
  // Error message state
52
53
  const [errorMessage, setErrorMessage] = useState('');
53
54
 
54
- // Username validation with caching
55
- const usernameCache = useRef<Map<string, { available: boolean; timestamp: number }>>(new Map());
55
+ // Username validation with caching - uses centralized cache
56
+ const usernameCache = useRef(new TTLCache<boolean>(5 * 60 * 1000)); // 5 minutes cache
57
+
58
+ // Register cache for cleanup on mount
59
+ useEffect(() => {
60
+ registerCacheForCleanup(usernameCache.current);
61
+ return () => {
62
+ usernameCache.current.clear();
63
+ };
64
+ }, []);
56
65
 
57
66
  const validateUsername = useCallback(async (usernameToValidate: string): Promise<boolean> => {
58
67
  if (!usernameToValidate || usernameToValidate.length < USERNAME_MIN_LENGTH) {
@@ -62,9 +71,8 @@ const SignUpScreen: React.FC<BaseScreenProps> = ({
62
71
 
63
72
  // Check cache first
64
73
  const cached = usernameCache.current.get(usernameToValidate);
65
- const now = Date.now();
66
- if (cached && (now - cached.timestamp) < 5 * 60 * 1000) {
67
- const isValid = cached.available;
74
+ if (cached !== null) {
75
+ const isValid = cached;
68
76
  setValidationState({
69
77
  status: isValid ? 'valid' : 'invalid',
70
78
  message: isValid ? '' : 'Username is already taken'
@@ -79,10 +87,7 @@ const SignUpScreen: React.FC<BaseScreenProps> = ({
79
87
  const isValid = result.available;
80
88
 
81
89
  // Cache the result
82
- usernameCache.current.set(usernameToValidate, {
83
- available: isValid,
84
- timestamp: now
85
- });
90
+ usernameCache.current.set(usernameToValidate, isValid);
86
91
 
87
92
  setValidationState({
88
93
  status: isValid ? 'valid' : 'invalid',
@@ -146,12 +151,6 @@ const SignUpScreen: React.FC<BaseScreenProps> = ({
146
151
  }
147
152
  }, [username, email, password, confirmPassword, validateEmail, validatePassword, signUp, navigate]);
148
153
 
149
- // Cleanup cache on unmount
150
- useEffect(() => {
151
- return () => {
152
- usernameCache.current.clear();
153
- };
154
- }, []);
155
154
 
156
155
  // Step configurations
157
156
  const steps: StepConfig[] = useMemo(() => [
@@ -69,8 +69,19 @@ const WelcomeNewUserScreen: React.FC<BaseScreenProps & { newUser?: any }> = ({
69
69
  const fadeAnim = useRef(new Animated.Value(1)).current;
70
70
  const slideAnim = useRef(new Animated.Value(0)).current;
71
71
  const [currentStep, setCurrentStep] = useState(0);
72
+ // Track avatar separately to ensure it updates immediately after selection
73
+ const [selectedAvatarId, setSelectedAvatarId] = useState<string | undefined>(currentUser?.avatar);
72
74
 
73
- const avatarUri = currentUser?.avatar ? oxyServices.getFileDownloadUrl(currentUser.avatar as string, 'thumb') : undefined;
75
+ // Update selectedAvatarId when user changes
76
+ useEffect(() => {
77
+ if (user?.avatar) {
78
+ setSelectedAvatarId(user.avatar);
79
+ } else if (newUser?.avatar) {
80
+ setSelectedAvatarId(newUser.avatar);
81
+ }
82
+ }, [user?.avatar, newUser?.avatar]);
83
+
84
+ const avatarUri = selectedAvatarId ? oxyServices.getFileDownloadUrl(selectedAvatarId, 'thumb') : undefined;
74
85
 
75
86
  // Steps content
76
87
  const welcomeTitle = currentUser?.username
@@ -100,7 +111,7 @@ const WelcomeNewUserScreen: React.FC<BaseScreenProps & { newUser?: any }> = ({
100
111
  const totalSteps = steps.length;
101
112
  const avatarStepIndex = steps.findIndex(s => s.showAvatar);
102
113
 
103
- const animateToStep = (next: number) => {
114
+ const animateToStepCallback = useCallback((next: number) => {
104
115
  Animated.timing(fadeAnim, { toValue: 0, duration: 180, useNativeDriver: Platform.OS !== 'web' }).start(() => {
105
116
  setCurrentStep(next);
106
117
  slideAnim.setValue(-40);
@@ -109,40 +120,59 @@ const WelcomeNewUserScreen: React.FC<BaseScreenProps & { newUser?: any }> = ({
109
120
  Animated.spring(slideAnim, { toValue: 0, useNativeDriver: Platform.OS !== 'web', friction: 9 })
110
121
  ]).start();
111
122
  });
112
- };
123
+ }, [fadeAnim, slideAnim]);
113
124
 
114
- const nextStep = useCallback(() => { if (currentStep < totalSteps - 1) animateToStep(currentStep + 1); }, [currentStep, totalSteps]);
115
- const prevStep = useCallback(() => { if (currentStep > 0) animateToStep(currentStep - 1); }, [currentStep]);
116
- const skipToAvatar = useCallback(() => { if (avatarStepIndex >= 0) animateToStep(avatarStepIndex); }, [avatarStepIndex]);
125
+ const nextStep = useCallback(() => { if (currentStep < totalSteps - 1) animateToStepCallback(currentStep + 1); }, [currentStep, totalSteps, animateToStepCallback]);
126
+ const prevStep = useCallback(() => { if (currentStep > 0) animateToStepCallback(currentStep - 1); }, [currentStep, animateToStepCallback]);
127
+ const skipToAvatar = useCallback(() => { if (avatarStepIndex >= 0) animateToStepCallback(avatarStepIndex); }, [avatarStepIndex, animateToStepCallback]);
117
128
  const finish = useCallback(() => { if (onAuthenticated && currentUser) onAuthenticated(currentUser); }, [onAuthenticated, currentUser]);
118
129
  const openAvatarPicker = useCallback(() => {
130
+ // Ensure we're on the avatar step before opening picker
131
+ if (avatarStepIndex >= 0 && currentStep !== avatarStepIndex) {
132
+ animateToStepCallback(avatarStepIndex);
133
+ }
134
+
119
135
  navigate('FileManagement', {
120
136
  selectMode: true,
121
137
  multiSelect: false,
122
138
  disabledMimeTypes: ['video/', 'audio/', 'application/pdf'],
123
- afterSelect: 'back',
139
+ afterSelect: 'none', // Don't navigate away - stay on current screen
124
140
  onSelect: async (file: any) => {
125
141
  if (!file.contentType.startsWith('image/')) {
126
142
  toast.error(t('editProfile.toasts.selectImage') || 'Please select an image file');
127
143
  return;
128
144
  }
129
145
  try {
130
- // Update file visibility to public for avatar
131
- try {
132
- await oxyServices.assetUpdateVisibility(file.id, 'public');
133
- console.log('[WelcomeNewUser] Avatar visibility updated to public');
134
- } catch (visError) {
135
- console.warn('[WelcomeNewUser] Failed to update avatar visibility, continuing anyway:', visError);
146
+ // Update file visibility to public for avatar (skip if temporary asset ID)
147
+ if (file.id && !file.id.startsWith('temp-')) {
148
+ try {
149
+ await oxyServices.assetUpdateVisibility(file.id, 'public');
150
+ console.log('[WelcomeNewUser] Avatar visibility updated to public');
151
+ } catch (visError: any) {
152
+ // Only log non-404 errors (404 means asset doesn't exist yet, which is OK)
153
+ if (visError?.response?.status !== 404) {
154
+ console.warn('[WelcomeNewUser] Failed to update avatar visibility, continuing anyway:', visError);
155
+ }
156
+ }
136
157
  }
137
158
 
159
+ // Update the avatar immediately in local state
160
+ setSelectedAvatarId(file.id);
161
+
162
+ // Update user in store
138
163
  await updateUser({ avatar: file.id }, oxyServices);
139
164
  toast.success(t('editProfile.toasts.avatarUpdated') || 'Avatar updated');
165
+
166
+ // Ensure we stay on the avatar step
167
+ if (avatarStepIndex >= 0 && currentStep !== avatarStepIndex) {
168
+ animateToStepCallback(avatarStepIndex);
169
+ }
140
170
  } catch (e: any) {
141
171
  toast.error(e.message || t('editProfile.toasts.updateAvatarFailed') || 'Failed to update avatar');
142
172
  }
143
173
  }
144
174
  });
145
- }, [navigate, updateUser, oxyServices]);
175
+ }, [navigate, updateUser, oxyServices, currentStep, avatarStepIndex, animateToStepCallback, t]);
146
176
 
147
177
  const step = steps[currentStep];
148
178
  const pillButtons = useMemo(() => {
@@ -201,7 +231,14 @@ const WelcomeNewUserScreen: React.FC<BaseScreenProps & { newUser?: any }> = ({
201
231
  )}
202
232
  {step.showAvatar && (
203
233
  <View style={[styles.avatarSection, styles.sectionSpacing]}>
204
- <Avatar size={120} name={currentUser?.username} uri={avatarUri} theme={theme} style={styles.avatar} />
234
+ <Avatar
235
+ size={120}
236
+ name={currentUser?.name?.full || currentUser?.name?.first || currentUser?.username}
237
+ uri={avatarUri}
238
+ theme={theme}
239
+ backgroundColor={colors.primary + '20'}
240
+ style={styles.avatar}
241
+ />
205
242
  <TouchableOpacity style={[styles.changeAvatarButton, { backgroundColor: colors.primary }]} onPress={openAvatarPicker}>
206
243
  <Ionicons name="image-outline" size={18} color="#FFFFFF" />
207
244
  <Text style={styles.changeAvatarText}>{avatarUri ? (t('welcomeNew.avatar.change') || 'Change Avatar') : (t('welcomeNew.avatar.add') || 'Add Avatar')}</Text>
@@ -9,6 +9,7 @@ import Animated, {
9
9
  import { Ionicons } from '@expo/vector-icons';
10
10
  import Avatar from '../../components/Avatar';
11
11
  import { useI18n } from '../../hooks/useI18n';
12
+ import { useOxy } from '../../context/OxyContext';
12
13
  import GroupedPillButtons from '../../components/internal/GroupedPillButtons';
13
14
  import TextField from '../../components/internal/TextField';
14
15
 
@@ -59,6 +60,7 @@ const SignInPasswordStep: React.FC<SignInPasswordStepProps> = ({
59
60
  }) => {
60
61
  const inputRef = useRef<TextInput>(null);
61
62
  const { t } = useI18n();
63
+ const { oxyServices } = useOxy();
62
64
 
63
65
  // Animated styles - properly memoized to prevent re-renders
64
66
  const containerAnimatedStyle = useAnimatedStyle(() => {
@@ -101,18 +103,14 @@ const SignInPasswordStep: React.FC<SignInPasswordStepProps> = ({
101
103
  containerAnimatedStyle
102
104
  ]}>
103
105
  <View style={styles.modernUserProfileContainer}>
104
- <Animated.View style={[
105
- styles.avatarContainer,
106
- logoAnimatedStyle
107
- ]}>
106
+ <Animated.View style={logoAnimatedStyle}>
108
107
  <Avatar
109
108
  name={userProfile?.displayName || userProfile?.name || username}
110
109
  size={100}
111
110
  theme={theme as 'light' | 'dark'}
112
- style={styles.modernUserAvatar}
113
111
  backgroundColor={colors.primary + '20'}
112
+ uri={userProfile?.avatar && oxyServices ? oxyServices.getFileDownloadUrl(userProfile.avatar, 'thumb') : undefined}
114
113
  />
115
- <View style={[styles.statusIndicator, { backgroundColor: colors.primary }]} />
116
114
  </Animated.View>
117
115
  <Text style={[styles.modernUserDisplayName, { color: colors.text }]}>
118
116
  {userProfile?.displayName || userProfile?.name || username}
@@ -6,6 +6,7 @@ import Avatar from '../../components/Avatar';
6
6
  import GroupedPillButtons from '../../components/internal/GroupedPillButtons';
7
7
  import TextField from '../../components/internal/TextField';
8
8
  import { useI18n } from '../../hooks/useI18n';
9
+ import { useOxy } from '../../context/OxyContext';
9
10
  import { STEP_GAP, STEP_INNER_GAP, stepStyles } from '../../styles/spacing';
10
11
 
11
12
  interface SignInPasswordStepProps {
@@ -69,6 +70,7 @@ const SignInPasswordStep: React.FC<SignInPasswordStepProps> = ({
69
70
  }) => {
70
71
  const inputRef = useRef<any>(null);
71
72
  const { t } = useI18n();
73
+ const { oxyServices } = useOxy();
72
74
  const baseStyles = stepStyles;
73
75
  const webShadowReset = Platform.OS === 'web' ? ({ boxShadow: 'none' } as any) : null;
74
76
 
@@ -114,16 +116,13 @@ const SignInPasswordStep: React.FC<SignInPasswordStepProps> = ({
114
116
  return (
115
117
  <>
116
118
  <View style={[baseStyles.container, baseStyles.sectionSpacing, stylesheet.userProfileContainer]}>
117
- <View style={stylesheet.avatarContainer}>
118
119
  <Avatar
119
120
  name={userProfile?.displayName || userProfile?.name || username}
120
121
  size={100}
121
122
  theme={theme as 'light' | 'dark'}
122
- style={styles.modernUserAvatar}
123
123
  backgroundColor={colors.primary + '20'}
124
+ uri={userProfile?.avatar && oxyServices ? oxyServices.getFileDownloadUrl(userProfile.avatar, 'thumb') : undefined}
124
125
  />
125
- <View style={[styles.statusIndicator, { backgroundColor: colors.primary }]} />
126
- </View>
127
126
  <Text style={[styles.modernUserDisplayName, stylesheet.displayName, { color: colors.text, marginBottom: 0, marginTop: 0 }]}>
128
127
  {userProfile?.displayName || userProfile?.name || username}
129
128
  </Text>
@@ -186,16 +185,13 @@ const SignInPasswordStep: React.FC<SignInPasswordStepProps> = ({
186
185
  return (
187
186
  <>
188
187
  <View style={[baseStyles.container, baseStyles.sectionSpacing, stylesheet.userProfileContainer]}>
189
- <View style={stylesheet.avatarContainer}>
190
188
  <Avatar
191
189
  name={userProfile?.displayName || userProfile?.name || username}
192
190
  size={100}
193
191
  theme={theme as 'light' | 'dark'}
194
- style={styles.modernUserAvatar}
195
192
  backgroundColor={colors.primary + '20'}
193
+ uri={userProfile?.avatar && oxyServices ? oxyServices.getFileDownloadUrl(userProfile.avatar, 'thumb') : undefined}
196
194
  />
197
- <View style={[styles.statusIndicator, { backgroundColor: colors.primary }]} />
198
- </View>
199
195
  <Text style={[styles.modernUserDisplayName, stylesheet.displayName, { color: colors.text, marginBottom: 0, marginTop: 0 }]}>
200
196
  {userProfile?.displayName || userProfile?.name || username}
201
197
  </Text>