@oxyhq/services 5.12.11 → 5.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/lib/commonjs/core/OxyServices.js +86 -8
  2. package/lib/commonjs/core/OxyServices.js.map +1 -1
  3. package/lib/commonjs/i18n/index.js +37 -1
  4. package/lib/commonjs/i18n/index.js.map +1 -1
  5. package/lib/commonjs/i18n/locales/ar-SA.json +128 -0
  6. package/lib/commonjs/i18n/locales/ca-ES.json +128 -0
  7. package/lib/commonjs/i18n/locales/de-DE.json +128 -0
  8. package/lib/commonjs/i18n/locales/en-US.json +85 -12
  9. package/lib/commonjs/i18n/locales/es-ES.json +58 -6
  10. package/lib/commonjs/i18n/locales/fr-FR.json +128 -0
  11. package/lib/commonjs/i18n/locales/it-IT.json +128 -0
  12. package/lib/commonjs/i18n/locales/ja-JP.json +127 -0
  13. package/lib/commonjs/i18n/locales/ko-KR.json +128 -0
  14. package/lib/commonjs/i18n/locales/pt-PT.json +128 -0
  15. package/lib/commonjs/i18n/locales/zh-CN.json +128 -0
  16. package/lib/commonjs/ui/components/FontLoader.js +22 -42
  17. package/lib/commonjs/ui/components/FontLoader.js.map +1 -1
  18. package/lib/commonjs/ui/components/OxyProvider.js +5 -8
  19. package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
  20. package/lib/commonjs/ui/components/StepBasedScreen.js +64 -44
  21. package/lib/commonjs/ui/components/StepBasedScreen.js.map +1 -1
  22. package/lib/commonjs/ui/components/internal/GroupedPillButtons.js +14 -35
  23. package/lib/commonjs/ui/components/internal/GroupedPillButtons.js.map +1 -1
  24. package/lib/commonjs/ui/components/internal/PinInput.js +2 -2
  25. package/lib/commonjs/ui/components/internal/PinInput.js.map +1 -1
  26. package/lib/commonjs/ui/context/OxyContext.js +434 -321
  27. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  28. package/lib/commonjs/ui/screens/FileManagementScreen.js +56 -5
  29. package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
  30. package/lib/commonjs/ui/screens/SignInScreen.js +43 -39
  31. package/lib/commonjs/ui/screens/SignInScreen.js.map +1 -1
  32. package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js +139 -125
  33. package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js.map +1 -1
  34. package/lib/commonjs/ui/screens/internal/SignInUsernameStep.js +2 -4
  35. package/lib/commonjs/ui/screens/internal/SignInUsernameStep.js.map +1 -1
  36. package/lib/commonjs/ui/screens/steps/RecoverRequestStep.js +45 -25
  37. package/lib/commonjs/ui/screens/steps/RecoverRequestStep.js.map +1 -1
  38. package/lib/commonjs/ui/screens/steps/RecoverResetPasswordStep.js +88 -53
  39. package/lib/commonjs/ui/screens/steps/RecoverResetPasswordStep.js.map +1 -1
  40. package/lib/commonjs/ui/screens/steps/RecoverSuccessStep.js +79 -58
  41. package/lib/commonjs/ui/screens/steps/RecoverSuccessStep.js.map +1 -1
  42. package/lib/commonjs/ui/screens/steps/RecoverVerifyStep.js +61 -52
  43. package/lib/commonjs/ui/screens/steps/RecoverVerifyStep.js.map +1 -1
  44. package/lib/commonjs/ui/screens/steps/SignInPasswordStep.js +220 -31
  45. package/lib/commonjs/ui/screens/steps/SignInPasswordStep.js.map +1 -1
  46. package/lib/commonjs/ui/screens/steps/SignInTotpStep.js +77 -50
  47. package/lib/commonjs/ui/screens/steps/SignInTotpStep.js.map +1 -1
  48. package/lib/commonjs/ui/screens/steps/SignInUsernameStep.js +527 -66
  49. package/lib/commonjs/ui/screens/steps/SignInUsernameStep.js.map +1 -1
  50. package/lib/commonjs/ui/screens/steps/SignUpIdentityStep.js +55 -30
  51. package/lib/commonjs/ui/screens/steps/SignUpIdentityStep.js.map +1 -1
  52. package/lib/commonjs/ui/screens/steps/SignUpSecurityStep.js +64 -46
  53. package/lib/commonjs/ui/screens/steps/SignUpSecurityStep.js.map +1 -1
  54. package/lib/commonjs/ui/screens/steps/SignUpSummaryStep.js +84 -146
  55. package/lib/commonjs/ui/screens/steps/SignUpSummaryStep.js.map +1 -1
  56. package/lib/commonjs/ui/screens/steps/SignUpWelcomeStep.js +113 -34
  57. package/lib/commonjs/ui/screens/steps/SignUpWelcomeStep.js.map +1 -1
  58. package/lib/commonjs/ui/stores/authStore.js +16 -20
  59. package/lib/commonjs/ui/stores/authStore.js.map +1 -1
  60. package/lib/commonjs/ui/styles/authStyles.js +2 -1
  61. package/lib/commonjs/ui/styles/authStyles.js.map +1 -1
  62. package/lib/commonjs/ui/styles/index.js +11 -0
  63. package/lib/commonjs/ui/styles/index.js.map +1 -1
  64. package/lib/commonjs/ui/styles/spacing.js +51 -0
  65. package/lib/commonjs/ui/styles/spacing.js.map +1 -0
  66. package/lib/commonjs/utils/validationUtils.js +1 -1
  67. package/lib/module/core/OxyServices.js +86 -8
  68. package/lib/module/core/OxyServices.js.map +1 -1
  69. package/lib/module/i18n/index.js +37 -1
  70. package/lib/module/i18n/index.js.map +1 -1
  71. package/lib/module/i18n/locales/ar-SA.json +128 -0
  72. package/lib/module/i18n/locales/ca-ES.json +128 -0
  73. package/lib/module/i18n/locales/de-DE.json +128 -0
  74. package/lib/module/i18n/locales/en-US.json +85 -12
  75. package/lib/module/i18n/locales/es-ES.json +58 -6
  76. package/lib/module/i18n/locales/fr-FR.json +128 -0
  77. package/lib/module/i18n/locales/it-IT.json +128 -0
  78. package/lib/module/i18n/locales/ja-JP.json +127 -0
  79. package/lib/module/i18n/locales/ko-KR.json +128 -0
  80. package/lib/module/i18n/locales/pt-PT.json +128 -0
  81. package/lib/module/i18n/locales/zh-CN.json +128 -0
  82. package/lib/module/ui/components/FontLoader.js +23 -43
  83. package/lib/module/ui/components/FontLoader.js.map +1 -1
  84. package/lib/module/ui/components/OxyProvider.js +6 -8
  85. package/lib/module/ui/components/OxyProvider.js.map +1 -1
  86. package/lib/module/ui/components/StepBasedScreen.js +65 -45
  87. package/lib/module/ui/components/StepBasedScreen.js.map +1 -1
  88. package/lib/module/ui/components/internal/GroupedPillButtons.js +14 -35
  89. package/lib/module/ui/components/internal/GroupedPillButtons.js.map +1 -1
  90. package/lib/module/ui/components/internal/PinInput.js +2 -2
  91. package/lib/module/ui/components/internal/PinInput.js.map +1 -1
  92. package/lib/module/ui/context/OxyContext.js +434 -321
  93. package/lib/module/ui/context/OxyContext.js.map +1 -1
  94. package/lib/module/ui/screens/FileManagementScreen.js +56 -5
  95. package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
  96. package/lib/module/ui/screens/SignInScreen.js +44 -40
  97. package/lib/module/ui/screens/SignInScreen.js.map +1 -1
  98. package/lib/module/ui/screens/WelcomeNewUserScreen.js +138 -126
  99. package/lib/module/ui/screens/WelcomeNewUserScreen.js.map +1 -1
  100. package/lib/module/ui/screens/internal/SignInUsernameStep.js +2 -4
  101. package/lib/module/ui/screens/internal/SignInUsernameStep.js.map +1 -1
  102. package/lib/module/ui/screens/steps/RecoverRequestStep.js +45 -25
  103. package/lib/module/ui/screens/steps/RecoverRequestStep.js.map +1 -1
  104. package/lib/module/ui/screens/steps/RecoverResetPasswordStep.js +89 -54
  105. package/lib/module/ui/screens/steps/RecoverResetPasswordStep.js.map +1 -1
  106. package/lib/module/ui/screens/steps/RecoverSuccessStep.js +80 -59
  107. package/lib/module/ui/screens/steps/RecoverSuccessStep.js.map +1 -1
  108. package/lib/module/ui/screens/steps/RecoverVerifyStep.js +62 -53
  109. package/lib/module/ui/screens/steps/RecoverVerifyStep.js.map +1 -1
  110. package/lib/module/ui/screens/steps/SignInPasswordStep.js +221 -32
  111. package/lib/module/ui/screens/steps/SignInPasswordStep.js.map +1 -1
  112. package/lib/module/ui/screens/steps/SignInTotpStep.js +78 -51
  113. package/lib/module/ui/screens/steps/SignInTotpStep.js.map +1 -1
  114. package/lib/module/ui/screens/steps/SignInUsernameStep.js +530 -68
  115. package/lib/module/ui/screens/steps/SignInUsernameStep.js.map +1 -1
  116. package/lib/module/ui/screens/steps/SignUpIdentityStep.js +55 -30
  117. package/lib/module/ui/screens/steps/SignUpIdentityStep.js.map +1 -1
  118. package/lib/module/ui/screens/steps/SignUpSecurityStep.js +65 -47
  119. package/lib/module/ui/screens/steps/SignUpSecurityStep.js.map +1 -1
  120. package/lib/module/ui/screens/steps/SignUpSummaryStep.js +84 -146
  121. package/lib/module/ui/screens/steps/SignUpSummaryStep.js.map +1 -1
  122. package/lib/module/ui/screens/steps/SignUpWelcomeStep.js +114 -35
  123. package/lib/module/ui/screens/steps/SignUpWelcomeStep.js.map +1 -1
  124. package/lib/module/ui/stores/authStore.js +16 -20
  125. package/lib/module/ui/stores/authStore.js.map +1 -1
  126. package/lib/module/ui/styles/authStyles.js +2 -1
  127. package/lib/module/ui/styles/authStyles.js.map +1 -1
  128. package/lib/module/ui/styles/index.js +1 -0
  129. package/lib/module/ui/styles/index.js.map +1 -1
  130. package/lib/module/ui/styles/spacing.js +48 -0
  131. package/lib/module/ui/styles/spacing.js.map +1 -0
  132. package/lib/module/utils/validationUtils.js +1 -1
  133. package/lib/typescript/core/OxyServices.d.ts +38 -2
  134. package/lib/typescript/core/OxyServices.d.ts.map +1 -1
  135. package/lib/typescript/i18n/index.d.ts.map +1 -1
  136. package/lib/typescript/ui/components/FontLoader.d.ts +3 -3
  137. package/lib/typescript/ui/components/FontLoader.d.ts.map +1 -1
  138. package/lib/typescript/ui/components/OxyProvider.d.ts +2 -2
  139. package/lib/typescript/ui/components/OxyProvider.d.ts.map +1 -1
  140. package/lib/typescript/ui/components/StepBasedScreen.d.ts.map +1 -1
  141. package/lib/typescript/ui/components/internal/GroupedPillButtons.d.ts.map +1 -1
  142. package/lib/typescript/ui/context/OxyContext.d.ts +1 -0
  143. package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
  144. package/lib/typescript/ui/screens/FileManagementScreen.d.ts +10 -0
  145. package/lib/typescript/ui/screens/FileManagementScreen.d.ts.map +1 -1
  146. package/lib/typescript/ui/screens/SignInScreen.d.ts.map +1 -1
  147. package/lib/typescript/ui/screens/WelcomeNewUserScreen.d.ts.map +1 -1
  148. package/lib/typescript/ui/screens/steps/RecoverRequestStep.d.ts.map +1 -1
  149. package/lib/typescript/ui/screens/steps/RecoverResetPasswordStep.d.ts.map +1 -1
  150. package/lib/typescript/ui/screens/steps/RecoverSuccessStep.d.ts.map +1 -1
  151. package/lib/typescript/ui/screens/steps/RecoverVerifyStep.d.ts.map +1 -1
  152. package/lib/typescript/ui/screens/steps/SignInPasswordStep.d.ts +2 -0
  153. package/lib/typescript/ui/screens/steps/SignInPasswordStep.d.ts.map +1 -1
  154. package/lib/typescript/ui/screens/steps/SignInTotpStep.d.ts.map +1 -1
  155. package/lib/typescript/ui/screens/steps/SignInUsernameStep.d.ts.map +1 -1
  156. package/lib/typescript/ui/screens/steps/SignUpIdentityStep.d.ts.map +1 -1
  157. package/lib/typescript/ui/screens/steps/SignUpSecurityStep.d.ts.map +1 -1
  158. package/lib/typescript/ui/screens/steps/SignUpSummaryStep.d.ts.map +1 -1
  159. package/lib/typescript/ui/screens/steps/SignUpWelcomeStep.d.ts.map +1 -1
  160. package/lib/typescript/ui/stores/authStore.d.ts +7 -3
  161. package/lib/typescript/ui/stores/authStore.d.ts.map +1 -1
  162. package/lib/typescript/ui/styles/authStyles.d.ts +1 -0
  163. package/lib/typescript/ui/styles/authStyles.d.ts.map +1 -1
  164. package/lib/typescript/ui/styles/index.d.ts +1 -0
  165. package/lib/typescript/ui/styles/index.d.ts.map +1 -1
  166. package/lib/typescript/ui/styles/spacing.d.ts +43 -0
  167. package/lib/typescript/ui/styles/spacing.d.ts.map +1 -0
  168. package/lib/typescript/utils/validationUtils.d.ts +1 -1
  169. package/package.json +1 -1
  170. package/src/core/OxyServices.ts +96 -10
  171. package/src/i18n/index.ts +36 -0
  172. package/src/i18n/locales/ar-SA.json +128 -0
  173. package/src/i18n/locales/ca-ES.json +128 -0
  174. package/src/i18n/locales/de-DE.json +128 -0
  175. package/src/i18n/locales/en-US.json +85 -12
  176. package/src/i18n/locales/es-ES.json +58 -6
  177. package/src/i18n/locales/fr-FR.json +128 -0
  178. package/src/i18n/locales/it-IT.json +128 -0
  179. package/src/i18n/locales/ja-JP.json +127 -0
  180. package/src/i18n/locales/ko-KR.json +128 -0
  181. package/src/i18n/locales/pt-PT.json +128 -0
  182. package/src/i18n/locales/zh-CN.json +128 -0
  183. package/src/ui/components/FontLoader.tsx +17 -37
  184. package/src/ui/components/OxyProvider.tsx +14 -13
  185. package/src/ui/components/StepBasedScreen.tsx +66 -43
  186. package/src/ui/components/internal/GroupedPillButtons.tsx +15 -31
  187. package/src/ui/components/internal/PinInput.tsx +2 -2
  188. package/src/ui/context/OxyContext.tsx +404 -285
  189. package/src/ui/screens/FileManagementScreen.tsx +81 -4
  190. package/src/ui/screens/SignInScreen.tsx +59 -36
  191. package/src/ui/screens/WelcomeNewUserScreen.tsx +102 -91
  192. package/src/ui/screens/internal/SignInUsernameStep.tsx +1 -1
  193. package/src/ui/screens/steps/RecoverRequestStep.tsx +34 -24
  194. package/src/ui/screens/steps/RecoverResetPasswordStep.tsx +65 -36
  195. package/src/ui/screens/steps/RecoverSuccessStep.tsx +71 -47
  196. package/src/ui/screens/steps/RecoverVerifyStep.tsx +60 -50
  197. package/src/ui/screens/steps/SignInPasswordStep.tsx +191 -29
  198. package/src/ui/screens/steps/SignInTotpStep.tsx +68 -34
  199. package/src/ui/screens/steps/SignInUsernameStep.tsx +586 -57
  200. package/src/ui/screens/steps/SignUpIdentityStep.tsx +49 -35
  201. package/src/ui/screens/steps/SignUpSecurityStep.tsx +56 -39
  202. package/src/ui/screens/steps/SignUpSummaryStep.tsx +99 -89
  203. package/src/ui/screens/steps/SignUpWelcomeStep.tsx +88 -20
  204. package/src/ui/stores/authStore.ts +15 -19
  205. package/src/ui/styles/authStyles.ts +2 -1
  206. package/src/ui/styles/index.ts +1 -0
  207. package/src/ui/styles/spacing.ts +46 -0
  208. package/src/utils/validationUtils.ts +1 -1
@@ -55,6 +55,16 @@ export interface FileManagementScreenProps extends BaseScreenProps {
55
55
  * Useful for third-party apps that want files to be public (e.g., GIF selector)
56
56
  */
57
57
  defaultVisibility?: 'private' | 'public' | 'unlisted';
58
+ /**
59
+ * Link context for tracking file usage by third-party apps
60
+ * When provided, selected files will be linked to this entity
61
+ */
62
+ linkContext?: {
63
+ app: string; // App identifier (e.g., 'chat-app', 'post-composer')
64
+ entityType: string; // Type of entity (e.g., 'message', 'post', 'profile')
65
+ entityId: string; // Unique ID of the entity using this file
66
+ webhookUrl?: string; // Optional webhook URL to receive file events
67
+ };
58
68
  }
59
69
 
60
70
  // Add this helper function near the top (after imports):
@@ -79,6 +89,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
79
89
  afterSelect = 'close',
80
90
  allowUploadInSelectMode = true,
81
91
  defaultVisibility = 'private',
92
+ linkContext,
82
93
  }) => {
83
94
  const { user, oxyServices } = useOxy();
84
95
 
@@ -139,7 +150,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
139
150
  }
140
151
  }, [initialSelectedIds]);
141
152
 
142
- const toggleSelect = useCallback((file: FileMetadata) => {
153
+ const toggleSelect = useCallback(async (file: FileMetadata) => {
143
154
  if (!selectMode) return;
144
155
  if (disabledMimeTypes.length) {
145
156
  const blocked = disabledMimeTypes.some(mt => file.contentType === mt || file.contentType.startsWith(mt.endsWith('/') ? mt : mt + '/'));
@@ -148,6 +159,37 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
148
159
  return;
149
160
  }
150
161
  }
162
+
163
+ // Update file visibility if it differs from defaultVisibility
164
+ const fileVisibility = (file.metadata as any)?.visibility || 'private';
165
+ if (fileVisibility !== defaultVisibility) {
166
+ try {
167
+ await oxyServices.assetUpdateVisibility(file.id, defaultVisibility);
168
+ console.log(`Updated file ${file.id} visibility from ${fileVisibility} to ${defaultVisibility}`);
169
+ } catch (error) {
170
+ console.error('Failed to update file visibility:', error);
171
+ // Continue anyway - selection shouldn't fail if visibility update fails
172
+ }
173
+ }
174
+
175
+ // Link file to entity if linkContext is provided
176
+ if (linkContext) {
177
+ try {
178
+ await oxyServices.assetLink(
179
+ file.id,
180
+ linkContext.app,
181
+ linkContext.entityType,
182
+ linkContext.entityId,
183
+ defaultVisibility,
184
+ (linkContext as any).webhookUrl
185
+ );
186
+ console.log(`Linked file ${file.id} to ${linkContext.app}/${linkContext.entityType}/${linkContext.entityId}`);
187
+ } catch (error) {
188
+ console.error('Failed to link file:', error);
189
+ // Continue anyway - selection shouldn't fail if linking fails
190
+ }
191
+ }
192
+
151
193
  if (!multiSelect) {
152
194
  onSelect?.(file);
153
195
  if (afterSelect === 'back') {
@@ -171,16 +213,51 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
171
213
  }
172
214
  return next;
173
215
  });
174
- }, [selectMode, multiSelect, onSelect, onClose, goBack, disabledMimeTypes, maxSelection, afterSelect]);
216
+ }, [selectMode, multiSelect, onSelect, onClose, goBack, disabledMimeTypes, maxSelection, afterSelect, defaultVisibility, oxyServices, linkContext]);
175
217
 
176
- const confirmMultiSelection = useCallback(() => {
218
+ const confirmMultiSelection = useCallback(async () => {
177
219
  if (!selectMode || !multiSelect) return;
178
220
  const map: Record<string, FileMetadata> = {};
179
221
  files.forEach(f => { map[f.id] = f; });
180
222
  const chosen = Array.from(selectedIds).map(id => map[id]).filter(Boolean);
223
+
224
+ // Update visibility and link files if needed
225
+ const updatePromises = chosen.map(async (file) => {
226
+ // Update visibility if needed
227
+ const fileVisibility = (file.metadata as any)?.visibility || 'private';
228
+ if (fileVisibility !== defaultVisibility) {
229
+ try {
230
+ await oxyServices.assetUpdateVisibility(file.id, defaultVisibility);
231
+ console.log(`Updated file ${file.id} visibility from ${fileVisibility} to ${defaultVisibility}`);
232
+ } catch (error) {
233
+ console.error(`Failed to update visibility for ${file.id}:`, error);
234
+ }
235
+ }
236
+
237
+ // Link file to entity if linkContext provided
238
+ if (linkContext) {
239
+ try {
240
+ await oxyServices.assetLink(
241
+ file.id,
242
+ linkContext.app,
243
+ linkContext.entityType,
244
+ linkContext.entityId,
245
+ defaultVisibility,
246
+ (linkContext as any).webhookUrl
247
+ );
248
+ console.log(`Linked file ${file.id} to ${linkContext.app}/${linkContext.entityType}/${linkContext.entityId}`);
249
+ } catch (error) {
250
+ console.error(`Failed to link file ${file.id}:`, error);
251
+ }
252
+ }
253
+ });
254
+
255
+ // Wait for all updates (but don't block on failures)
256
+ await Promise.allSettled(updatePromises);
257
+
181
258
  onConfirmSelection?.(chosen);
182
259
  onClose?.();
183
- }, [selectMode, multiSelect, selectedIds, files, onConfirmSelection, onClose]);
260
+ }, [selectMode, multiSelect, selectedIds, files, onConfirmSelection, onClose, defaultVisibility, oxyServices, linkContext]);
184
261
 
185
262
  const endUpload = useCallback(() => {
186
263
  const started = uploadStartRef.current;
@@ -1,5 +1,5 @@
1
1
  import type React from 'react';
2
- import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
2
+ import { useState, useMemo, useCallback } from 'react';
3
3
  import type { BaseScreenProps } from '../navigation/types';
4
4
  import { useOxy } from '../context/OxyContext';
5
5
  import { useThemeColors } from '../styles';
@@ -29,11 +29,9 @@ const SignInScreen: React.FC<BaseScreenProps> = ({
29
29
  const [validationStatus, setValidationStatus] = useState<'idle' | 'validating' | 'valid' | 'invalid'>(
30
30
  initialUserProfile ? 'valid' : 'idle'
31
31
  );
32
+ const [existingSession, setExistingSession] = useState<any>(null);
32
33
 
33
- // Cache for validation results to prevent repeated API calls
34
- const validationCache = useRef<Map<string, { profile: any }>>(new Map());
35
-
36
- const { login, completeMfaLogin, isLoading, user, isAuthenticated, sessions, oxyServices } = useOxy();
34
+ const { login, completeMfaLogin, isLoading, user, isAuthenticated, sessions, oxyServices, switchSession } = useOxy();
37
35
 
38
36
  // Only log props in development mode to reduce console noise
39
37
  if (__DEV__) {
@@ -68,14 +66,13 @@ const SignInScreen: React.FC<BaseScreenProps> = ({
68
66
  return false;
69
67
  }
70
68
 
71
- // Check cache first
72
- const cached = validationCache.current.get(usernameToValidate);
73
- if (cached) {
74
- if (__DEV__) console.log(' Username found in cache:', cached.profile);
75
- setUserProfile(cached.profile);
76
- setValidationStatus('valid');
77
- setErrorMessage('');
78
- return true;
69
+ const offlineDetected = typeof navigator !== 'undefined' && navigator.onLine === false;
70
+
71
+ if (offlineDetected) {
72
+ if (__DEV__) console.log('⚠️ Offline detected, skipping username validation');
73
+ setValidationStatus('invalid');
74
+ setErrorMessage('No connection. Check your internet connection and try again.');
75
+ return false;
79
76
  }
80
77
 
81
78
  if (__DEV__) console.log('🔄 Validating username with API...');
@@ -97,14 +94,24 @@ const SignInScreen: React.FC<BaseScreenProps> = ({
97
94
 
98
95
  if (__DEV__) console.log('✅ Username is valid:', profileData);
99
96
  setUserProfile(profileData);
97
+
98
+ // Check if this account is already signed in
99
+ const profileUserId = profile.id?.toString();
100
+ const existing = sessions?.find(s => {
101
+ const sessionUserId = s.userId?.toString();
102
+ return sessionUserId === profileUserId;
103
+ });
104
+
105
+ if (existing) {
106
+ setExistingSession(existing);
107
+ if (__DEV__) console.log('✅ Account already signed in:', existing);
108
+ } else {
109
+ setExistingSession(null);
110
+ }
111
+
100
112
  setValidationStatus('valid');
101
113
  setErrorMessage('');
102
114
 
103
- // Cache the result
104
- validationCache.current.set(usernameToValidate, {
105
- profile: profileData
106
- });
107
-
108
115
  return true;
109
116
  } else {
110
117
  if (__DEV__) console.log('❌ Username not found');
@@ -116,25 +123,29 @@ const SignInScreen: React.FC<BaseScreenProps> = ({
116
123
  if (__DEV__) console.log('🚨 Validation error:', error);
117
124
 
118
125
  // If user not found (404), username doesn't exist
119
- if (error.status === 404 || error.code === 'USER_NOT_FOUND') {
126
+ if (error?.status === 404 || error?.code === 'USER_NOT_FOUND') {
120
127
  console.log('❌ Username not found (404)');
121
128
  setValidationStatus('invalid');
122
129
  setErrorMessage('Username not found.');
123
130
  return false;
124
131
  }
125
132
 
126
- // For development/testing: if API fails, allow any 3+ character username
127
- if (__DEV__) {
128
- if (__DEV__) console.log('⚠️ Development mode: allowing username due to API error');
129
- setValidationStatus('valid');
130
- setErrorMessage('');
131
- return true;
132
- }
133
+ const isNetworkError =
134
+ error?.status === 0 ||
135
+ error?.code === 'ECONNABORTED' ||
136
+ error?.code === 'ERR_NETWORK' ||
137
+ error?.message?.toLowerCase?.().includes('network request failed') ||
138
+ error?.message?.toLowerCase?.().includes('network error') ||
139
+ error?.name === 'AbortError' ||
140
+ error?.type === 'network';
133
141
 
134
- // For other errors, show generic message
135
142
  console.error('Username validation error:', error);
136
143
  setValidationStatus('invalid');
137
- setErrorMessage('Unable to validate username. Please try again.');
144
+ setErrorMessage(
145
+ isNetworkError
146
+ ? 'No connection. Check your internet connection and try again.'
147
+ : 'Unable to validate username. Please try again.'
148
+ );
138
149
  return false;
139
150
  } finally {
140
151
  setIsValidating(false);
@@ -177,6 +188,22 @@ const SignInScreen: React.FC<BaseScreenProps> = ({
177
188
 
178
189
  const [mfaToken, setMfaToken] = useState<string | null>(null);
179
190
 
191
+ const handleContinueWithExistingAccount = useCallback(async () => {
192
+ if (!existingSession) return;
193
+
194
+ try {
195
+ setErrorMessage('');
196
+ await switchSession(existingSession.sessionId);
197
+ // Get the user for the authenticated callback
198
+ const currentUser = await oxyServices.getUserBySession(existingSession.sessionId);
199
+ if (onAuthenticated) {
200
+ onAuthenticated(currentUser);
201
+ }
202
+ } catch (error: any) {
203
+ setErrorMessage(error.message || 'Failed to switch account');
204
+ }
205
+ }, [existingSession, switchSession, oxyServices, onAuthenticated]);
206
+
180
207
  const handleSignIn = useCallback(async () => {
181
208
  if (!password) {
182
209
  setErrorMessage('Please enter your password.');
@@ -186,6 +213,7 @@ const SignInScreen: React.FC<BaseScreenProps> = ({
186
213
  setErrorMessage('Please enter a valid username first.');
187
214
  return;
188
215
  }
216
+
189
217
  try {
190
218
  setErrorMessage('');
191
219
  const user = await login(username, password);
@@ -201,13 +229,6 @@ const SignInScreen: React.FC<BaseScreenProps> = ({
201
229
  }
202
230
  }, [username, password, login, onAuthenticated, userProfile]);
203
231
 
204
- // Simple cleanup on unmount - that's all we need for username validation
205
- useEffect(() => {
206
- return () => {
207
- validationCache.current.clear();
208
- };
209
- }, []);
210
-
211
232
  // Step configurations
212
233
  const steps: StepConfig[] = useMemo(() => {
213
234
  const base: StepConfig[] = [
@@ -259,6 +280,8 @@ const SignInScreen: React.FC<BaseScreenProps> = ({
259
280
  handleInputBlur,
260
281
  handleSignIn, // Add sign-in function for password step
261
282
  mfaToken,
283
+ existingSession,
284
+ handleContinueWithExistingAccount,
262
285
  },
263
286
  ...(mfaToken ? [{
264
287
  username,
@@ -272,7 +295,7 @@ const SignInScreen: React.FC<BaseScreenProps> = ({
272
295
  username, password, errorMessage, validationStatus, userProfile, mfaToken,
273
296
  isValidating, isInputFocused, isAddAccountMode, user, showPassword,
274
297
  isLoading, handleUsernameChange, handlePasswordChange, handleInputFocus, handleInputBlur,
275
- validateUsername, handleSignIn, completeMfaLogin
298
+ validateUsername, handleSignIn, completeMfaLogin, existingSession, handleContinueWithExistingAccount
276
299
  ]);
277
300
 
278
301
  return (
@@ -1,5 +1,6 @@
1
- import React, { useCallback, useMemo, useRef, useState } from 'react';
1
+ import React, { useCallback, useMemo, useRef, useState, useEffect } from 'react';
2
2
  import { View, Text, TouchableOpacity, StyleSheet, Platform, Animated, ScrollView } from 'react-native';
3
+ import AnimatedReanimated, { useSharedValue, useAnimatedStyle, withTiming } from 'react-native-reanimated';
3
4
  import type { BaseScreenProps } from '../navigation/types';
4
5
  import { useOxy } from '../context/OxyContext';
5
6
  import Avatar from '../components/Avatar';
@@ -10,6 +11,41 @@ import { useThemeColors } from '../styles';
10
11
  import GroupedPillButtons from '../components/internal/GroupedPillButtons';
11
12
  import { useI18n } from '../hooks/useI18n';
12
13
 
14
+ const GAP = 12;
15
+ const INNER_GAP = 8;
16
+
17
+ // Individual animated progress dot
18
+ const AnimatedProgressDot: React.FC<{
19
+ isActive: boolean;
20
+ colors: any;
21
+ styles: any;
22
+ }> = ({ isActive, colors, styles }) => {
23
+ const width = useSharedValue(isActive ? 12 : 6);
24
+ const backgroundColor = useSharedValue(isActive ? colors.primary : colors.border);
25
+
26
+ useEffect(() => {
27
+ width.value = withTiming(isActive ? 12 : 6, { duration: 300 });
28
+ backgroundColor.value = withTiming(
29
+ isActive ? colors.primary : colors.border,
30
+ { duration: 300 }
31
+ );
32
+ }, [isActive, colors.primary, colors.border, width, backgroundColor]);
33
+
34
+ const animatedStyle = useAnimatedStyle(() => ({
35
+ width: width.value,
36
+ backgroundColor: backgroundColor.value,
37
+ }));
38
+
39
+ return (
40
+ <AnimatedReanimated.View
41
+ style={[
42
+ styles.progressDot,
43
+ animatedStyle,
44
+ ]}
45
+ />
46
+ );
47
+ };
48
+
13
49
  /**
14
50
  * Post-signup welcome & onboarding screen.
15
51
  * - Greets the newly registered user
@@ -138,33 +174,44 @@ const WelcomeNewUserScreen: React.FC<BaseScreenProps & { newUser?: any }> = ({
138
174
  <View style={styles.container}>
139
175
  <View style={styles.progressContainer}>
140
176
  {steps.map((s, i) => (
141
- <View key={s.key} style={[styles.progressDot, i === currentStep ? { backgroundColor: colors.primary, width: 22 } : { backgroundColor: colors.border }]} />
177
+ <AnimatedProgressDot
178
+ key={s.key}
179
+ isActive={i === currentStep}
180
+ colors={colors}
181
+ styles={styles}
182
+ />
142
183
  ))}
143
184
  </View>
144
185
  <Animated.View style={{ opacity: fadeAnim, transform: [{ translateX: slideAnim }] }}>
145
186
  <ScrollView contentContainerStyle={styles.scrollInner} showsVerticalScrollIndicator={false}>
146
- <Text style={styles.title}>{step.title}</Text>
147
- {step.body && <Text style={styles.body}>{step.body}</Text>}
148
- {Array.isArray(step.bullets) && step.bullets.length > 0 && (
149
- <View style={styles.bulletContainer}>
150
- {step.bullets.map(b => (
151
- <View key={b} style={styles.bulletRow}>
152
- <Ionicons name="ellipse" size={8} color={colors.primary} style={{ marginTop: 6 }} />
153
- <Text style={styles.bulletText}>{b}</Text>
154
- </View>
155
- ))}
187
+ <View style={styles.contentContainer}>
188
+ <View style={[styles.header, styles.sectionSpacing]}>
189
+ <Text style={[styles.title, { color: colors.text }]}>{step.title}</Text>
190
+ {step.body && <Text style={[styles.body, { color: colors.secondaryText }]}>{step.body}</Text>}
156
191
  </View>
157
- )}
158
- {step.showAvatar && (
159
- <View style={styles.avatarSection}>
160
- <Avatar size={120} name={currentUser?.username} uri={avatarUri} theme={theme} style={styles.avatar} />
161
- <TouchableOpacity style={styles.changeAvatarButton} onPress={openAvatarPicker}>
162
- <Ionicons name="image-outline" size={18} color="#FFFFFF" />
163
- <Text style={styles.changeAvatarText}>{avatarUri ? (t('welcomeNew.avatar.change') || 'Change Avatar') : (t('welcomeNew.avatar.add') || 'Add Avatar')}</Text>
164
- </TouchableOpacity>
192
+ {Array.isArray(step.bullets) && step.bullets.length > 0 && (
193
+ <View style={[styles.bulletContainer, styles.sectionSpacing]}>
194
+ {step.bullets.map(b => (
195
+ <View key={b} style={styles.bulletRow}>
196
+ <Ionicons name="ellipse" size={8} color={colors.primary} style={{ marginTop: 6 }} />
197
+ <Text style={[styles.bulletText, { color: colors.secondaryText }]}>{b}</Text>
198
+ </View>
199
+ ))}
200
+ </View>
201
+ )}
202
+ {step.showAvatar && (
203
+ <View style={[styles.avatarSection, styles.sectionSpacing]}>
204
+ <Avatar size={120} name={currentUser?.username} uri={avatarUri} theme={theme} style={styles.avatar} />
205
+ <TouchableOpacity style={[styles.changeAvatarButton, { backgroundColor: colors.primary }]} onPress={openAvatarPicker}>
206
+ <Ionicons name="image-outline" size={18} color="#FFFFFF" />
207
+ <Text style={styles.changeAvatarText}>{avatarUri ? (t('welcomeNew.avatar.change') || 'Change Avatar') : (t('welcomeNew.avatar.add') || 'Add Avatar')}</Text>
208
+ </TouchableOpacity>
209
+ </View>
210
+ )}
211
+ <View style={styles.sectionSpacing}>
212
+ <GroupedPillButtons buttons={pillButtons} colors={colors} />
165
213
  </View>
166
- )}
167
- <GroupedPillButtons buttons={pillButtons} colors={colors} />
214
+ </View>
168
215
  </ScrollView>
169
216
  </Animated.View>
170
217
  </View>
@@ -174,11 +221,7 @@ const WelcomeNewUserScreen: React.FC<BaseScreenProps & { newUser?: any }> = ({
174
221
 
175
222
  const createStyles = (theme: string) => {
176
223
  const isDark = theme === 'dark';
177
- const textColor = isDark ? '#FFFFFF' : '#000000';
178
- const secondary = isDark ? '#CCCCCC' : '#555555';
179
- const cardBg = isDark ? '#1E1E1E' : '#FFFFFF';
180
224
  const border = isDark ? '#333333' : '#E0E0E0';
181
- const primary = '#007AFF';
182
225
  return StyleSheet.create({
183
226
  container: {
184
227
  width: '100%',
@@ -186,25 +229,38 @@ const createStyles = (theme: string) => {
186
229
  },
187
230
  scrollInner: {
188
231
  paddingBottom: 12,
232
+ paddingTop: 0,
233
+ },
234
+ contentContainer: {
235
+ width: '100%',
236
+ maxWidth: 420,
237
+ alignSelf: 'center',
238
+ },
239
+ sectionSpacing: {
240
+ marginBottom: GAP,
241
+ },
242
+ header: {
243
+ alignItems: 'flex-start',
244
+ width: '100%',
245
+ gap: INNER_GAP / 2,
189
246
  },
190
247
  title: {
191
248
  fontSize: 42,
192
249
  fontFamily: Platform.OS === 'web' ? 'Phudu' : 'Phudu-Bold',
193
250
  fontWeight: Platform.OS === 'web' ? 'bold' : undefined,
194
251
  letterSpacing: -1,
195
- color: textColor,
196
- marginBottom: 12,
252
+ textAlign: 'left',
197
253
  },
198
254
  body: {
199
255
  fontSize: 16,
200
256
  lineHeight: 22,
201
- color: secondary,
202
- marginBottom: 28,
203
- maxWidth: 620,
257
+ textAlign: 'left',
258
+ maxWidth: 320,
259
+ alignSelf: 'flex-start',
204
260
  },
205
261
  bulletContainer: {
206
- gap: 10,
207
- marginBottom: 32,
262
+ gap: INNER_GAP,
263
+ width: '100%',
208
264
  },
209
265
  bulletRow: {
210
266
  flexDirection: 'row',
@@ -215,26 +271,26 @@ const createStyles = (theme: string) => {
215
271
  flex: 1,
216
272
  fontSize: 15,
217
273
  lineHeight: 20,
218
- color: secondary,
219
274
  },
220
275
  avatarSection: {
221
276
  width: '100%',
222
277
  alignItems: 'center',
223
- marginBottom: 40,
224
278
  },
225
279
  avatar: {
226
- marginBottom: 16,
227
- borderWidth: 4,
228
- borderColor: primary + '40',
280
+ marginBottom: INNER_GAP,
229
281
  },
230
282
  changeAvatarButton: {
231
283
  flexDirection: 'row',
232
284
  alignItems: 'center',
233
- backgroundColor: primary,
234
285
  paddingHorizontal: 18,
235
286
  paddingVertical: 10,
236
- borderRadius: 35,
287
+ borderRadius: 28,
237
288
  gap: 8,
289
+ shadowOpacity: 0,
290
+ shadowRadius: 0,
291
+ shadowOffset: { width: 0, height: 0 },
292
+ elevation: 0,
293
+ ...(Platform.OS === 'web' ? { boxShadow: 'none' } : null),
238
294
  },
239
295
  changeAvatarText: {
240
296
  color: '#FFFFFF',
@@ -245,61 +301,16 @@ const createStyles = (theme: string) => {
245
301
  flexDirection: 'row',
246
302
  width: '100%',
247
303
  justifyContent: 'center',
248
- marginBottom: 24,
249
- marginTop: 4,
304
+ marginTop: 24, // Space for bottom sheet handle (~20px) + small buffer
305
+ marginBottom: 24, // Equal spacing below dots
250
306
  },
251
307
  progressDot: {
252
- height: 10,
253
- width: 10,
254
- borderRadius: 5,
255
- marginHorizontal: 6,
308
+ height: 6,
309
+ width: 6,
310
+ borderRadius: 3,
311
+ marginHorizontal: 3,
256
312
  backgroundColor: border,
257
313
  },
258
- navBar: {
259
- flexDirection: 'row',
260
- alignItems: 'center',
261
- width: '100%',
262
- gap: 12,
263
- marginTop: 8,
264
- },
265
- navButton: {
266
- flexDirection: 'row',
267
- alignItems: 'center',
268
- gap: 6,
269
- paddingHorizontal: 14,
270
- paddingVertical: 10,
271
- borderRadius: 12,
272
- },
273
- backButton: {
274
- backgroundColor: cardBg,
275
- borderWidth: 1,
276
- borderColor: border,
277
- },
278
- skipButton: {
279
- marginLeft: 'auto',
280
- backgroundColor: 'transparent',
281
- paddingHorizontal: 4,
282
- },
283
- navText: {
284
- fontSize: 14,
285
- fontWeight: '500',
286
- },
287
- primaryButton: {
288
- flexDirection: 'row',
289
- alignItems: 'center',
290
- justifyContent: 'center',
291
- gap: 8,
292
- backgroundColor: primary,
293
- paddingVertical: 18,
294
- borderRadius: 16,
295
- width: '100%',
296
- },
297
- primaryButtonText: {
298
- color: '#FFFFFF',
299
- fontSize: 16,
300
- fontWeight: '600',
301
- letterSpacing: 0.5,
302
- },
303
314
  });
304
315
  };
305
316
 
@@ -95,7 +95,7 @@ const SignInUsernameStep: React.FC<SignInUsernameStepProps> = ({
95
95
  <View style={[styles.modernInfoCard, { backgroundColor: colors.inputBackground }]}>
96
96
  <Ionicons name="information-circle" size={20} color={colors.primary} />
97
97
  <Text style={[styles.modernInfoText, { color: colors.text }]}>
98
- {t('signin.currentlySignedInAs', { username: user?.username }) || 'Currently signed in as '}<Text style={{ fontWeight: 'bold' }}>{user?.username}</Text>
98
+ {t('signin.currentlySignedInAs') || 'Currently signed in as'} <Text style={{ fontWeight: 'bold' }}>{user?.name?.full || user?.username}</Text>
99
99
  </Text>
100
100
  </View>
101
101
  )}