@oxyhq/services 5.13.0 → 5.13.2

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 (353) 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 +530 -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/i18n/index.js +37 -1
  11. package/lib/commonjs/i18n/index.js.map +1 -1
  12. package/lib/commonjs/i18n/locales/ar-SA.json +128 -0
  13. package/lib/commonjs/i18n/locales/ca-ES.json +128 -0
  14. package/lib/commonjs/i18n/locales/de-DE.json +128 -0
  15. package/lib/commonjs/i18n/locales/en-US.json +85 -12
  16. package/lib/commonjs/i18n/locales/es-ES.json +58 -6
  17. package/lib/commonjs/i18n/locales/fr-FR.json +128 -0
  18. package/lib/commonjs/i18n/locales/it-IT.json +128 -0
  19. package/lib/commonjs/i18n/locales/ja-JP.json +127 -0
  20. package/lib/commonjs/i18n/locales/ko-KR.json +128 -0
  21. package/lib/commonjs/i18n/locales/pt-PT.json +128 -0
  22. package/lib/commonjs/i18n/locales/zh-CN.json +128 -0
  23. package/lib/commonjs/index.js +36 -0
  24. package/lib/commonjs/index.js.map +1 -1
  25. package/lib/commonjs/ui/components/Avatar.js +94 -27
  26. package/lib/commonjs/ui/components/Avatar.js.map +1 -1
  27. package/lib/commonjs/ui/components/FollowButton.js +1 -0
  28. package/lib/commonjs/ui/components/FollowButton.js.map +1 -1
  29. package/lib/commonjs/ui/components/FontLoader.js +22 -42
  30. package/lib/commonjs/ui/components/FontLoader.js.map +1 -1
  31. package/lib/commonjs/ui/components/OxyProvider.js +5 -8
  32. package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
  33. package/lib/commonjs/ui/components/StepBasedScreen.js +64 -44
  34. package/lib/commonjs/ui/components/StepBasedScreen.js.map +1 -1
  35. package/lib/commonjs/ui/components/internal/GroupedPillButtons.js +14 -35
  36. package/lib/commonjs/ui/components/internal/GroupedPillButtons.js.map +1 -1
  37. package/lib/commonjs/ui/components/internal/PinInput.js +2 -2
  38. package/lib/commonjs/ui/components/internal/PinInput.js.map +1 -1
  39. package/lib/commonjs/ui/components/internal/TextField.js +13 -8
  40. package/lib/commonjs/ui/components/internal/TextField.js.map +1 -1
  41. package/lib/commonjs/ui/context/OxyContext.js +443 -371
  42. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  43. package/lib/commonjs/ui/hooks/useSessionSocket.js +80 -22
  44. package/lib/commonjs/ui/hooks/useSessionSocket.js.map +1 -1
  45. package/lib/commonjs/ui/index.js +4 -1
  46. package/lib/commonjs/ui/index.js.map +1 -1
  47. package/lib/commonjs/ui/screens/AccountSettingsScreen.js +32 -2
  48. package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
  49. package/lib/commonjs/ui/screens/AccountSwitcherScreen.js +101 -59
  50. package/lib/commonjs/ui/screens/AccountSwitcherScreen.js.map +1 -1
  51. package/lib/commonjs/ui/screens/FileManagementScreen.js +3 -2
  52. package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
  53. package/lib/commonjs/ui/screens/LanguageSelectorScreen.js +75 -117
  54. package/lib/commonjs/ui/screens/LanguageSelectorScreen.js.map +1 -1
  55. package/lib/commonjs/ui/screens/SignInScreen.js +43 -50
  56. package/lib/commonjs/ui/screens/SignInScreen.js.map +1 -1
  57. package/lib/commonjs/ui/screens/SignUpScreen.js +14 -16
  58. package/lib/commonjs/ui/screens/SignUpScreen.js.map +1 -1
  59. package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js +188 -142
  60. package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js.map +1 -1
  61. package/lib/commonjs/ui/screens/internal/SignInPasswordStep.js +10 -10
  62. package/lib/commonjs/ui/screens/internal/SignInPasswordStep.js.map +1 -1
  63. package/lib/commonjs/ui/screens/internal/SignInUsernameStep.js +2 -4
  64. package/lib/commonjs/ui/screens/internal/SignInUsernameStep.js.map +1 -1
  65. package/lib/commonjs/ui/screens/steps/RecoverRequestStep.js +45 -25
  66. package/lib/commonjs/ui/screens/steps/RecoverRequestStep.js.map +1 -1
  67. package/lib/commonjs/ui/screens/steps/RecoverResetPasswordStep.js +88 -53
  68. package/lib/commonjs/ui/screens/steps/RecoverResetPasswordStep.js.map +1 -1
  69. package/lib/commonjs/ui/screens/steps/RecoverSuccessStep.js +79 -58
  70. package/lib/commonjs/ui/screens/steps/RecoverSuccessStep.js.map +1 -1
  71. package/lib/commonjs/ui/screens/steps/RecoverVerifyStep.js +61 -52
  72. package/lib/commonjs/ui/screens/steps/RecoverVerifyStep.js.map +1 -1
  73. package/lib/commonjs/ui/screens/steps/SignInPasswordStep.js +218 -39
  74. package/lib/commonjs/ui/screens/steps/SignInPasswordStep.js.map +1 -1
  75. package/lib/commonjs/ui/screens/steps/SignInTotpStep.js +77 -50
  76. package/lib/commonjs/ui/screens/steps/SignInTotpStep.js.map +1 -1
  77. package/lib/commonjs/ui/screens/steps/SignInUsernameStep.js +424 -71
  78. package/lib/commonjs/ui/screens/steps/SignInUsernameStep.js.map +1 -1
  79. package/lib/commonjs/ui/screens/steps/SignUpIdentityStep.js +55 -30
  80. package/lib/commonjs/ui/screens/steps/SignUpIdentityStep.js.map +1 -1
  81. package/lib/commonjs/ui/screens/steps/SignUpSecurityStep.js +64 -46
  82. package/lib/commonjs/ui/screens/steps/SignUpSecurityStep.js.map +1 -1
  83. package/lib/commonjs/ui/screens/steps/SignUpSummaryStep.js +84 -146
  84. package/lib/commonjs/ui/screens/steps/SignUpSummaryStep.js.map +1 -1
  85. package/lib/commonjs/ui/screens/steps/SignUpWelcomeStep.js +113 -34
  86. package/lib/commonjs/ui/screens/steps/SignUpWelcomeStep.js.map +1 -1
  87. package/lib/commonjs/ui/stores/accountStore.js +237 -0
  88. package/lib/commonjs/ui/stores/accountStore.js.map +1 -0
  89. package/lib/commonjs/ui/stores/authStore.js +17 -20
  90. package/lib/commonjs/ui/stores/authStore.js.map +1 -1
  91. package/lib/commonjs/ui/styles/authStyles.js +16 -8
  92. package/lib/commonjs/ui/styles/authStyles.js.map +1 -1
  93. package/lib/commonjs/ui/styles/index.js +11 -0
  94. package/lib/commonjs/ui/styles/index.js.map +1 -1
  95. package/lib/commonjs/ui/styles/spacing.js +51 -0
  96. package/lib/commonjs/ui/styles/spacing.js.map +1 -0
  97. package/lib/commonjs/utils/asyncUtils.js +9 -22
  98. package/lib/commonjs/utils/asyncUtils.js.map +1 -1
  99. package/lib/commonjs/utils/cache.js +259 -0
  100. package/lib/commonjs/utils/cache.js.map +1 -0
  101. package/lib/commonjs/utils/index.js +99 -0
  102. package/lib/commonjs/utils/index.js.map +1 -1
  103. package/lib/commonjs/utils/languageUtils.js +159 -0
  104. package/lib/commonjs/utils/languageUtils.js.map +1 -0
  105. package/lib/commonjs/utils/requestUtils.js +217 -0
  106. package/lib/commonjs/utils/requestUtils.js.map +1 -0
  107. package/lib/commonjs/utils/sessionUtils.js +191 -0
  108. package/lib/commonjs/utils/sessionUtils.js.map +1 -0
  109. package/lib/commonjs/utils/validationUtils.js +1 -1
  110. package/lib/module/core/HttpClient.js +232 -0
  111. package/lib/module/core/HttpClient.js.map +1 -0
  112. package/lib/module/core/OxyServices.js +528 -326
  113. package/lib/module/core/OxyServices.js.map +1 -1
  114. package/lib/module/core/RequestManager.js +194 -0
  115. package/lib/module/core/RequestManager.js.map +1 -0
  116. package/lib/module/core/index.js +2 -0
  117. package/lib/module/core/index.js.map +1 -1
  118. package/lib/module/i18n/index.js +37 -1
  119. package/lib/module/i18n/index.js.map +1 -1
  120. package/lib/module/i18n/locales/ar-SA.json +128 -0
  121. package/lib/module/i18n/locales/ca-ES.json +128 -0
  122. package/lib/module/i18n/locales/de-DE.json +128 -0
  123. package/lib/module/i18n/locales/en-US.json +85 -12
  124. package/lib/module/i18n/locales/es-ES.json +58 -6
  125. package/lib/module/i18n/locales/fr-FR.json +128 -0
  126. package/lib/module/i18n/locales/it-IT.json +128 -0
  127. package/lib/module/i18n/locales/ja-JP.json +127 -0
  128. package/lib/module/i18n/locales/ko-KR.json +128 -0
  129. package/lib/module/i18n/locales/pt-PT.json +128 -0
  130. package/lib/module/i18n/locales/zh-CN.json +128 -0
  131. package/lib/module/index.js +2 -0
  132. package/lib/module/index.js.map +1 -1
  133. package/lib/module/ui/components/Avatar.js +94 -27
  134. package/lib/module/ui/components/Avatar.js.map +1 -1
  135. package/lib/module/ui/components/FollowButton.js +1 -0
  136. package/lib/module/ui/components/FollowButton.js.map +1 -1
  137. package/lib/module/ui/components/FontLoader.js +23 -43
  138. package/lib/module/ui/components/FontLoader.js.map +1 -1
  139. package/lib/module/ui/components/OxyProvider.js +6 -8
  140. package/lib/module/ui/components/OxyProvider.js.map +1 -1
  141. package/lib/module/ui/components/StepBasedScreen.js +65 -45
  142. package/lib/module/ui/components/StepBasedScreen.js.map +1 -1
  143. package/lib/module/ui/components/internal/GroupedPillButtons.js +14 -35
  144. package/lib/module/ui/components/internal/GroupedPillButtons.js.map +1 -1
  145. package/lib/module/ui/components/internal/PinInput.js +2 -2
  146. package/lib/module/ui/components/internal/PinInput.js.map +1 -1
  147. package/lib/module/ui/components/internal/TextField.js +13 -8
  148. package/lib/module/ui/components/internal/TextField.js.map +1 -1
  149. package/lib/module/ui/context/OxyContext.js +442 -370
  150. package/lib/module/ui/context/OxyContext.js.map +1 -1
  151. package/lib/module/ui/hooks/useSessionSocket.js +80 -22
  152. package/lib/module/ui/hooks/useSessionSocket.js.map +1 -1
  153. package/lib/module/ui/index.js +4 -2
  154. package/lib/module/ui/index.js.map +1 -1
  155. package/lib/module/ui/screens/AccountSettingsScreen.js +33 -2
  156. package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
  157. package/lib/module/ui/screens/AccountSwitcherScreen.js +102 -60
  158. package/lib/module/ui/screens/AccountSwitcherScreen.js.map +1 -1
  159. package/lib/module/ui/screens/FileManagementScreen.js +3 -2
  160. package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
  161. package/lib/module/ui/screens/LanguageSelectorScreen.js +73 -117
  162. package/lib/module/ui/screens/LanguageSelectorScreen.js.map +1 -1
  163. package/lib/module/ui/screens/SignInScreen.js +44 -51
  164. package/lib/module/ui/screens/SignInScreen.js.map +1 -1
  165. package/lib/module/ui/screens/SignUpScreen.js +14 -16
  166. package/lib/module/ui/screens/SignUpScreen.js.map +1 -1
  167. package/lib/module/ui/screens/WelcomeNewUserScreen.js +187 -143
  168. package/lib/module/ui/screens/WelcomeNewUserScreen.js.map +1 -1
  169. package/lib/module/ui/screens/internal/SignInPasswordStep.js +10 -10
  170. package/lib/module/ui/screens/internal/SignInPasswordStep.js.map +1 -1
  171. package/lib/module/ui/screens/internal/SignInUsernameStep.js +2 -4
  172. package/lib/module/ui/screens/internal/SignInUsernameStep.js.map +1 -1
  173. package/lib/module/ui/screens/steps/RecoverRequestStep.js +45 -25
  174. package/lib/module/ui/screens/steps/RecoverRequestStep.js.map +1 -1
  175. package/lib/module/ui/screens/steps/RecoverResetPasswordStep.js +89 -54
  176. package/lib/module/ui/screens/steps/RecoverResetPasswordStep.js.map +1 -1
  177. package/lib/module/ui/screens/steps/RecoverSuccessStep.js +80 -59
  178. package/lib/module/ui/screens/steps/RecoverSuccessStep.js.map +1 -1
  179. package/lib/module/ui/screens/steps/RecoverVerifyStep.js +62 -53
  180. package/lib/module/ui/screens/steps/RecoverVerifyStep.js.map +1 -1
  181. package/lib/module/ui/screens/steps/SignInPasswordStep.js +219 -40
  182. package/lib/module/ui/screens/steps/SignInPasswordStep.js.map +1 -1
  183. package/lib/module/ui/screens/steps/SignInTotpStep.js +78 -51
  184. package/lib/module/ui/screens/steps/SignInTotpStep.js.map +1 -1
  185. package/lib/module/ui/screens/steps/SignInUsernameStep.js +426 -73
  186. package/lib/module/ui/screens/steps/SignInUsernameStep.js.map +1 -1
  187. package/lib/module/ui/screens/steps/SignUpIdentityStep.js +55 -30
  188. package/lib/module/ui/screens/steps/SignUpIdentityStep.js.map +1 -1
  189. package/lib/module/ui/screens/steps/SignUpSecurityStep.js +65 -47
  190. package/lib/module/ui/screens/steps/SignUpSecurityStep.js.map +1 -1
  191. package/lib/module/ui/screens/steps/SignUpSummaryStep.js +84 -146
  192. package/lib/module/ui/screens/steps/SignUpSummaryStep.js.map +1 -1
  193. package/lib/module/ui/screens/steps/SignUpWelcomeStep.js +114 -35
  194. package/lib/module/ui/screens/steps/SignUpWelcomeStep.js.map +1 -1
  195. package/lib/module/ui/stores/accountStore.js +229 -0
  196. package/lib/module/ui/stores/accountStore.js.map +1 -0
  197. package/lib/module/ui/stores/authStore.js +17 -20
  198. package/lib/module/ui/stores/authStore.js.map +1 -1
  199. package/lib/module/ui/styles/authStyles.js +16 -8
  200. package/lib/module/ui/styles/authStyles.js.map +1 -1
  201. package/lib/module/ui/styles/index.js +1 -0
  202. package/lib/module/ui/styles/index.js.map +1 -1
  203. package/lib/module/ui/styles/spacing.js +48 -0
  204. package/lib/module/ui/styles/spacing.js.map +1 -0
  205. package/lib/module/utils/asyncUtils.js +10 -22
  206. package/lib/module/utils/asyncUtils.js.map +1 -1
  207. package/lib/module/utils/cache.js +250 -0
  208. package/lib/module/utils/cache.js.map +1 -0
  209. package/lib/module/utils/index.js +7 -0
  210. package/lib/module/utils/index.js.map +1 -1
  211. package/lib/module/utils/languageUtils.js +151 -0
  212. package/lib/module/utils/languageUtils.js.map +1 -0
  213. package/lib/module/utils/requestUtils.js +210 -0
  214. package/lib/module/utils/requestUtils.js.map +1 -0
  215. package/lib/module/utils/sessionUtils.js +180 -0
  216. package/lib/module/utils/sessionUtils.js.map +1 -0
  217. package/lib/module/utils/validationUtils.js +1 -1
  218. package/lib/typescript/core/HttpClient.d.ts +64 -0
  219. package/lib/typescript/core/HttpClient.d.ts.map +1 -0
  220. package/lib/typescript/core/OxyServices.d.ts +86 -73
  221. package/lib/typescript/core/OxyServices.d.ts.map +1 -1
  222. package/lib/typescript/core/RequestManager.d.ts +67 -0
  223. package/lib/typescript/core/RequestManager.d.ts.map +1 -0
  224. package/lib/typescript/core/index.d.ts +2 -0
  225. package/lib/typescript/core/index.d.ts.map +1 -1
  226. package/lib/typescript/i18n/index.d.ts.map +1 -1
  227. package/lib/typescript/index.d.ts +2 -0
  228. package/lib/typescript/index.d.ts.map +1 -1
  229. package/lib/typescript/models/interfaces.d.ts +15 -0
  230. package/lib/typescript/models/interfaces.d.ts.map +1 -1
  231. package/lib/typescript/models/session.d.ts +1 -0
  232. package/lib/typescript/models/session.d.ts.map +1 -1
  233. package/lib/typescript/ui/components/Avatar.d.ts +6 -7
  234. package/lib/typescript/ui/components/Avatar.d.ts.map +1 -1
  235. package/lib/typescript/ui/components/FollowButton.d.ts.map +1 -1
  236. package/lib/typescript/ui/components/FontLoader.d.ts +3 -3
  237. package/lib/typescript/ui/components/FontLoader.d.ts.map +1 -1
  238. package/lib/typescript/ui/components/OxyProvider.d.ts +2 -2
  239. package/lib/typescript/ui/components/OxyProvider.d.ts.map +1 -1
  240. package/lib/typescript/ui/components/StepBasedScreen.d.ts.map +1 -1
  241. package/lib/typescript/ui/components/internal/GroupedPillButtons.d.ts.map +1 -1
  242. package/lib/typescript/ui/components/internal/TextField.d.ts.map +1 -1
  243. package/lib/typescript/ui/context/OxyContext.d.ts +5 -0
  244. package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
  245. package/lib/typescript/ui/hooks/useSessionSocket.d.ts.map +1 -1
  246. package/lib/typescript/ui/index.d.ts +2 -2
  247. package/lib/typescript/ui/index.d.ts.map +1 -1
  248. package/lib/typescript/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
  249. package/lib/typescript/ui/screens/AccountSwitcherScreen.d.ts.map +1 -1
  250. package/lib/typescript/ui/screens/LanguageSelectorScreen.d.ts +3 -3
  251. package/lib/typescript/ui/screens/LanguageSelectorScreen.d.ts.map +1 -1
  252. package/lib/typescript/ui/screens/SignInScreen.d.ts.map +1 -1
  253. package/lib/typescript/ui/screens/SignUpScreen.d.ts.map +1 -1
  254. package/lib/typescript/ui/screens/WelcomeNewUserScreen.d.ts.map +1 -1
  255. package/lib/typescript/ui/screens/internal/SignInPasswordStep.d.ts.map +1 -1
  256. package/lib/typescript/ui/screens/steps/RecoverRequestStep.d.ts.map +1 -1
  257. package/lib/typescript/ui/screens/steps/RecoverResetPasswordStep.d.ts.map +1 -1
  258. package/lib/typescript/ui/screens/steps/RecoverSuccessStep.d.ts.map +1 -1
  259. package/lib/typescript/ui/screens/steps/RecoverVerifyStep.d.ts.map +1 -1
  260. package/lib/typescript/ui/screens/steps/SignInPasswordStep.d.ts +2 -0
  261. package/lib/typescript/ui/screens/steps/SignInPasswordStep.d.ts.map +1 -1
  262. package/lib/typescript/ui/screens/steps/SignInTotpStep.d.ts.map +1 -1
  263. package/lib/typescript/ui/screens/steps/SignInUsernameStep.d.ts.map +1 -1
  264. package/lib/typescript/ui/screens/steps/SignUpIdentityStep.d.ts.map +1 -1
  265. package/lib/typescript/ui/screens/steps/SignUpSecurityStep.d.ts.map +1 -1
  266. package/lib/typescript/ui/screens/steps/SignUpSummaryStep.d.ts.map +1 -1
  267. package/lib/typescript/ui/screens/steps/SignUpWelcomeStep.d.ts.map +1 -1
  268. package/lib/typescript/ui/stores/accountStore.d.ts +34 -0
  269. package/lib/typescript/ui/stores/accountStore.d.ts.map +1 -0
  270. package/lib/typescript/ui/stores/authStore.d.ts +7 -3
  271. package/lib/typescript/ui/stores/authStore.d.ts.map +1 -1
  272. package/lib/typescript/ui/styles/authStyles.d.ts +19 -2
  273. package/lib/typescript/ui/styles/authStyles.d.ts.map +1 -1
  274. package/lib/typescript/ui/styles/index.d.ts +1 -0
  275. package/lib/typescript/ui/styles/index.d.ts.map +1 -1
  276. package/lib/typescript/ui/styles/spacing.d.ts +43 -0
  277. package/lib/typescript/ui/styles/spacing.d.ts.map +1 -0
  278. package/lib/typescript/utils/asyncUtils.d.ts +2 -0
  279. package/lib/typescript/utils/asyncUtils.d.ts.map +1 -1
  280. package/lib/typescript/utils/cache.d.ts +128 -0
  281. package/lib/typescript/utils/cache.d.ts.map +1 -0
  282. package/lib/typescript/utils/index.d.ts +4 -0
  283. package/lib/typescript/utils/index.d.ts.map +1 -1
  284. package/lib/typescript/utils/languageUtils.d.ts +38 -0
  285. package/lib/typescript/utils/languageUtils.d.ts.map +1 -0
  286. package/lib/typescript/utils/requestUtils.d.ts +122 -0
  287. package/lib/typescript/utils/requestUtils.d.ts.map +1 -0
  288. package/lib/typescript/utils/sessionUtils.d.ts +55 -0
  289. package/lib/typescript/utils/sessionUtils.d.ts.map +1 -0
  290. package/lib/typescript/utils/validationUtils.d.ts +1 -1
  291. package/package.json +1 -1
  292. package/src/core/HttpClient.ts +277 -0
  293. package/src/core/OxyServices.ts +461 -352
  294. package/src/core/RequestManager.ts +240 -0
  295. package/src/core/index.ts +10 -0
  296. package/src/i18n/index.ts +36 -0
  297. package/src/i18n/locales/ar-SA.json +128 -0
  298. package/src/i18n/locales/ca-ES.json +128 -0
  299. package/src/i18n/locales/de-DE.json +128 -0
  300. package/src/i18n/locales/en-US.json +85 -12
  301. package/src/i18n/locales/es-ES.json +58 -6
  302. package/src/i18n/locales/fr-FR.json +128 -0
  303. package/src/i18n/locales/it-IT.json +128 -0
  304. package/src/i18n/locales/ja-JP.json +127 -0
  305. package/src/i18n/locales/ko-KR.json +128 -0
  306. package/src/i18n/locales/pt-PT.json +128 -0
  307. package/src/i18n/locales/zh-CN.json +128 -0
  308. package/src/index.ts +10 -0
  309. package/src/models/interfaces.ts +19 -0
  310. package/src/models/session.ts +1 -1
  311. package/src/ui/components/Avatar.tsx +151 -35
  312. package/src/ui/components/FollowButton.tsx +1 -0
  313. package/src/ui/components/FontLoader.tsx +17 -37
  314. package/src/ui/components/OxyProvider.tsx +14 -13
  315. package/src/ui/components/StepBasedScreen.tsx +66 -43
  316. package/src/ui/components/internal/GroupedPillButtons.tsx +15 -31
  317. package/src/ui/components/internal/PinInput.tsx +2 -2
  318. package/src/ui/components/internal/TextField.tsx +7 -6
  319. package/src/ui/context/OxyContext.tsx +441 -326
  320. package/src/ui/hooks/useSessionSocket.ts +72 -18
  321. package/src/ui/index.ts +4 -1
  322. package/src/ui/screens/AccountSettingsScreen.tsx +34 -2
  323. package/src/ui/screens/AccountSwitcherScreen.tsx +102 -68
  324. package/src/ui/screens/FileManagementScreen.tsx +16 -16
  325. package/src/ui/screens/LanguageSelectorScreen.tsx +86 -143
  326. package/src/ui/screens/SignInScreen.tsx +59 -43
  327. package/src/ui/screens/SignUpScreen.tsx +14 -15
  328. package/src/ui/screens/WelcomeNewUserScreen.tsx +153 -105
  329. package/src/ui/screens/internal/SignInPasswordStep.tsx +4 -6
  330. package/src/ui/screens/internal/SignInUsernameStep.tsx +1 -1
  331. package/src/ui/screens/steps/RecoverRequestStep.tsx +34 -24
  332. package/src/ui/screens/steps/RecoverResetPasswordStep.tsx +65 -36
  333. package/src/ui/screens/steps/RecoverSuccessStep.tsx +71 -47
  334. package/src/ui/screens/steps/RecoverVerifyStep.tsx +60 -50
  335. package/src/ui/screens/steps/SignInPasswordStep.tsx +190 -32
  336. package/src/ui/screens/steps/SignInTotpStep.tsx +68 -34
  337. package/src/ui/screens/steps/SignInUsernameStep.tsx +446 -63
  338. package/src/ui/screens/steps/SignUpIdentityStep.tsx +49 -35
  339. package/src/ui/screens/steps/SignUpSecurityStep.tsx +56 -39
  340. package/src/ui/screens/steps/SignUpSummaryStep.tsx +99 -89
  341. package/src/ui/screens/steps/SignUpWelcomeStep.tsx +88 -20
  342. package/src/ui/stores/accountStore.ts +285 -0
  343. package/src/ui/stores/authStore.ts +16 -19
  344. package/src/ui/styles/authStyles.ts +16 -8
  345. package/src/ui/styles/index.ts +1 -0
  346. package/src/ui/styles/spacing.ts +46 -0
  347. package/src/utils/asyncUtils.ts +10 -24
  348. package/src/utils/cache.ts +264 -0
  349. package/src/utils/index.ts +19 -0
  350. package/src/utils/languageUtils.ts +174 -0
  351. package/src/utils/requestUtils.ts +234 -0
  352. package/src/utils/sessionUtils.ts +206 -0
  353. package/src/utils/validationUtils.ts +1 -1
@@ -1,16 +1,18 @@
1
1
  import type React from 'react';
2
2
  import { createContext, useContext, useEffect, useCallback, useMemo, useRef, useState, type ReactNode } from 'react';
3
3
  import type { UseFollowHook } from '../hooks/useFollow.types';
4
- import { View, Text } from 'react-native';
5
4
  import { OxyServices } from '../../core';
6
5
  import type { User, ApiError } from '../../models/interfaces';
7
6
  import type { SessionLoginResponse, ClientSession, MinimalUserData } from '../../models/session';
7
+ import { normalizeAndSortSessions, mergeSessions, sessionsArraysEqual } from '../../utils/sessionUtils';
8
8
  import { DeviceManager } from '../../utils/deviceManager';
9
9
  import { useSessionSocket } from '../hooks/useSessionSocket';
10
10
  import { toast } from '../../lib/sonner';
11
11
  import { useAuthStore } from '../stores/authStore';
12
12
  import type { BottomSheetController } from '../navigation/types';
13
13
  import type { RouteName } from '../navigation/routes';
14
+ import { getLanguageMetadata, getLanguageName, getNativeLanguageName, normalizeLanguageCode } from '../../utils/languageUtils';
15
+ import type { LanguageMetadata } from '../../utils/languageUtils';
14
16
 
15
17
  // Define the context shape
16
18
  // NOTE: We intentionally avoid importing useFollow here to prevent a require cycle.
@@ -24,10 +26,14 @@ export interface OxyContextState {
24
26
  activeSessionId: string | null;
25
27
  isAuthenticated: boolean; // Single source of truth for authentication - use this instead of service methods
26
28
  isLoading: boolean;
29
+ isTokenReady: boolean; // Whether the token has been loaded/restored and is ready for use
27
30
  error: string | null;
28
31
 
29
32
  // Language state
30
33
  currentLanguage: string;
34
+ currentLanguageMetadata: LanguageMetadata | null; // Full language metadata (name, nativeName, etc.)
35
+ currentLanguageName: string; // Language name (e.g., 'English')
36
+ currentNativeLanguageName: string; // Native language name (e.g., 'Español')
31
37
 
32
38
  // Auth methods
33
39
  login: (username: string, password: string, deviceName?: string) => Promise<User>;
@@ -64,6 +70,26 @@ export interface OxyContextState {
64
70
  useFollow: UseFollowHook; // Back-compat; prefer direct import
65
71
  }
66
72
 
73
+ // Empty follow hook fallback
74
+ const createEmptyFollowHook = (): UseFollowHook => {
75
+ const emptyResult = {
76
+ isFollowing: false,
77
+ isLoading: false,
78
+ error: null,
79
+ toggleFollow: async () => { },
80
+ setFollowStatus: () => { },
81
+ fetchStatus: async () => { },
82
+ clearError: () => { },
83
+ followerCount: null,
84
+ followingCount: null,
85
+ isLoadingCounts: false,
86
+ fetchUserCounts: async () => { },
87
+ setFollowerCount: () => { },
88
+ setFollowingCount: () => { },
89
+ };
90
+ return () => emptyResult;
91
+ };
92
+
67
93
  // Create the context with default values
68
94
  const OxyContext = createContext<OxyContextState | null>(null);
69
95
 
@@ -134,6 +160,7 @@ const getStorage = async (): Promise<StorageInterface> => {
134
160
  // Storage keys for sessions
135
161
  const getStorageKeys = (prefix = 'oxy_session') => ({
136
162
  activeSessionId: `${prefix}_active_session_id`, // Only store the active session ID
163
+ sessionIds: `${prefix}_session_ids`, // Store all session IDs for quick account loading
137
164
  language: `${prefix}_language`, // Store the selected language
138
165
  });
139
166
 
@@ -174,34 +201,98 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
174
201
  const [minimalUser, setMinimalUser] = useState<MinimalUserData | null>(null);
175
202
  const [sessions, setSessions] = useState<ClientSession[]>([]);
176
203
  const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
204
+
205
+ // Track in-flight refresh to prevent duplicate calls
206
+ const refreshInFlightRef = useRef<Promise<void> | null>(null);
207
+
177
208
  const [storage, setStorage] = useState<StorageInterface | null>(null);
178
209
  const [currentLanguage, setCurrentLanguage] = useState<string>('en-US');
179
210
 
180
- // Normalize language codes to BCP-47 (e.g., en-US)
181
- const normalizeLanguageCode = useCallback((lang?: string | null): string | null => {
182
- if (!lang) return null;
183
- if (lang.includes('-')) return lang;
184
- const map: Record<string, string> = {
185
- en: 'en-US', es: 'es-ES', ca: 'ca-ES', fr: 'fr-FR', de: 'de-DE', it: 'it-IT', pt: 'pt-PT',
186
- ja: 'ja-JP', ko: 'ko-KR', zh: 'zh-CN', ar: 'ar-SA'
187
- };
188
- return map[lang] || lang;
211
+ // Storage keys (memoized to prevent infinite loops) - declared early for use in helpers
212
+ const keys = useMemo(() => getStorageKeys(storageKeyPrefix), [storageKeyPrefix]);
213
+
214
+ // Helper to apply language preference from user/server
215
+ const applyLanguagePreference = useCallback(async (user: User): Promise<void> => {
216
+ const userLanguage = (user as Record<string, unknown>)?.language as string | undefined;
217
+ if (!userLanguage || !storage) return;
218
+
219
+ try {
220
+ const serverLang = normalizeLanguageCode(userLanguage);
221
+ await storage.setItem(keys.language, serverLang);
222
+ setCurrentLanguage(serverLang);
223
+ } catch (e) {
224
+ if (__DEV__) {
225
+ console.warn('Failed to apply server language preference', e);
226
+ }
227
+ }
228
+ }, [storage, keys.language]);
229
+
230
+ const mapSessionsToClient = useCallback((sessions: Array<{
231
+ sessionId: string;
232
+ deviceId?: string;
233
+ expiresAt?: string;
234
+ lastActive?: string;
235
+ user?: { id?: string; _id?: { toString(): string } };
236
+ userId?: string;
237
+ isCurrent?: boolean;
238
+ }>, fallbackDeviceId?: string, fallbackUserId?: string): ClientSession[] => {
239
+ return sessions.map((s: any) => ({
240
+ sessionId: s.sessionId,
241
+ deviceId: s.deviceId || fallbackDeviceId || '',
242
+ expiresAt: s.expiresAt || new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
243
+ lastActive: s.lastActive || new Date().toISOString(),
244
+ userId: s.user?.id || s.userId || (s.user?._id?.toString()) || fallbackUserId || '',
245
+ isCurrent: Boolean(s.isCurrent),
246
+ }));
189
247
  }, []);
190
- // Add a new state to track token restoration
191
- const [tokenReady, setTokenReady] = useState(false);
192
248
 
193
- // Storage keys (memoized to prevent infinite loops)
194
- const keys = useMemo(() => getStorageKeys(storageKeyPrefix), [storageKeyPrefix]);
249
+ // Save all session IDs to storage for quick loading on initialization
250
+ const saveSessionIds = useCallback(async (sessionIds: string[]): Promise<void> => {
251
+ if (!storage) return;
252
+ try {
253
+ const uniqueIds = Array.from(new Set(sessionIds));
254
+ await storage.setItem(keys.sessionIds, JSON.stringify(uniqueIds));
255
+ } catch (err) {
256
+ if (__DEV__) {
257
+ console.warn('Failed to save session IDs:', err);
258
+ }
259
+ }
260
+ }, [storage, keys.sessionIds]);
261
+
262
+ const updateSessions = useCallback((newSessions: ClientSession[], mergeWithExisting = false) => {
263
+ setSessions((prevSessions) => {
264
+ const sessionsToProcess = mergeWithExisting
265
+ ? mergeSessions(prevSessions, newSessions, activeSessionId, false)
266
+ : normalizeAndSortSessions(newSessions, activeSessionId, false);
267
+
268
+ // Save all session IDs to storage
269
+ if (storage) {
270
+ const allSessionIds = sessionsToProcess.map(s => s.sessionId);
271
+ saveSessionIds(allSessionIds).catch(() => {
272
+ // Ignore errors - non-critical
273
+ });
274
+ }
275
+
276
+ return sessionsArraysEqual(prevSessions, sessionsToProcess) ? prevSessions : sessionsToProcess;
277
+ });
278
+ }, [activeSessionId, storage, saveSessionIds]);
195
279
 
196
- // Clear all storage - defined before initAuth to avoid dependency issues
280
+ // Token ready state - start optimistically so children render immediately
281
+ const [tokenReady, setTokenReady] = useState(true);
282
+
283
+ // Clear all storage
197
284
  const clearAllStorage = useCallback(async (): Promise<void> => {
198
285
  if (!storage) return;
199
286
  try {
200
287
  await storage.removeItem(keys.activeSessionId);
288
+ await storage.removeItem(keys.sessionIds);
201
289
  } catch (err) {
202
- console.error('Clear storage error:', err);
290
+ if (__DEV__) {
291
+ console.error('Clear storage error:', err);
292
+ }
293
+ onError?.({ message: 'Failed to clear storage', code: 'STORAGE_ERROR', status: 500 });
203
294
  }
204
- }, [storage, keys]);
295
+ }, [storage, keys, onError]);
205
296
 
206
297
  // Initialize storage
207
298
  useEffect(() => {
@@ -210,21 +301,22 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
210
301
  const platformStorage = await getStorage();
211
302
  setStorage(platformStorage);
212
303
  } catch (error) {
213
- console.error('Init storage failed', error);
214
- useAuthStore.setState({ error: 'Failed to initialize storage' });
304
+ const errorMessage = error instanceof Error ? error.message : 'Failed to initialize storage';
305
+ useAuthStore.setState({ error: errorMessage });
306
+ onError?.({ message: errorMessage, code: 'STORAGE_INIT_ERROR', status: 500 });
215
307
  }
216
308
  };
217
309
  initStorage();
218
- }, []);
310
+ }, [onError]);
219
311
 
220
312
  // Initialize authentication state
313
+ // Note: We don't set isLoading during initialization to avoid showing spinners
314
+ // Children render immediately and can check isTokenReady/isAuthenticated themselves
221
315
  useEffect(() => {
222
316
  const initAuth = async () => {
223
317
  if (!storage) return;
224
- useAuthStore.setState({ isLoading: true });
318
+ // Don't set isLoading during initialization - let it happen in background
225
319
  try {
226
- setTokenReady(false);
227
-
228
320
  // Load saved language preference
229
321
  const savedLanguageRaw = await storage.getItem(keys.language);
230
322
  const savedLanguage = normalizeLanguageCode(savedLanguageRaw) || savedLanguageRaw;
@@ -232,8 +324,49 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
232
324
  setCurrentLanguage(savedLanguage);
233
325
  }
234
326
 
327
+ // Load all stored session IDs and validate them
328
+ const storedSessionIdsJson = await storage.getItem(keys.sessionIds);
329
+ const storedSessionIds: string[] = storedSessionIdsJson ? JSON.parse(storedSessionIdsJson) : [];
330
+
235
331
  // Try to restore active session from storage
236
332
  const storedActiveSessionId = await storage.getItem(keys.activeSessionId);
333
+ const validSessions: ClientSession[] = [];
334
+
335
+ // If we have stored session IDs, validate them (even without active session)
336
+ if (storedSessionIds.length > 0) {
337
+ if (__DEV__) {
338
+ console.log('Loading stored sessions on init:', storedSessionIds.length);
339
+ }
340
+
341
+ // Validate each stored session ID and build session list
342
+ for (const sessionId of storedSessionIds) {
343
+ try {
344
+ const validation = await oxyServices.validateSession(sessionId, { useHeaderValidation: true });
345
+ if (validation.valid && validation.user) {
346
+ validSessions.push({
347
+ sessionId,
348
+ userId: validation.user.id?.toString() || '',
349
+ deviceId: '',
350
+ expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
351
+ lastActive: new Date().toISOString(),
352
+ isCurrent: sessionId === storedActiveSessionId,
353
+ });
354
+ }
355
+ } catch (e) {
356
+ // Session invalid, skip it
357
+ if (__DEV__) {
358
+ console.warn('Session validation failed for:', sessionId, e);
359
+ }
360
+ }
361
+ }
362
+
363
+ // Update sessions list with validated sessions (even if no active session)
364
+ if (validSessions.length > 0) {
365
+ updateSessions(validSessions, false);
366
+ }
367
+ }
368
+
369
+ // If we have an active session, authenticate with it
237
370
  if (storedActiveSessionId) {
238
371
  try {
239
372
  const validation = await oxyServices.validateSession(storedActiveSessionId, { useHeaderValidation: true });
@@ -243,68 +376,47 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
243
376
  const fullUser = await oxyServices.getUserBySession(storedActiveSessionId);
244
377
  loginSuccess(fullUser);
245
378
  setMinimalUser({ id: fullUser.id, username: fullUser.username, avatar: fullUser.avatar });
246
- // Apply server language if present
247
- if ((fullUser as any)?.language) {
248
- try {
249
- const serverLang = normalizeLanguageCode((fullUser as any).language) || (fullUser as any).language;
250
- await storage.setItem(keys.language, serverLang);
251
- setCurrentLanguage(serverLang);
252
- } catch (e) {
253
- console.warn('Failed to apply server language preference', e);
379
+
380
+ await applyLanguagePreference(fullUser);
381
+
382
+ try {
383
+ const deviceSessions = await oxyServices.getDeviceSessions(storedActiveSessionId);
384
+ const allDeviceSessions = mapSessionsToClient(deviceSessions, undefined, fullUser.id);
385
+ updateSessions(allDeviceSessions, true);
386
+ } catch (e) {
387
+ if (__DEV__) {
388
+ console.warn('Failed to get device sessions on init, falling back to user sessions:', e);
254
389
  }
390
+ const serverSessions = await oxyServices.getSessionsBySessionId(storedActiveSessionId);
391
+ updateSessions(mapSessionsToClient(serverSessions, undefined, fullUser.id), false);
255
392
  }
256
- const serverSessions = await oxyServices.getSessionsBySessionId(storedActiveSessionId);
257
- const clientSessions: ClientSession[] = serverSessions.map(s => ({
258
- sessionId: s.sessionId,
259
- deviceId: s.deviceId,
260
- expiresAt: s.expiresAt || new Date().toISOString(),
261
- lastActive: s.lastActive || new Date().toISOString(),
262
- userId: s.userId || fullUser.id
263
- }));
264
- setSessions(clientSessions);
265
393
  onAuthStateChange?.(fullUser);
266
394
  } else {
267
- await clearAllStorage();
395
+ // Active session invalid, remove it but keep other sessions
396
+ await storage.removeItem(keys.activeSessionId);
397
+ // Update session list to remove invalid active session
398
+ updateSessions(validSessions.filter(s => s.sessionId !== storedActiveSessionId), false);
268
399
  }
269
400
  } catch (e) {
270
- console.error('Session validation error', e);
271
- await clearAllStorage();
401
+ if (__DEV__) {
402
+ console.error('Active session validation error', e);
403
+ }
404
+ // Remove invalid active session but keep other sessions
405
+ await storage.removeItem(keys.activeSessionId);
406
+ updateSessions(validSessions.filter(s => s.sessionId !== storedActiveSessionId), false);
272
407
  }
273
408
  }
274
409
  setTokenReady(true);
275
410
  } catch (e) {
276
- console.error('Auth init error', e);
411
+ if (__DEV__) {
412
+ console.error('Auth init error', e);
413
+ }
277
414
  await clearAllStorage();
278
- } finally {
279
- useAuthStore.setState({ isLoading: false });
415
+ setTokenReady(true);
280
416
  }
281
417
  };
282
418
  initAuth();
283
- }, [storage, oxyServices, keys, onAuthStateChange, loginSuccess, clearAllStorage]);
284
-
285
-
286
-
287
- // Remove invalid session - refresh sessions from backend
288
- const removeInvalidSession = useCallback(async (sessionId: string): Promise<void> => {
289
- // Remove from local state
290
- const filteredSessions = sessions.filter(s => s.sessionId !== sessionId);
291
- setSessions(filteredSessions);
292
-
293
- // If there are other sessions, switch to the first one
294
- if (filteredSessions.length > 0) {
295
- await switchToSession(filteredSessions[0].sessionId);
296
- } else {
297
- // No valid sessions left
298
- setActiveSessionId(null);
299
- logoutStore();
300
- setMinimalUser(null);
301
- await storage?.removeItem(keys.activeSessionId);
302
-
303
- if (onAuthStateChange) {
304
- onAuthStateChange(null);
305
- }
306
- }
307
- }, [sessions, storage, keys, onAuthStateChange, logoutStore]);
419
+ }, [storage, oxyServices, keys, onAuthStateChange, loginSuccess, clearAllStorage, applyLanguagePreference, mapSessionsToClient, updateSessions]);
308
420
 
309
421
  // Save active session ID to storage (only session ID, no user data)
310
422
  const saveActiveSessionId = useCallback(async (sessionId: string): Promise<void> => {
@@ -312,16 +424,21 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
312
424
  await storage.setItem(keys.activeSessionId, sessionId);
313
425
  }, [storage, keys.activeSessionId]);
314
426
 
315
- // Switch to a different session
316
427
  const switchToSession = useCallback(async (sessionId: string): Promise<void> => {
317
428
  try {
318
- useAuthStore.setState({ isLoading: true });
429
+ const validation = await oxyServices.validateSession(sessionId, { useHeaderValidation: true });
430
+ if (!validation.valid) {
431
+ updateSessions(sessions.filter(s => s.sessionId !== sessionId), false);
432
+ throw new Error('Session is invalid or expired');
433
+ }
319
434
 
320
- // Get access token for this session
321
- await oxyServices.getTokenBySession(sessionId);
435
+ if (!validation.user) {
436
+ throw new Error('User data not available from session validation');
437
+ }
322
438
 
323
- // Load full user data
324
- const fullUser = await oxyServices.getUserBySession(sessionId);
439
+ const fullUser = validation.user;
440
+ await oxyServices.getTokenBySession(sessionId);
441
+ setTokenReady(true);
325
442
 
326
443
  setActiveSessionId(sessionId);
327
444
  loginSuccess(fullUser);
@@ -332,44 +449,63 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
332
449
  });
333
450
 
334
451
  await saveActiveSessionId(sessionId);
335
- // Apply server language if present
336
- if ((fullUser as any)?.language) {
337
- try {
338
- const serverLang = normalizeLanguageCode((fullUser as any).language) || (fullUser as any).language;
339
- await storage?.setItem(keys.language, serverLang);
340
- setCurrentLanguage(serverLang);
341
- } catch (e) {
342
- console.warn('Failed to apply server language after switch', e);
452
+ await applyLanguagePreference(fullUser);
453
+
454
+ oxyServices.getDeviceSessions(sessionId)
455
+ .then((deviceSessions) => {
456
+ const allDeviceSessions = mapSessionsToClient(deviceSessions, undefined, fullUser.id);
457
+ updateSessions(allDeviceSessions, true);
458
+ })
459
+ .catch((error) => {
460
+ if (__DEV__) console.warn('Failed to get device sessions after switch:', error);
461
+ });
462
+
463
+ onAuthStateChange?.(fullUser);
464
+ } catch (error: any) {
465
+ const isInvalidSession = error?.response?.status === 401 ||
466
+ error?.message?.includes('Invalid or expired session') ||
467
+ error?.message?.includes('Session is invalid');
468
+
469
+ if (isInvalidSession) {
470
+ updateSessions(sessions.filter(s => s.sessionId !== sessionId), false);
471
+
472
+ if (sessionId === activeSessionId && sessions.length > 1) {
473
+ const otherSessions = sessions.filter(s => s.sessionId !== sessionId);
474
+ for (const otherSession of otherSessions) {
475
+ try {
476
+ const otherValidation = await oxyServices.validateSession(otherSession.sessionId, { useHeaderValidation: true });
477
+ if (otherValidation.valid) {
478
+ await switchToSession(otherSession.sessionId);
479
+ return;
480
+ }
481
+ } catch {
482
+ // Continue to next session
483
+ continue;
484
+ }
485
+ }
343
486
  }
344
487
  }
345
488
 
346
- if (onAuthStateChange) {
347
- onAuthStateChange(fullUser);
489
+ const errorMessage = error instanceof Error ? error.message : 'Failed to switch session';
490
+ if (__DEV__) {
491
+ console.error('Switch session error:', error);
348
492
  }
349
- } catch (error) {
350
- console.error('Switch session error:', error);
351
- useAuthStore.setState({ error: 'Failed to switch session' });
352
- } finally {
353
- useAuthStore.setState({ isLoading: false });
493
+ useAuthStore.setState({ error: errorMessage });
494
+ onError?.({ message: errorMessage, code: isInvalidSession ? 'INVALID_SESSION' : 'SESSION_SWITCH_ERROR', status: isInvalidSession ? 401 : 500 });
495
+ setTokenReady(false);
496
+ throw error; // Re-throw so calling code can handle it
354
497
  }
355
- }, [oxyServices, onAuthStateChange, loginSuccess, saveActiveSessionId]);
498
+ }, [oxyServices, onAuthStateChange, loginSuccess, saveActiveSessionId, applyLanguagePreference, mapSessionsToClient, onError, activeSessionId, sessions]);
356
499
 
357
- // Login method - only store session ID, retrieve data from backend
358
500
  const login = useCallback(async (username: string, password: string, deviceName?: string): Promise<User> => {
359
501
  if (!storage) throw new Error('Storage not initialized');
360
502
  useAuthStore.setState({ isLoading: true, error: null });
361
503
 
362
504
  try {
363
- // Get device fingerprint for enhanced device identification
364
505
  const deviceFingerprint = DeviceManager.getDeviceFingerprint();
365
-
366
- // Get or generate persistent device info
367
506
  const deviceInfo = await DeviceManager.getDeviceInfo();
368
507
 
369
- console.log('Auth - Using device fingerprint:', deviceFingerprint);
370
- console.log('Auth - Using device ID:', deviceInfo.deviceId);
371
-
372
- const response: any = await oxyServices.signIn(
508
+ const response = await oxyServices.signIn(
373
509
  username,
374
510
  password,
375
511
  deviceName || deviceInfo.deviceName || DeviceManager.getDefaultDeviceName(),
@@ -377,49 +513,73 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
377
513
  );
378
514
 
379
515
  // Handle MFA requirement
380
- if (response && response.mfaRequired) {
381
- const err: any = new Error('Multi-factor authentication required');
382
- err.code = 'MFA_REQUIRED';
383
- err.mfaToken = response.mfaToken;
384
- err.expiresAt = response.expiresAt;
385
- throw err;
516
+ if (response && 'mfaRequired' in response && response.mfaRequired) {
517
+ const mfaError = new Error('Multi-factor authentication required') as Error & {
518
+ code: string;
519
+ mfaToken?: string;
520
+ expiresAt?: string;
521
+ };
522
+ mfaError.code = 'MFA_REQUIRED';
523
+ mfaError.mfaToken = (response as { mfaToken?: string }).mfaToken;
524
+ mfaError.expiresAt = (response as { expiresAt?: string }).expiresAt;
525
+ throw mfaError;
386
526
  }
387
527
 
388
- // Set as active session (only store session ID)
389
- setActiveSessionId((response as SessionLoginResponse).sessionId);
390
- await saveActiveSessionId((response as SessionLoginResponse).sessionId);
528
+ const sessionResponse = response as SessionLoginResponse;
391
529
 
392
- // Get access token for API calls
393
- await oxyServices.getTokenBySession(response.sessionId);
530
+ await oxyServices.getTokenBySession(sessionResponse.sessionId);
531
+ const fullUser = await oxyServices.getUserBySession(sessionResponse.sessionId);
394
532
 
395
- // Load full user data from backend
396
- const fullUser = await oxyServices.getUserBySession((response as SessionLoginResponse).sessionId);
397
- loginSuccess(fullUser);
398
- setMinimalUser(response.user);
399
-
400
- // Load sessions from backend
401
- const serverSessions = await oxyServices.getSessionsBySessionId((response as SessionLoginResponse).sessionId);
402
- const clientSessions: ClientSession[] = serverSessions.map(serverSession => ({
403
- sessionId: serverSession.sessionId,
404
- deviceId: serverSession.deviceId,
405
- expiresAt: serverSession.expiresAt || new Date().toISOString(),
406
- lastActive: serverSession.lastActive || new Date().toISOString(),
407
- userId: serverSession.userId || fullUser.id
408
- }));
409
- setSessions(clientSessions);
410
-
411
- if (onAuthStateChange) {
412
- onAuthStateChange(fullUser);
533
+ let allDeviceSessions: ClientSession[] = [];
534
+ try {
535
+ const deviceSessions = await oxyServices.getDeviceSessions(sessionResponse.sessionId);
536
+ allDeviceSessions = mapSessionsToClient(deviceSessions, sessionResponse.deviceId, fullUser.id);
537
+ } catch (error) {
538
+ if (__DEV__) {
539
+ console.warn('Failed to get device sessions, falling back to user sessions:', error);
540
+ }
541
+ const serverSessions = await oxyServices.getSessionsBySessionId(sessionResponse.sessionId);
542
+ allDeviceSessions = mapSessionsToClient(serverSessions, undefined, fullUser.id);
413
543
  }
414
544
 
545
+ const userUserId = fullUser.id?.toString();
546
+ const existingSession = allDeviceSessions.find(
547
+ s => s.userId?.toString() === userUserId && s.sessionId !== sessionResponse.sessionId
548
+ );
549
+
550
+ if (existingSession) {
551
+ try {
552
+ await oxyServices.logoutSession(sessionResponse.sessionId, sessionResponse.sessionId);
553
+ } catch (logoutError) {
554
+ if (__DEV__) {
555
+ console.warn('Failed to logout duplicate session:', logoutError);
556
+ }
557
+ }
558
+ await switchToSession(existingSession.sessionId);
559
+ loginSuccess(fullUser);
560
+ setMinimalUser(sessionResponse.user);
561
+ updateSessions(allDeviceSessions.filter(s => s.sessionId !== sessionResponse.sessionId), false);
562
+ onAuthStateChange?.(fullUser);
563
+ return fullUser;
564
+ }
565
+
566
+ setActiveSessionId(sessionResponse.sessionId);
567
+ await saveActiveSessionId(sessionResponse.sessionId);
568
+ loginSuccess(fullUser);
569
+ setMinimalUser(sessionResponse.user);
570
+ updateSessions(allDeviceSessions, true);
571
+
572
+ onAuthStateChange?.(fullUser);
415
573
  return fullUser;
416
- } catch (error: any) {
417
- loginFailure(error.message || 'Login failed');
574
+ } catch (error) {
575
+ const errorMessage = error instanceof Error ? error.message : 'Login failed';
576
+ loginFailure(errorMessage);
577
+ onError?.({ message: errorMessage, code: 'LOGIN_ERROR', status: 401 });
418
578
  throw error;
419
579
  } finally {
420
580
  useAuthStore.setState({ isLoading: false });
421
581
  }
422
- }, [storage, oxyServices, saveActiveSessionId, loginSuccess, setMinimalUser, onAuthStateChange, loginFailure]);
582
+ }, [storage, oxyServices, saveActiveSessionId, loginSuccess, onAuthStateChange, loginFailure, mapSessionsToClient, onError, sessions, switchToSession]);
423
583
 
424
584
  // Logout method
425
585
  const logout = useCallback(async (targetSessionId?: string): Promise<void> => {
@@ -429,17 +589,13 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
429
589
  const sessionToLogout = targetSessionId || activeSessionId;
430
590
  await oxyServices.logoutSession(activeSessionId, sessionToLogout);
431
591
 
432
- // Remove session from local state
433
592
  const filteredSessions = sessions.filter(s => s.sessionId !== sessionToLogout);
434
- setSessions(filteredSessions);
593
+ updateSessions(filteredSessions, false);
435
594
 
436
- // If logging out active session
437
595
  if (sessionToLogout === activeSessionId) {
438
596
  if (filteredSessions.length > 0) {
439
- // Switch to another session
440
597
  await switchToSession(filteredSessions[0].sessionId);
441
598
  } else {
442
- // No sessions left
443
599
  setActiveSessionId(null);
444
600
  logoutStore();
445
601
  setMinimalUser(null);
@@ -451,70 +607,42 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
451
607
  }
452
608
  }
453
609
  } catch (error) {
454
- console.error('Logout error:', error);
455
- useAuthStore.setState({ error: 'Logout failed' });
610
+ const errorMessage = error instanceof Error ? error.message : 'Logout failed';
611
+ if (__DEV__) {
612
+ console.error('Logout error:', error);
613
+ }
614
+ useAuthStore.setState({ error: errorMessage });
615
+ onError?.({ message: errorMessage, code: 'LOGOUT_ERROR', status: 500 });
456
616
  }
457
- }, [activeSessionId, oxyServices, sessions, switchToSession, logoutStore, setMinimalUser, storage, keys.activeSessionId, onAuthStateChange]);
617
+ }, [activeSessionId, oxyServices, sessions, switchToSession, logoutStore, storage, keys.activeSessionId, onAuthStateChange, onError]);
458
618
 
459
- // Logout all sessions
460
619
  const logoutAll = useCallback(async (): Promise<void> => {
461
- console.log('logoutAll called with activeSessionId:', activeSessionId);
462
-
463
620
  if (!activeSessionId) {
464
- console.error('No active session ID found, cannot logout all');
465
- useAuthStore.setState({ error: 'No active session found' });
466
- throw new Error('No active session found');
467
- }
468
-
469
- if (!oxyServices) {
470
- console.error('OxyServices not initialized');
471
- useAuthStore.setState({ error: 'Service not available' });
472
- throw new Error('Service not available');
621
+ const error = new Error('No active session found');
622
+ useAuthStore.setState({ error: error.message });
623
+ onError?.({ message: error.message, code: 'NO_SESSION_ERROR', status: 404 });
624
+ throw error;
473
625
  }
474
626
 
475
627
  try {
476
- console.log('Calling oxyServices.logoutAllSessions with sessionId:', activeSessionId);
477
628
  await oxyServices.logoutAllSessions(activeSessionId);
478
- console.log('logoutAllSessions completed successfully');
479
629
 
480
- // Clear all local data
481
- setSessions([]);
630
+ updateSessions([], false);
482
631
  setActiveSessionId(null);
483
632
  logoutStore();
484
633
  setMinimalUser(null);
485
634
  await clearAllStorage();
486
- console.log('Local storage cleared');
487
-
488
- if (onAuthStateChange) {
489
- onAuthStateChange(null);
490
- console.log('Auth state change callback called');
491
- }
635
+ onAuthStateChange?.(null);
492
636
  } catch (error) {
493
- console.error('Logout all error:', error);
494
- useAuthStore.setState({ error: `Logout all failed: ${error instanceof Error ? error.message : 'Unknown error'}` });
637
+ const errorMessage = error instanceof Error ? error.message : 'Logout all failed';
638
+ useAuthStore.setState({ error: errorMessage });
639
+ onError?.({ message: errorMessage, code: 'LOGOUT_ALL_ERROR', status: 500 });
495
640
  throw error;
496
641
  }
497
- }, [activeSessionId, oxyServices, logoutStore, setMinimalUser, clearAllStorage, onAuthStateChange]);
642
+ }, [activeSessionId, oxyServices, logoutStore, clearAllStorage, onAuthStateChange, onError]);
498
643
 
499
- // Effect to restore token on app load or session switch
500
- useEffect(() => {
501
- const restoreToken = async () => {
502
- if (activeSessionId && oxyServices) {
503
- try {
504
- await oxyServices.getTokenBySession(activeSessionId);
505
- setTokenReady(true);
506
- } catch (err) {
507
- // If token restoration fails, force logout
508
- await logout();
509
- setTokenReady(false);
510
- }
511
- } else {
512
- setTokenReady(true); // No session, so token is not needed
513
- }
514
- };
515
- restoreToken();
516
- // Only run when activeSessionId or oxyServices changes
517
- }, [activeSessionId, oxyServices, logout]);
644
+ // Token restoration is handled in initAuth and switchToSession
645
+ // No separate effect needed - children render immediately with isTokenReady available
518
646
 
519
647
  // Sign up method
520
648
  const signUp = useCallback(async (username: string, email: string, password: string): Promise<User> => {
@@ -523,23 +651,18 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
523
651
  useAuthStore.setState({ isLoading: true, error: null });
524
652
 
525
653
  try {
526
- // Create new account using the OxyServices signUp method
527
- const response = await oxyServices.signUp(username, email, password);
528
-
529
- console.log('SignUp successful:', response);
530
-
531
- // Now log the user in to create a session
532
- // This will handle the session creation and device registration
654
+ await oxyServices.signUp(username, email, password);
533
655
  const user = await login(username, password);
534
-
535
656
  return user;
536
- } catch (error: any) {
537
- loginFailure(error.message || 'Sign up failed');
657
+ } catch (error) {
658
+ const errorMessage = error instanceof Error ? error.message : 'Sign up failed';
659
+ loginFailure(errorMessage);
660
+ onError?.({ message: errorMessage, code: 'SIGNUP_ERROR', status: 400 });
538
661
  throw error;
539
662
  } finally {
540
663
  useAuthStore.setState({ isLoading: false });
541
664
  }
542
- }, [storage, oxyServices, login, loginFailure]);
665
+ }, [storage, oxyServices, login, loginFailure, onError]);
543
666
 
544
667
  // Complete MFA login by verifying TOTP
545
668
  const completeMfaLogin = useCallback(async (mfaToken: string, code: string): Promise<User> => {
@@ -555,190 +678,173 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
555
678
  // Fetch access token and user data
556
679
  await oxyServices.getTokenBySession(response.sessionId);
557
680
  const fullUser = await oxyServices.getUserBySession(response.sessionId);
681
+
558
682
  loginSuccess(fullUser);
559
683
  setMinimalUser({ id: fullUser.id, username: fullUser.username, avatar: fullUser.avatar });
560
- // Apply server language if present
561
- if ((fullUser as any)?.language) {
562
- try {
563
- const serverLang = normalizeLanguageCode((fullUser as any).language) || (fullUser as any).language;
564
- await storage.setItem(keys.language, serverLang);
565
- setCurrentLanguage(serverLang);
566
- } catch (e) {
567
- console.warn('Failed to apply server language on MFA login', e);
568
- }
569
- }
684
+ await applyLanguagePreference(fullUser);
570
685
 
571
- // Load sessions list
572
- const serverSessions = await oxyServices.getSessionsBySessionId(response.sessionId);
573
- const clientSessions: ClientSession[] = serverSessions.map(s => ({
574
- sessionId: s.sessionId,
575
- deviceId: s.deviceId,
576
- expiresAt: s.expiresAt || new Date().toISOString(),
577
- lastActive: s.lastActive || new Date().toISOString(),
578
- userId: s.userId || fullUser.id
579
- }));
580
- setSessions(clientSessions);
581
- // Apply server language if present
582
- if ((fullUser as any)?.language) {
583
- try {
584
- await storage.setItem(keys.language, (fullUser as any).language);
585
- setCurrentLanguage((fullUser as any).language);
586
- } catch (e) {
587
- console.warn('Failed to apply server language on MFA login', e);
686
+ // Get all device sessions to support multiple accounts
687
+ try {
688
+ const deviceSessions = await oxyServices.getDeviceSessions(response.sessionId);
689
+ const allDeviceSessions = mapSessionsToClient(deviceSessions, undefined, fullUser.id);
690
+ updateSessions(allDeviceSessions, true);
691
+ } catch (error) {
692
+ // Fallback to user sessions if device sessions fail
693
+ if (__DEV__) {
694
+ console.warn('Failed to get device sessions for MFA, falling back to user sessions:', error);
588
695
  }
696
+ const serverSessions = await oxyServices.getSessionsBySessionId(response.sessionId);
697
+ const userSessions = mapSessionsToClient(serverSessions, undefined, fullUser.id);
698
+
699
+ updateSessions(userSessions, true);
589
700
  }
590
701
 
591
- if (onAuthStateChange) onAuthStateChange(fullUser);
702
+ onAuthStateChange?.(fullUser);
592
703
  return fullUser;
593
- } catch (error: any) {
594
- loginFailure(error.message || 'MFA verification failed');
704
+ } catch (error) {
705
+ const errorMessage = error instanceof Error ? error.message : 'MFA verification failed';
706
+ loginFailure(errorMessage);
707
+ onError?.({ message: errorMessage, code: 'MFA_ERROR', status: 401 });
595
708
  throw error;
596
709
  } finally {
597
710
  useAuthStore.setState({ isLoading: false });
598
711
  }
599
- }, [storage, oxyServices, loginSuccess, loginFailure, saveActiveSessionId, onAuthStateChange]);
712
+ }, [storage, oxyServices, loginSuccess, loginFailure, saveActiveSessionId, onAuthStateChange, applyLanguagePreference, onError]);
600
713
 
601
- // Switch session method
602
714
  const switchSession = useCallback(async (sessionId: string): Promise<void> => {
603
715
  await switchToSession(sessionId);
604
716
  }, [switchToSession]);
605
717
 
606
- // Remove session method
607
718
  const removeSession = useCallback(async (sessionId: string): Promise<void> => {
608
719
  await logout(sessionId);
609
720
  }, [logout]);
610
721
 
611
- // Refresh sessions method
612
722
  const refreshSessions = useCallback(async (): Promise<void> => {
613
- console.log('refreshSessions called with activeSessionId:', activeSessionId);
723
+ if (!activeSessionId) return;
614
724
 
615
- if (!activeSessionId) {
616
- console.log('refreshSessions: No activeSessionId, returning');
617
- return;
725
+ // If a refresh is already in progress, return the existing promise
726
+ if (refreshInFlightRef.current) {
727
+ return refreshInFlightRef.current;
618
728
  }
619
729
 
620
- try {
621
- console.log('refreshSessions: Calling getSessionsBySessionId...');
622
- const serverSessions = await oxyServices.getSessionsBySessionId(activeSessionId);
623
- console.log('refreshSessions: Server sessions received:', serverSessions);
624
-
625
- // Update local sessions with server data
626
- const updatedSessions: ClientSession[] = serverSessions.map(serverSession => ({
627
- sessionId: serverSession.sessionId,
628
- deviceId: serverSession.deviceId,
629
- expiresAt: serverSession.expiresAt || new Date().toISOString(),
630
- lastActive: serverSession.lastActive || new Date().toISOString(),
631
- userId: serverSession.userId || user?.id
632
- }));
633
-
634
- console.log('refreshSessions: Updated sessions:', updatedSessions);
635
- setSessions(updatedSessions);
636
- console.log('refreshSessions: Sessions updated in state');
637
- } catch (error) {
638
- console.error('Refresh sessions error:', error);
639
-
640
- // If the current session is invalid, try to find another valid session
641
- if (sessions.length > 1) {
642
- console.log('Current session invalid, trying to switch to another session...');
643
- const otherSessions = sessions.filter(s => s.sessionId !== activeSessionId);
730
+ // Create the refresh promise
731
+ const refreshPromise = (async () => {
732
+ try {
733
+ const deviceSessions = await oxyServices.getDeviceSessions(activeSessionId);
734
+ const allDeviceSessions = mapSessionsToClient(deviceSessions, undefined, user?.id);
735
+ updateSessions(allDeviceSessions, true);
736
+ } catch (error) {
737
+ if (__DEV__) {
738
+ console.warn('Failed to refresh device sessions, falling back to user sessions:', error);
739
+ }
740
+ try {
741
+ const serverSessions = await oxyServices.getSessionsBySessionId(activeSessionId);
742
+ const userSessions = mapSessionsToClient(serverSessions, undefined, user?.id);
743
+ updateSessions(userSessions, true);
744
+ } catch (fallbackError) {
745
+ if (__DEV__) {
746
+ console.error('Refresh sessions error:', fallbackError);
747
+ }
644
748
 
645
- for (const session of otherSessions) {
646
- try {
647
- // Try to validate this session
648
- await oxyServices.validateSession(session.sessionId, {
649
- useHeaderValidation: true
650
- });
651
- console.log('Found valid session, switching to:', session.sessionId);
652
- await switchToSession(session.sessionId);
653
- return; // Successfully switched to another session
654
- } catch (sessionError) {
655
- console.log('Session validation failed for:', session.sessionId, sessionError);
656
- continue; // Try next session
749
+ // If the current session is invalid, try to find another valid session
750
+ if (sessions.length > 1) {
751
+ const otherSessions = sessions.filter(s => s.sessionId !== activeSessionId);
752
+
753
+ for (const session of otherSessions) {
754
+ try {
755
+ const validation = await oxyServices.validateSession(session.sessionId, {
756
+ useHeaderValidation: true
757
+ });
758
+ if (validation.valid) {
759
+ await switchToSession(session.sessionId);
760
+ return;
761
+ }
762
+ } catch {
763
+ continue;
764
+ }
765
+ }
657
766
  }
767
+
768
+ // No valid sessions found, clear all
769
+ updateSessions([], false);
770
+ setActiveSessionId(null);
771
+ logoutStore();
772
+ setMinimalUser(null);
773
+ await clearAllStorage();
774
+ onAuthStateChange?.(null);
658
775
  }
776
+ } finally {
777
+ // Clear the in-flight ref when done
778
+ refreshInFlightRef.current = null;
659
779
  }
780
+ })();
660
781
 
661
- // If no valid sessions found, clear all sessions
662
- console.log('No valid sessions found, clearing all sessions');
663
- setSessions([]);
664
- setActiveSessionId(null);
665
- logoutStore();
666
- setMinimalUser(null);
667
- await clearAllStorage();
668
-
669
- if (onAuthStateChange) {
670
- onAuthStateChange(null);
671
- }
672
- }
673
- }, [activeSessionId, oxyServices, user?.id, sessions, switchToSession, logoutStore, setMinimalUser, clearAllStorage, onAuthStateChange]);
782
+ refreshInFlightRef.current = refreshPromise;
783
+ return refreshPromise;
784
+ }, [activeSessionId, oxyServices, user?.id, updateSessions, sessions, switchToSession, logoutStore, clearAllStorage, onAuthStateChange, mapSessionsToClient]);
674
785
 
675
786
  // Device management methods
676
- const getDeviceSessions = useCallback(async (): Promise<any[]> => {
787
+ const getDeviceSessions = useCallback(async (): Promise<Array<{
788
+ sessionId: string;
789
+ deviceId: string;
790
+ deviceName?: string;
791
+ lastActive?: string;
792
+ expiresAt?: string;
793
+ }>> => {
677
794
  if (!activeSessionId) throw new Error('No active session');
678
-
679
795
  try {
680
796
  return await oxyServices.getDeviceSessions(activeSessionId);
681
797
  } catch (error) {
682
- console.error('Get device sessions error:', error);
798
+ const errorMessage = error instanceof Error ? error.message : 'Failed to get device sessions';
799
+ onError?.({ message: errorMessage, code: 'GET_DEVICE_SESSIONS_ERROR', status: 500 });
683
800
  throw error;
684
801
  }
685
- }, [activeSessionId, oxyServices]);
802
+ }, [activeSessionId, oxyServices, onError]);
686
803
 
687
804
  const logoutAllDeviceSessions = useCallback(async (): Promise<void> => {
688
805
  if (!activeSessionId) throw new Error('No active session');
689
806
 
690
807
  try {
691
808
  await oxyServices.logoutAllDeviceSessions(activeSessionId);
692
-
693
- // Clear all local sessions since we logged out from all devices
694
- setSessions([]);
809
+ updateSessions([], false);
695
810
  setActiveSessionId(null);
696
811
  logoutStore();
697
812
  setMinimalUser(null);
698
813
  await clearAllStorage();
699
-
700
- if (onAuthStateChange) {
701
- onAuthStateChange(null);
702
- }
814
+ onAuthStateChange?.(null);
703
815
  } catch (error) {
704
- console.error('Logout all device sessions error:', error);
816
+ const errorMessage = error instanceof Error ? error.message : 'Failed to logout all device sessions';
817
+ onError?.({ message: errorMessage, code: 'LOGOUT_ALL_DEVICES_ERROR', status: 500 });
705
818
  throw error;
706
819
  }
707
- }, [activeSessionId, oxyServices, logoutStore, setMinimalUser, clearAllStorage, onAuthStateChange]);
820
+ }, [activeSessionId, oxyServices, logoutStore, clearAllStorage, onAuthStateChange, onError]);
708
821
 
709
822
  const updateDeviceName = useCallback(async (deviceName: string): Promise<void> => {
710
823
  if (!activeSessionId) throw new Error('No active session');
711
824
 
712
825
  try {
713
826
  await oxyServices.updateDeviceName(activeSessionId, deviceName);
714
-
715
- // Update local device info
716
827
  await DeviceManager.updateDeviceName(deviceName);
717
828
  } catch (error) {
718
- console.error('Update device name error:', error);
829
+ const errorMessage = error instanceof Error ? error.message : 'Failed to update device name';
830
+ onError?.({ message: errorMessage, code: 'UPDATE_DEVICE_NAME_ERROR', status: 500 });
719
831
  throw error;
720
832
  }
721
- }, [activeSessionId, oxyServices]);
833
+ }, [activeSessionId, oxyServices, onError]);
722
834
 
723
835
  // Language management method
724
836
  const setLanguage = useCallback(async (languageId: string): Promise<void> => {
725
837
  if (!storage) throw new Error('Storage not initialized');
726
838
 
727
839
  try {
728
- // Save language preference
729
840
  await storage.setItem(keys.language, languageId);
730
841
  setCurrentLanguage(languageId);
731
-
732
- console.log(`Language changed to ${languageId}`);
733
-
734
- // TODO: Here you can add any additional logic needed for app-wide language updates
735
- // such as updating i18n configuration, refreshing translations, etc.
736
-
737
842
  } catch (error) {
738
- console.error('Error saving language preference:', error);
843
+ const errorMessage = error instanceof Error ? error.message : 'Failed to save language preference';
844
+ onError?.({ message: errorMessage, code: 'LANGUAGE_SAVE_ERROR', status: 500 });
739
845
  throw error;
740
846
  }
741
- }, [storage, keys.language]);
847
+ }, [storage, keys.language, onError]);
742
848
 
743
849
  // Bottom sheet control methods
744
850
  const showBottomSheet = useCallback((screenOrConfig?: RouteName | string | { screen: RouteName | string; props?: Record<string, any> }) => {
@@ -754,9 +860,8 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
754
860
  } else if (bottomSheetRef.current.present) {
755
861
  if (__DEV__) console.log('Presenting bottom sheet');
756
862
  bottomSheetRef.current.present();
757
- } else {
863
+ } else if (__DEV__) {
758
864
  console.warn('No expand or present method available on bottomSheetRef');
759
- if (__DEV__) console.log('Available methods on bottomSheetRef.current:', Object.keys(bottomSheetRef.current as any));
760
865
  }
761
866
 
762
867
  // Then navigate to the specified screen if provided
@@ -774,10 +879,8 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
774
879
  }
775
880
  }, 100);
776
881
  }
777
- } else {
778
- console.warn('bottomSheetRef is not available');
779
- console.warn('To fix this, ensure you pass a bottomSheetRef to OxyProvider:');
780
- console.warn('<OxyProvider baseURL="..." bottomSheetRef={yourBottomSheetRef}>');
882
+ } else if (__DEV__) {
883
+ console.warn('bottomSheetRef is not available. Pass a bottomSheetRef to OxyProvider.');
781
884
  }
782
885
  }, [bottomSheetRef]);
783
886
 
@@ -811,14 +914,23 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
811
914
  if (mod && typeof mod.useFollow === 'function') {
812
915
  return mod.useFollow(userId);
813
916
  }
814
- console.warn('useFollow module did not export a function as expected');
815
- return { isFollowing: false, isLoading: false, error: null, toggleFollow: async () => { }, setFollowStatus: () => { }, fetchStatus: async () => { }, clearError: () => { }, followerCount: null, followingCount: null, isLoadingCounts: false, fetchUserCounts: async () => { }, setFollowerCount: () => { }, setFollowingCount: () => { } } as any;
917
+ if (__DEV__) {
918
+ console.warn('useFollow module did not export a function as expected');
919
+ }
920
+ return createEmptyFollowHook()(userId);
816
921
  } catch (e) {
817
- console.warn('Failed to dynamically load useFollow hook:', e);
818
- return { isFollowing: false, isLoading: false, error: null, toggleFollow: async () => { }, setFollowStatus: () => { }, fetchStatus: async () => { }, clearError: () => { }, followerCount: null, followingCount: null, isLoadingCounts: false, fetchUserCounts: async () => { }, setFollowerCount: () => { }, setFollowingCount: () => { } } as any;
922
+ if (__DEV__) {
923
+ console.warn('Failed to dynamically load useFollow hook:', e);
924
+ }
925
+ return createEmptyFollowHook()(userId);
819
926
  }
820
927
  };
821
928
 
929
+ // Compute language metadata from currentLanguage
930
+ const languageMetadata = useMemo(() => getLanguageMetadata(currentLanguage), [currentLanguage]);
931
+ const languageName = useMemo(() => getLanguageName(currentLanguage), [currentLanguage]);
932
+ const nativeLanguageName = useMemo(() => getNativeLanguageName(currentLanguage), [currentLanguage]);
933
+
822
934
  const contextValue: OxyContextState = useMemo(() => ({
823
935
  user,
824
936
  minimalUser,
@@ -826,8 +938,12 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
826
938
  activeSessionId,
827
939
  isAuthenticated,
828
940
  isLoading,
941
+ isTokenReady: tokenReady,
829
942
  error,
830
943
  currentLanguage,
944
+ currentLanguageMetadata: languageMetadata,
945
+ currentLanguageName: languageName,
946
+ currentNativeLanguageName: nativeLanguageName,
831
947
  login,
832
948
  logout,
833
949
  logoutAll,
@@ -852,8 +968,12 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
852
968
  activeSessionId,
853
969
  isAuthenticated,
854
970
  isLoading,
971
+ tokenReady,
855
972
  error,
856
973
  currentLanguage,
974
+ languageMetadata,
975
+ languageName,
976
+ nativeLanguageName,
857
977
  login,
858
978
  logout,
859
979
  logoutAll,
@@ -872,13 +992,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
872
992
  hideBottomSheet,
873
993
  ]);
874
994
 
875
- // Wrap children rendering to block until token is ready
876
- if (!tokenReady) {
877
- return <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
878
- <Text>Loading authentication...</Text>
879
- </View>;
880
- }
881
-
995
+ // Always render children - let the consuming app decide how to handle token loading state
882
996
  return (
883
997
  <OxyContext.Provider value={contextValue}>
884
998
  {children}
@@ -899,3 +1013,4 @@ export const useOxy = (): OxyContextState => {
899
1013
  };
900
1014
 
901
1015
  export default OxyContext;
1016
+