@oxyhq/services 5.13.0 → 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 (204) hide show
  1. package/lib/commonjs/core/OxyServices.js +7 -7
  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.map +1 -1
  29. package/lib/commonjs/ui/screens/SignInScreen.js +43 -39
  30. package/lib/commonjs/ui/screens/SignInScreen.js.map +1 -1
  31. package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js +139 -125
  32. package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js.map +1 -1
  33. package/lib/commonjs/ui/screens/internal/SignInUsernameStep.js +2 -4
  34. package/lib/commonjs/ui/screens/internal/SignInUsernameStep.js.map +1 -1
  35. package/lib/commonjs/ui/screens/steps/RecoverRequestStep.js +45 -25
  36. package/lib/commonjs/ui/screens/steps/RecoverRequestStep.js.map +1 -1
  37. package/lib/commonjs/ui/screens/steps/RecoverResetPasswordStep.js +88 -53
  38. package/lib/commonjs/ui/screens/steps/RecoverResetPasswordStep.js.map +1 -1
  39. package/lib/commonjs/ui/screens/steps/RecoverSuccessStep.js +79 -58
  40. package/lib/commonjs/ui/screens/steps/RecoverSuccessStep.js.map +1 -1
  41. package/lib/commonjs/ui/screens/steps/RecoverVerifyStep.js +61 -52
  42. package/lib/commonjs/ui/screens/steps/RecoverVerifyStep.js.map +1 -1
  43. package/lib/commonjs/ui/screens/steps/SignInPasswordStep.js +220 -31
  44. package/lib/commonjs/ui/screens/steps/SignInPasswordStep.js.map +1 -1
  45. package/lib/commonjs/ui/screens/steps/SignInTotpStep.js +77 -50
  46. package/lib/commonjs/ui/screens/steps/SignInTotpStep.js.map +1 -1
  47. package/lib/commonjs/ui/screens/steps/SignInUsernameStep.js +527 -66
  48. package/lib/commonjs/ui/screens/steps/SignInUsernameStep.js.map +1 -1
  49. package/lib/commonjs/ui/screens/steps/SignUpIdentityStep.js +55 -30
  50. package/lib/commonjs/ui/screens/steps/SignUpIdentityStep.js.map +1 -1
  51. package/lib/commonjs/ui/screens/steps/SignUpSecurityStep.js +64 -46
  52. package/lib/commonjs/ui/screens/steps/SignUpSecurityStep.js.map +1 -1
  53. package/lib/commonjs/ui/screens/steps/SignUpSummaryStep.js +84 -146
  54. package/lib/commonjs/ui/screens/steps/SignUpSummaryStep.js.map +1 -1
  55. package/lib/commonjs/ui/screens/steps/SignUpWelcomeStep.js +113 -34
  56. package/lib/commonjs/ui/screens/steps/SignUpWelcomeStep.js.map +1 -1
  57. package/lib/commonjs/ui/stores/authStore.js +16 -20
  58. package/lib/commonjs/ui/stores/authStore.js.map +1 -1
  59. package/lib/commonjs/ui/styles/authStyles.js +2 -1
  60. package/lib/commonjs/ui/styles/authStyles.js.map +1 -1
  61. package/lib/commonjs/ui/styles/index.js +11 -0
  62. package/lib/commonjs/ui/styles/index.js.map +1 -1
  63. package/lib/commonjs/ui/styles/spacing.js +51 -0
  64. package/lib/commonjs/ui/styles/spacing.js.map +1 -0
  65. package/lib/commonjs/utils/validationUtils.js +1 -1
  66. package/lib/module/core/OxyServices.js +7 -7
  67. package/lib/module/core/OxyServices.js.map +1 -1
  68. package/lib/module/i18n/index.js +37 -1
  69. package/lib/module/i18n/index.js.map +1 -1
  70. package/lib/module/i18n/locales/ar-SA.json +128 -0
  71. package/lib/module/i18n/locales/ca-ES.json +128 -0
  72. package/lib/module/i18n/locales/de-DE.json +128 -0
  73. package/lib/module/i18n/locales/en-US.json +85 -12
  74. package/lib/module/i18n/locales/es-ES.json +58 -6
  75. package/lib/module/i18n/locales/fr-FR.json +128 -0
  76. package/lib/module/i18n/locales/it-IT.json +128 -0
  77. package/lib/module/i18n/locales/ja-JP.json +127 -0
  78. package/lib/module/i18n/locales/ko-KR.json +128 -0
  79. package/lib/module/i18n/locales/pt-PT.json +128 -0
  80. package/lib/module/i18n/locales/zh-CN.json +128 -0
  81. package/lib/module/ui/components/FontLoader.js +23 -43
  82. package/lib/module/ui/components/FontLoader.js.map +1 -1
  83. package/lib/module/ui/components/OxyProvider.js +6 -8
  84. package/lib/module/ui/components/OxyProvider.js.map +1 -1
  85. package/lib/module/ui/components/StepBasedScreen.js +65 -45
  86. package/lib/module/ui/components/StepBasedScreen.js.map +1 -1
  87. package/lib/module/ui/components/internal/GroupedPillButtons.js +14 -35
  88. package/lib/module/ui/components/internal/GroupedPillButtons.js.map +1 -1
  89. package/lib/module/ui/components/internal/PinInput.js +2 -2
  90. package/lib/module/ui/components/internal/PinInput.js.map +1 -1
  91. package/lib/module/ui/context/OxyContext.js +434 -321
  92. package/lib/module/ui/context/OxyContext.js.map +1 -1
  93. package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
  94. package/lib/module/ui/screens/SignInScreen.js +44 -40
  95. package/lib/module/ui/screens/SignInScreen.js.map +1 -1
  96. package/lib/module/ui/screens/WelcomeNewUserScreen.js +138 -126
  97. package/lib/module/ui/screens/WelcomeNewUserScreen.js.map +1 -1
  98. package/lib/module/ui/screens/internal/SignInUsernameStep.js +2 -4
  99. package/lib/module/ui/screens/internal/SignInUsernameStep.js.map +1 -1
  100. package/lib/module/ui/screens/steps/RecoverRequestStep.js +45 -25
  101. package/lib/module/ui/screens/steps/RecoverRequestStep.js.map +1 -1
  102. package/lib/module/ui/screens/steps/RecoverResetPasswordStep.js +89 -54
  103. package/lib/module/ui/screens/steps/RecoverResetPasswordStep.js.map +1 -1
  104. package/lib/module/ui/screens/steps/RecoverSuccessStep.js +80 -59
  105. package/lib/module/ui/screens/steps/RecoverSuccessStep.js.map +1 -1
  106. package/lib/module/ui/screens/steps/RecoverVerifyStep.js +62 -53
  107. package/lib/module/ui/screens/steps/RecoverVerifyStep.js.map +1 -1
  108. package/lib/module/ui/screens/steps/SignInPasswordStep.js +221 -32
  109. package/lib/module/ui/screens/steps/SignInPasswordStep.js.map +1 -1
  110. package/lib/module/ui/screens/steps/SignInTotpStep.js +78 -51
  111. package/lib/module/ui/screens/steps/SignInTotpStep.js.map +1 -1
  112. package/lib/module/ui/screens/steps/SignInUsernameStep.js +530 -68
  113. package/lib/module/ui/screens/steps/SignInUsernameStep.js.map +1 -1
  114. package/lib/module/ui/screens/steps/SignUpIdentityStep.js +55 -30
  115. package/lib/module/ui/screens/steps/SignUpIdentityStep.js.map +1 -1
  116. package/lib/module/ui/screens/steps/SignUpSecurityStep.js +65 -47
  117. package/lib/module/ui/screens/steps/SignUpSecurityStep.js.map +1 -1
  118. package/lib/module/ui/screens/steps/SignUpSummaryStep.js +84 -146
  119. package/lib/module/ui/screens/steps/SignUpSummaryStep.js.map +1 -1
  120. package/lib/module/ui/screens/steps/SignUpWelcomeStep.js +114 -35
  121. package/lib/module/ui/screens/steps/SignUpWelcomeStep.js.map +1 -1
  122. package/lib/module/ui/stores/authStore.js +16 -20
  123. package/lib/module/ui/stores/authStore.js.map +1 -1
  124. package/lib/module/ui/styles/authStyles.js +2 -1
  125. package/lib/module/ui/styles/authStyles.js.map +1 -1
  126. package/lib/module/ui/styles/index.js +1 -0
  127. package/lib/module/ui/styles/index.js.map +1 -1
  128. package/lib/module/ui/styles/spacing.js +48 -0
  129. package/lib/module/ui/styles/spacing.js.map +1 -0
  130. package/lib/module/utils/validationUtils.js +1 -1
  131. package/lib/typescript/core/OxyServices.d.ts +4 -2
  132. package/lib/typescript/core/OxyServices.d.ts.map +1 -1
  133. package/lib/typescript/i18n/index.d.ts.map +1 -1
  134. package/lib/typescript/ui/components/FontLoader.d.ts +3 -3
  135. package/lib/typescript/ui/components/FontLoader.d.ts.map +1 -1
  136. package/lib/typescript/ui/components/OxyProvider.d.ts +2 -2
  137. package/lib/typescript/ui/components/OxyProvider.d.ts.map +1 -1
  138. package/lib/typescript/ui/components/StepBasedScreen.d.ts.map +1 -1
  139. package/lib/typescript/ui/components/internal/GroupedPillButtons.d.ts.map +1 -1
  140. package/lib/typescript/ui/context/OxyContext.d.ts +1 -0
  141. package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
  142. package/lib/typescript/ui/screens/SignInScreen.d.ts.map +1 -1
  143. package/lib/typescript/ui/screens/WelcomeNewUserScreen.d.ts.map +1 -1
  144. package/lib/typescript/ui/screens/steps/RecoverRequestStep.d.ts.map +1 -1
  145. package/lib/typescript/ui/screens/steps/RecoverResetPasswordStep.d.ts.map +1 -1
  146. package/lib/typescript/ui/screens/steps/RecoverSuccessStep.d.ts.map +1 -1
  147. package/lib/typescript/ui/screens/steps/RecoverVerifyStep.d.ts.map +1 -1
  148. package/lib/typescript/ui/screens/steps/SignInPasswordStep.d.ts +2 -0
  149. package/lib/typescript/ui/screens/steps/SignInPasswordStep.d.ts.map +1 -1
  150. package/lib/typescript/ui/screens/steps/SignInTotpStep.d.ts.map +1 -1
  151. package/lib/typescript/ui/screens/steps/SignInUsernameStep.d.ts.map +1 -1
  152. package/lib/typescript/ui/screens/steps/SignUpIdentityStep.d.ts.map +1 -1
  153. package/lib/typescript/ui/screens/steps/SignUpSecurityStep.d.ts.map +1 -1
  154. package/lib/typescript/ui/screens/steps/SignUpSummaryStep.d.ts.map +1 -1
  155. package/lib/typescript/ui/screens/steps/SignUpWelcomeStep.d.ts.map +1 -1
  156. package/lib/typescript/ui/stores/authStore.d.ts +7 -3
  157. package/lib/typescript/ui/stores/authStore.d.ts.map +1 -1
  158. package/lib/typescript/ui/styles/authStyles.d.ts +1 -0
  159. package/lib/typescript/ui/styles/authStyles.d.ts.map +1 -1
  160. package/lib/typescript/ui/styles/index.d.ts +1 -0
  161. package/lib/typescript/ui/styles/index.d.ts.map +1 -1
  162. package/lib/typescript/ui/styles/spacing.d.ts +43 -0
  163. package/lib/typescript/ui/styles/spacing.d.ts.map +1 -0
  164. package/lib/typescript/utils/validationUtils.d.ts +1 -1
  165. package/package.json +1 -1
  166. package/src/core/OxyServices.ts +10 -8
  167. package/src/i18n/index.ts +36 -0
  168. package/src/i18n/locales/ar-SA.json +128 -0
  169. package/src/i18n/locales/ca-ES.json +128 -0
  170. package/src/i18n/locales/de-DE.json +128 -0
  171. package/src/i18n/locales/en-US.json +85 -12
  172. package/src/i18n/locales/es-ES.json +58 -6
  173. package/src/i18n/locales/fr-FR.json +128 -0
  174. package/src/i18n/locales/it-IT.json +128 -0
  175. package/src/i18n/locales/ja-JP.json +127 -0
  176. package/src/i18n/locales/ko-KR.json +128 -0
  177. package/src/i18n/locales/pt-PT.json +128 -0
  178. package/src/i18n/locales/zh-CN.json +128 -0
  179. package/src/ui/components/FontLoader.tsx +17 -37
  180. package/src/ui/components/OxyProvider.tsx +14 -13
  181. package/src/ui/components/StepBasedScreen.tsx +66 -43
  182. package/src/ui/components/internal/GroupedPillButtons.tsx +15 -31
  183. package/src/ui/components/internal/PinInput.tsx +2 -2
  184. package/src/ui/context/OxyContext.tsx +404 -285
  185. package/src/ui/screens/FileManagementScreen.tsx +15 -15
  186. package/src/ui/screens/SignInScreen.tsx +59 -36
  187. package/src/ui/screens/WelcomeNewUserScreen.tsx +102 -91
  188. package/src/ui/screens/internal/SignInUsernameStep.tsx +1 -1
  189. package/src/ui/screens/steps/RecoverRequestStep.tsx +34 -24
  190. package/src/ui/screens/steps/RecoverResetPasswordStep.tsx +65 -36
  191. package/src/ui/screens/steps/RecoverSuccessStep.tsx +71 -47
  192. package/src/ui/screens/steps/RecoverVerifyStep.tsx +60 -50
  193. package/src/ui/screens/steps/SignInPasswordStep.tsx +191 -29
  194. package/src/ui/screens/steps/SignInTotpStep.tsx +68 -34
  195. package/src/ui/screens/steps/SignInUsernameStep.tsx +586 -57
  196. package/src/ui/screens/steps/SignUpIdentityStep.tsx +49 -35
  197. package/src/ui/screens/steps/SignUpSecurityStep.tsx +56 -39
  198. package/src/ui/screens/steps/SignUpSummaryStep.tsx +99 -89
  199. package/src/ui/screens/steps/SignUpWelcomeStep.tsx +88 -20
  200. package/src/ui/stores/authStore.ts +15 -19
  201. package/src/ui/styles/authStyles.ts +2 -1
  202. package/src/ui/styles/index.ts +1 -0
  203. package/src/ui/styles/spacing.ts +46 -0
  204. package/src/utils/validationUtils.ts +1 -1
@@ -159,7 +159,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
159
159
  return;
160
160
  }
161
161
  }
162
-
162
+
163
163
  // Update file visibility if it differs from defaultVisibility
164
164
  const fileVisibility = (file.metadata as any)?.visibility || 'private';
165
165
  if (fileVisibility !== defaultVisibility) {
@@ -171,25 +171,25 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
171
171
  // Continue anyway - selection shouldn't fail if visibility update fails
172
172
  }
173
173
  }
174
-
174
+
175
175
  // Link file to entity if linkContext is provided
176
176
  if (linkContext) {
177
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
- );
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
186
  console.log(`Linked file ${file.id} to ${linkContext.app}/${linkContext.entityType}/${linkContext.entityId}`);
187
187
  } catch (error) {
188
188
  console.error('Failed to link file:', error);
189
189
  // Continue anyway - selection shouldn't fail if linking fails
190
190
  }
191
191
  }
192
-
192
+
193
193
  if (!multiSelect) {
194
194
  onSelect?.(file);
195
195
  if (afterSelect === 'back') {
@@ -220,7 +220,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
220
220
  const map: Record<string, FileMetadata> = {};
221
221
  files.forEach(f => { map[f.id] = f; });
222
222
  const chosen = Array.from(selectedIds).map(id => map[id]).filter(Boolean);
223
-
223
+
224
224
  // Update visibility and link files if needed
225
225
  const updatePromises = chosen.map(async (file) => {
226
226
  // Update visibility if needed
@@ -233,7 +233,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
233
233
  console.error(`Failed to update visibility for ${file.id}:`, error);
234
234
  }
235
235
  }
236
-
236
+
237
237
  // Link file to entity if linkContext provided
238
238
  if (linkContext) {
239
239
  try {
@@ -251,10 +251,10 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
251
251
  }
252
252
  }
253
253
  });
254
-
254
+
255
255
  // Wait for all updates (but don't block on failures)
256
256
  await Promise.allSettled(updatePromises);
257
-
257
+
258
258
  onConfirmSelection?.(chosen);
259
259
  onClose?.();
260
260
  }, [selectMode, multiSelect, selectedIds, files, onConfirmSelection, onClose, defaultVisibility, oxyServices, linkContext]);
@@ -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
  )}
@@ -9,6 +9,7 @@ import GroupedPillButtons from '../../components/internal/GroupedPillButtons';
9
9
  import { toast } from '../../../lib/sonner';
10
10
  import type { OxyServices } from '../../../core';
11
11
  import { useI18n } from '../../hooks/useI18n';
12
+ import { stepStyles } from '../../styles/spacing';
12
13
 
13
14
  interface RecoverRequestStepProps {
14
15
  // Common props from StepBasedScreen
@@ -51,6 +52,7 @@ const RecoverRequestStep: React.FC<RecoverRequestStepProps> = ({
51
52
  }) => {
52
53
  const inputRef = useRef<any>(null);
53
54
  const { t } = useI18n();
55
+ const baseStyles = stepStyles;
54
56
 
55
57
  const handleIdentifierChange = (text: string) => {
56
58
  setIdentifier(text);
@@ -83,13 +85,15 @@ const RecoverRequestStep: React.FC<RecoverRequestStepProps> = ({
83
85
 
84
86
  return (
85
87
  <>
86
- <HighFive width={100} height={100} />
87
- <View style={styles.modernHeader}>
88
- <Text style={[styles.modernTitle, { color: colors.text }]}>{t('recover.title')}</Text>
89
- <Text style={[styles.modernSubtitle, { color: colors.secondaryText }]}>{t('recover.noEmail')}</Text>
88
+ <View style={[baseStyles.container, baseStyles.sectionSpacing, { alignItems: 'flex-start' }]}>
89
+ <HighFive width={100} height={100} />
90
+ </View>
91
+ <View style={[baseStyles.container, baseStyles.sectionSpacing, baseStyles.header]}>
92
+ <Text style={[styles.modernTitle, baseStyles.title, { color: colors.text, marginBottom: 0, marginTop: 0 }]}>{t('recover.title')}</Text>
93
+ <Text style={[styles.modernSubtitle, baseStyles.subtitle, { color: colors.secondaryText, marginBottom: 0, marginTop: 0 }]}>{t('recover.noEmail')}</Text>
90
94
  </View>
91
95
 
92
- <View style={styles.modernInputContainer}>
96
+ <View style={[baseStyles.container, baseStyles.sectionSpacing]}>
93
97
  <TextField
94
98
  ref={inputRef}
95
99
  label={t('recover.username.label')}
@@ -101,31 +105,37 @@ const RecoverRequestStep: React.FC<RecoverRequestStepProps> = ({
101
105
  testID="recover-identifier-input"
102
106
  variant="filled"
103
107
  error={errorMessage || undefined}
108
+ helperText={t('recover.username.helper') || 'Enter your username or email'}
104
109
  editable={!isLoading}
105
110
  onSubmitEditing={handleRequestWithFocus}
106
111
  autoFocus
112
+ accessibilityLabel={t('recover.username.label')}
113
+ accessibilityHint={t('recover.username.helper') || 'Enter your username or email to recover your account'}
114
+ style={{ marginBottom: 0 }}
107
115
  />
108
116
  </View>
109
117
 
110
- <GroupedPillButtons
111
- buttons={[
112
- {
113
- text: t('common.actions.back'),
114
- onPress: () => navigate('SignIn'),
115
- icon: 'arrow-back',
116
- variant: 'transparent',
117
- },
118
- {
119
- text: t('common.actions.continue'),
120
- onPress: handleRequest,
121
- icon: 'information-circle-outline',
122
- variant: 'primary',
123
- loading: isLoading,
124
- disabled: isLoading,
125
- },
126
- ]}
127
- colors={colors}
128
- />
118
+ <View style={[baseStyles.container, baseStyles.sectionSpacing, baseStyles.buttonContainer]}>
119
+ <GroupedPillButtons
120
+ buttons={[
121
+ {
122
+ text: t('common.actions.back'),
123
+ onPress: () => navigate('SignIn'),
124
+ icon: 'arrow-back',
125
+ variant: 'transparent',
126
+ },
127
+ {
128
+ text: t('common.actions.continue'),
129
+ onPress: handleRequest,
130
+ icon: 'information-circle-outline',
131
+ variant: 'primary',
132
+ loading: isLoading,
133
+ disabled: isLoading || !identifier || identifier.length < 3,
134
+ },
135
+ ]}
136
+ colors={colors}
137
+ />
138
+ </View>
129
139
  </>
130
140
  );
131
141
  };