@oxyhq/services 28.0.0 → 29.0.0

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 (324) hide show
  1. package/NOTICE +10 -9
  2. package/README.md +65 -2
  3. package/android/src/main/java/so/oxy/devicesession/OxyDeviceSessionModule.kt +178 -0
  4. package/android/src/main/java/so/oxy/devicesession/OxyDeviceSessionProvider.kt +99 -0
  5. package/android/src/main/java/so/oxy/devicesession/OxyDeviceSessionStore.kt +115 -0
  6. package/expo-module.config.json +1 -0
  7. package/lib/commonjs/index.js +60 -16
  8. package/lib/commonjs/index.js.map +1 -1
  9. package/lib/commonjs/ui/boot/runProviderColdBoot.js +7 -0
  10. package/lib/commonjs/ui/boot/runProviderColdBoot.js.map +1 -1
  11. package/lib/commonjs/ui/client.js +12 -7
  12. package/lib/commonjs/ui/client.js.map +1 -1
  13. package/lib/commonjs/ui/components/OxyAccountDialogScreen.js +10 -27
  14. package/lib/commonjs/ui/components/OxyAccountDialogScreen.js.map +1 -1
  15. package/lib/commonjs/ui/components/OxyAuthChooser.js +132 -44
  16. package/lib/commonjs/ui/components/OxyAuthChooser.js.map +1 -1
  17. package/lib/commonjs/ui/components/OxyProvider.js +4 -2
  18. package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
  19. package/lib/commonjs/ui/components/SurfaceScreen.js +31 -3
  20. package/lib/commonjs/ui/components/SurfaceScreen.js.map +1 -1
  21. package/lib/commonjs/ui/components/authChooser/AccountsMenuView.js +209 -84
  22. package/lib/commonjs/ui/components/authChooser/AccountsMenuView.js.map +1 -1
  23. package/lib/commonjs/ui/components/authChooser/SignInEntryView.js +25 -7
  24. package/lib/commonjs/ui/components/authChooser/SignInEntryView.js.map +1 -1
  25. package/lib/commonjs/ui/components/authChooser/primitives.js +35 -22
  26. package/lib/commonjs/ui/components/authChooser/primitives.js.map +1 -1
  27. package/lib/commonjs/ui/components/authChooser/types.js +11 -0
  28. package/lib/commonjs/ui/components/authChooser/types.js.map +1 -1
  29. package/lib/commonjs/ui/context/OxyContext.js +257 -306
  30. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  31. package/lib/commonjs/ui/context/hooks/useAuthOperations.js +27 -37
  32. package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
  33. package/lib/commonjs/ui/context/oxyContextHelpers.js +0 -29
  34. package/lib/commonjs/ui/context/oxyContextHelpers.js.map +1 -1
  35. package/lib/commonjs/ui/context/useOxyAccountGraph.js +6 -17
  36. package/lib/commonjs/ui/context/useOxyAccountGraph.js.map +1 -1
  37. package/lib/commonjs/ui/hooks/accountDialogSnapshot.js +42 -0
  38. package/lib/commonjs/ui/hooks/accountDialogSnapshot.js.map +1 -0
  39. package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js +0 -19
  40. package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js.map +1 -1
  41. package/lib/commonjs/ui/hooks/useDeviceSwitcher.js +57 -0
  42. package/lib/commonjs/ui/hooks/useDeviceSwitcher.js.map +1 -0
  43. package/lib/commonjs/ui/hooks/useI18n.js +22 -3
  44. package/lib/commonjs/ui/hooks/useI18n.js.map +1 -1
  45. package/lib/commonjs/ui/hooks/useSessionManagement.js +46 -153
  46. package/lib/commonjs/ui/hooks/useSessionManagement.js.map +1 -1
  47. package/lib/commonjs/ui/index.js +12 -7
  48. package/lib/commonjs/ui/index.js.map +1 -1
  49. package/lib/commonjs/ui/navigation/routes.js +85 -73
  50. package/lib/commonjs/ui/navigation/routes.js.map +1 -1
  51. package/lib/commonjs/ui/navigation/surfaces.js +8 -7
  52. package/lib/commonjs/ui/navigation/surfaces.js.map +1 -1
  53. package/lib/commonjs/ui/runtime/createOxyRuntime.js +433 -0
  54. package/lib/commonjs/ui/runtime/createOxyRuntime.js.map +1 -0
  55. package/lib/commonjs/ui/runtime/index.js +63 -0
  56. package/lib/commonjs/ui/runtime/index.js.map +1 -0
  57. package/lib/commonjs/ui/runtime/runtimeContext.js +83 -0
  58. package/lib/commonjs/ui/runtime/runtimeContext.js.map +1 -0
  59. package/lib/commonjs/ui/runtime/types.js +6 -0
  60. package/lib/commonjs/ui/runtime/types.js.map +1 -0
  61. package/lib/commonjs/ui/runtime/useRuntimeSnapshot.js +57 -0
  62. package/lib/commonjs/ui/runtime/useRuntimeSnapshot.js.map +1 -0
  63. package/lib/commonjs/ui/server.js +1 -2
  64. package/lib/commonjs/ui/server.js.map +1 -1
  65. package/lib/commonjs/ui/session/authStore.js +45 -9
  66. package/lib/commonjs/ui/session/authStore.js.map +1 -1
  67. package/lib/commonjs/ui/session/sharedDeviceCredentialStore.js +306 -0
  68. package/lib/commonjs/ui/session/sharedDeviceCredentialStore.js.map +1 -0
  69. package/lib/commonjs/ui/stores/authStore.js +154 -55
  70. package/lib/commonjs/ui/stores/authStore.js.map +1 -1
  71. package/lib/commonjs/ui/utils/avatarUtils.js +2 -36
  72. package/lib/commonjs/ui/utils/avatarUtils.js.map +1 -1
  73. package/lib/module/index.js +16 -8
  74. package/lib/module/index.js.map +1 -1
  75. package/lib/module/ui/boot/runProviderColdBoot.js +7 -0
  76. package/lib/module/ui/boot/runProviderColdBoot.js.map +1 -1
  77. package/lib/module/ui/client.js +1 -2
  78. package/lib/module/ui/client.js.map +1 -1
  79. package/lib/module/ui/components/OxyAccountDialogScreen.js +10 -27
  80. package/lib/module/ui/components/OxyAccountDialogScreen.js.map +1 -1
  81. package/lib/module/ui/components/OxyAuthChooser.js +134 -44
  82. package/lib/module/ui/components/OxyAuthChooser.js.map +1 -1
  83. package/lib/module/ui/components/OxyProvider.js +5 -3
  84. package/lib/module/ui/components/OxyProvider.js.map +1 -1
  85. package/lib/module/ui/components/SurfaceScreen.js +33 -5
  86. package/lib/module/ui/components/SurfaceScreen.js.map +1 -1
  87. package/lib/module/ui/components/authChooser/AccountsMenuView.js +210 -85
  88. package/lib/module/ui/components/authChooser/AccountsMenuView.js.map +1 -1
  89. package/lib/module/ui/components/authChooser/SignInEntryView.js +25 -7
  90. package/lib/module/ui/components/authChooser/SignInEntryView.js.map +1 -1
  91. package/lib/module/ui/components/authChooser/primitives.js +35 -22
  92. package/lib/module/ui/components/authChooser/primitives.js.map +1 -1
  93. package/lib/module/ui/components/authChooser/types.js +11 -0
  94. package/lib/module/ui/components/authChooser/types.js.map +1 -1
  95. package/lib/module/ui/context/OxyContext.js +251 -303
  96. package/lib/module/ui/context/OxyContext.js.map +1 -1
  97. package/lib/module/ui/context/hooks/useAuthOperations.js +27 -37
  98. package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
  99. package/lib/module/ui/context/oxyContextHelpers.js +0 -28
  100. package/lib/module/ui/context/oxyContextHelpers.js.map +1 -1
  101. package/lib/module/ui/context/useOxyAccountGraph.js +6 -17
  102. package/lib/module/ui/context/useOxyAccountGraph.js.map +1 -1
  103. package/lib/module/ui/hooks/accountDialogSnapshot.js +38 -0
  104. package/lib/module/ui/hooks/accountDialogSnapshot.js.map +1 -0
  105. package/lib/module/ui/hooks/mutations/useAccountMutations.js +0 -19
  106. package/lib/module/ui/hooks/mutations/useAccountMutations.js.map +1 -1
  107. package/lib/module/ui/hooks/useDeviceSwitcher.js +53 -0
  108. package/lib/module/ui/hooks/useDeviceSwitcher.js.map +1 -0
  109. package/lib/module/ui/hooks/useI18n.js +24 -4
  110. package/lib/module/ui/hooks/useI18n.js.map +1 -1
  111. package/lib/module/ui/hooks/useSessionManagement.js +47 -155
  112. package/lib/module/ui/hooks/useSessionManagement.js.map +1 -1
  113. package/lib/module/ui/index.js +1 -2
  114. package/lib/module/ui/index.js.map +1 -1
  115. package/lib/module/ui/navigation/routes.js +84 -72
  116. package/lib/module/ui/navigation/routes.js.map +1 -1
  117. package/lib/module/ui/navigation/surfaces.js +6 -6
  118. package/lib/module/ui/navigation/surfaces.js.map +1 -1
  119. package/lib/module/ui/runtime/createOxyRuntime.js +430 -0
  120. package/lib/module/ui/runtime/createOxyRuntime.js.map +1 -0
  121. package/lib/module/ui/runtime/index.js +15 -0
  122. package/lib/module/ui/runtime/index.js.map +1 -0
  123. package/lib/module/ui/runtime/runtimeContext.js +75 -0
  124. package/lib/module/ui/runtime/runtimeContext.js.map +1 -0
  125. package/lib/module/ui/runtime/types.js +4 -0
  126. package/lib/module/ui/runtime/types.js.map +1 -0
  127. package/lib/module/ui/runtime/useRuntimeSnapshot.js +52 -0
  128. package/lib/module/ui/runtime/useRuntimeSnapshot.js.map +1 -0
  129. package/lib/module/ui/server.js +0 -1
  130. package/lib/module/ui/server.js.map +1 -1
  131. package/lib/module/ui/session/authStore.js +46 -11
  132. package/lib/module/ui/session/authStore.js.map +1 -1
  133. package/lib/module/ui/session/sharedDeviceCredentialStore.js +302 -0
  134. package/lib/module/ui/session/sharedDeviceCredentialStore.js.map +1 -0
  135. package/lib/module/ui/stores/authStore.js +154 -55
  136. package/lib/module/ui/stores/authStore.js.map +1 -1
  137. package/lib/module/ui/utils/avatarUtils.js +2 -35
  138. package/lib/module/ui/utils/avatarUtils.js.map +1 -1
  139. package/lib/typescript/commonjs/index.d.ts +5 -3
  140. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  141. package/lib/typescript/commonjs/ui/boot/runProviderColdBoot.d.ts.map +1 -1
  142. package/lib/typescript/commonjs/ui/client.d.ts +1 -2
  143. package/lib/typescript/commonjs/ui/client.d.ts.map +1 -1
  144. package/lib/typescript/commonjs/ui/components/OxyAccountDialogScreen.d.ts.map +1 -1
  145. package/lib/typescript/commonjs/ui/components/OxyAuthChooser.d.ts.map +1 -1
  146. package/lib/typescript/commonjs/ui/components/OxyProvider.d.ts.map +1 -1
  147. package/lib/typescript/commonjs/ui/components/SurfaceScreen.d.ts.map +1 -1
  148. package/lib/typescript/commonjs/ui/components/authChooser/AccountsMenuView.d.ts +11 -4
  149. package/lib/typescript/commonjs/ui/components/authChooser/AccountsMenuView.d.ts.map +1 -1
  150. package/lib/typescript/commonjs/ui/components/authChooser/SignInEntryView.d.ts +3 -1
  151. package/lib/typescript/commonjs/ui/components/authChooser/SignInEntryView.d.ts.map +1 -1
  152. package/lib/typescript/commonjs/ui/components/authChooser/primitives.d.ts +18 -6
  153. package/lib/typescript/commonjs/ui/components/authChooser/primitives.d.ts.map +1 -1
  154. package/lib/typescript/commonjs/ui/components/authChooser/types.d.ts +33 -1
  155. package/lib/typescript/commonjs/ui/components/authChooser/types.d.ts.map +1 -1
  156. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +35 -4
  157. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
  158. package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts +8 -12
  159. package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  160. package/lib/typescript/commonjs/ui/context/oxyContextHelpers.d.ts +0 -2
  161. package/lib/typescript/commonjs/ui/context/oxyContextHelpers.d.ts.map +1 -1
  162. package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts +9 -1
  163. package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts.map +1 -1
  164. package/lib/typescript/commonjs/ui/context/useOxyAccountGraph.d.ts +1 -3
  165. package/lib/typescript/commonjs/ui/context/useOxyAccountGraph.d.ts.map +1 -1
  166. package/lib/typescript/commonjs/ui/hooks/accountDialogSnapshot.d.ts +15 -0
  167. package/lib/typescript/commonjs/ui/hooks/accountDialogSnapshot.d.ts.map +1 -0
  168. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  169. package/lib/typescript/commonjs/ui/hooks/useDeviceSwitcher.d.ts +49 -0
  170. package/lib/typescript/commonjs/ui/hooks/useDeviceSwitcher.d.ts.map +1 -0
  171. package/lib/typescript/commonjs/ui/hooks/useI18n.d.ts +13 -0
  172. package/lib/typescript/commonjs/ui/hooks/useI18n.d.ts.map +1 -1
  173. package/lib/typescript/commonjs/ui/hooks/useSessionManagement.d.ts +23 -18
  174. package/lib/typescript/commonjs/ui/hooks/useSessionManagement.d.ts.map +1 -1
  175. package/lib/typescript/commonjs/ui/index.d.ts +1 -2
  176. package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
  177. package/lib/typescript/commonjs/ui/navigation/routes.d.ts +15 -1
  178. package/lib/typescript/commonjs/ui/navigation/routes.d.ts.map +1 -1
  179. package/lib/typescript/commonjs/ui/navigation/surfaces.d.ts.map +1 -1
  180. package/lib/typescript/commonjs/ui/runtime/createOxyRuntime.d.ts +144 -0
  181. package/lib/typescript/commonjs/ui/runtime/createOxyRuntime.d.ts.map +1 -0
  182. package/lib/typescript/commonjs/ui/runtime/index.d.ts +16 -0
  183. package/lib/typescript/commonjs/ui/runtime/index.d.ts.map +1 -0
  184. package/lib/typescript/commonjs/ui/runtime/runtimeContext.d.ts +43 -0
  185. package/lib/typescript/commonjs/ui/runtime/runtimeContext.d.ts.map +1 -0
  186. package/lib/typescript/commonjs/ui/runtime/types.d.ts +89 -0
  187. package/lib/typescript/commonjs/ui/runtime/types.d.ts.map +1 -0
  188. package/lib/typescript/commonjs/ui/runtime/useRuntimeSnapshot.d.ts +23 -0
  189. package/lib/typescript/commonjs/ui/runtime/useRuntimeSnapshot.d.ts.map +1 -0
  190. package/lib/typescript/commonjs/ui/server.d.ts +0 -1
  191. package/lib/typescript/commonjs/ui/server.d.ts.map +1 -1
  192. package/lib/typescript/commonjs/ui/session/authStore.d.ts +60 -9
  193. package/lib/typescript/commonjs/ui/session/authStore.d.ts.map +1 -1
  194. package/lib/typescript/commonjs/ui/session/sharedDeviceCredentialStore.d.ts +32 -0
  195. package/lib/typescript/commonjs/ui/session/sharedDeviceCredentialStore.d.ts.map +1 -0
  196. package/lib/typescript/commonjs/ui/stores/authStore.d.ts +49 -5
  197. package/lib/typescript/commonjs/ui/stores/authStore.d.ts.map +1 -1
  198. package/lib/typescript/commonjs/ui/types/navigation.d.ts +26 -0
  199. package/lib/typescript/commonjs/ui/types/navigation.d.ts.map +1 -1
  200. package/lib/typescript/commonjs/ui/utils/avatarUtils.d.ts +2 -10
  201. package/lib/typescript/commonjs/ui/utils/avatarUtils.d.ts.map +1 -1
  202. package/lib/typescript/module/index.d.ts +5 -3
  203. package/lib/typescript/module/index.d.ts.map +1 -1
  204. package/lib/typescript/module/ui/boot/runProviderColdBoot.d.ts.map +1 -1
  205. package/lib/typescript/module/ui/client.d.ts +1 -2
  206. package/lib/typescript/module/ui/client.d.ts.map +1 -1
  207. package/lib/typescript/module/ui/components/OxyAccountDialogScreen.d.ts.map +1 -1
  208. package/lib/typescript/module/ui/components/OxyAuthChooser.d.ts.map +1 -1
  209. package/lib/typescript/module/ui/components/OxyProvider.d.ts.map +1 -1
  210. package/lib/typescript/module/ui/components/SurfaceScreen.d.ts.map +1 -1
  211. package/lib/typescript/module/ui/components/authChooser/AccountsMenuView.d.ts +11 -4
  212. package/lib/typescript/module/ui/components/authChooser/AccountsMenuView.d.ts.map +1 -1
  213. package/lib/typescript/module/ui/components/authChooser/SignInEntryView.d.ts +3 -1
  214. package/lib/typescript/module/ui/components/authChooser/SignInEntryView.d.ts.map +1 -1
  215. package/lib/typescript/module/ui/components/authChooser/primitives.d.ts +18 -6
  216. package/lib/typescript/module/ui/components/authChooser/primitives.d.ts.map +1 -1
  217. package/lib/typescript/module/ui/components/authChooser/types.d.ts +33 -1
  218. package/lib/typescript/module/ui/components/authChooser/types.d.ts.map +1 -1
  219. package/lib/typescript/module/ui/context/OxyContext.d.ts +35 -4
  220. package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
  221. package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts +8 -12
  222. package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  223. package/lib/typescript/module/ui/context/oxyContextHelpers.d.ts +0 -2
  224. package/lib/typescript/module/ui/context/oxyContextHelpers.d.ts.map +1 -1
  225. package/lib/typescript/module/ui/context/oxyContextTypes.d.ts +9 -1
  226. package/lib/typescript/module/ui/context/oxyContextTypes.d.ts.map +1 -1
  227. package/lib/typescript/module/ui/context/useOxyAccountGraph.d.ts +1 -3
  228. package/lib/typescript/module/ui/context/useOxyAccountGraph.d.ts.map +1 -1
  229. package/lib/typescript/module/ui/hooks/accountDialogSnapshot.d.ts +15 -0
  230. package/lib/typescript/module/ui/hooks/accountDialogSnapshot.d.ts.map +1 -0
  231. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  232. package/lib/typescript/module/ui/hooks/useDeviceSwitcher.d.ts +49 -0
  233. package/lib/typescript/module/ui/hooks/useDeviceSwitcher.d.ts.map +1 -0
  234. package/lib/typescript/module/ui/hooks/useI18n.d.ts +13 -0
  235. package/lib/typescript/module/ui/hooks/useI18n.d.ts.map +1 -1
  236. package/lib/typescript/module/ui/hooks/useSessionManagement.d.ts +23 -18
  237. package/lib/typescript/module/ui/hooks/useSessionManagement.d.ts.map +1 -1
  238. package/lib/typescript/module/ui/index.d.ts +1 -2
  239. package/lib/typescript/module/ui/index.d.ts.map +1 -1
  240. package/lib/typescript/module/ui/navigation/routes.d.ts +15 -1
  241. package/lib/typescript/module/ui/navigation/routes.d.ts.map +1 -1
  242. package/lib/typescript/module/ui/navigation/surfaces.d.ts.map +1 -1
  243. package/lib/typescript/module/ui/runtime/createOxyRuntime.d.ts +144 -0
  244. package/lib/typescript/module/ui/runtime/createOxyRuntime.d.ts.map +1 -0
  245. package/lib/typescript/module/ui/runtime/index.d.ts +16 -0
  246. package/lib/typescript/module/ui/runtime/index.d.ts.map +1 -0
  247. package/lib/typescript/module/ui/runtime/runtimeContext.d.ts +43 -0
  248. package/lib/typescript/module/ui/runtime/runtimeContext.d.ts.map +1 -0
  249. package/lib/typescript/module/ui/runtime/types.d.ts +89 -0
  250. package/lib/typescript/module/ui/runtime/types.d.ts.map +1 -0
  251. package/lib/typescript/module/ui/runtime/useRuntimeSnapshot.d.ts +23 -0
  252. package/lib/typescript/module/ui/runtime/useRuntimeSnapshot.d.ts.map +1 -0
  253. package/lib/typescript/module/ui/server.d.ts +0 -1
  254. package/lib/typescript/module/ui/server.d.ts.map +1 -1
  255. package/lib/typescript/module/ui/session/authStore.d.ts +60 -9
  256. package/lib/typescript/module/ui/session/authStore.d.ts.map +1 -1
  257. package/lib/typescript/module/ui/session/sharedDeviceCredentialStore.d.ts +32 -0
  258. package/lib/typescript/module/ui/session/sharedDeviceCredentialStore.d.ts.map +1 -0
  259. package/lib/typescript/module/ui/stores/authStore.d.ts +49 -5
  260. package/lib/typescript/module/ui/stores/authStore.d.ts.map +1 -1
  261. package/lib/typescript/module/ui/types/navigation.d.ts +26 -0
  262. package/lib/typescript/module/ui/types/navigation.d.ts.map +1 -1
  263. package/lib/typescript/module/ui/utils/avatarUtils.d.ts +2 -10
  264. package/lib/typescript/module/ui/utils/avatarUtils.d.ts.map +1 -1
  265. package/package.json +6 -7
  266. package/plugins/withSharedDeviceSessionProvider.js +102 -0
  267. package/plugins/withSharedDeviceSessionReader.js +57 -0
  268. package/src/__tests__/esmSourcePurity.test.ts +61 -0
  269. package/src/index.ts +31 -9
  270. package/src/ui/boot/runProviderColdBoot.ts +7 -0
  271. package/src/ui/client.ts +1 -2
  272. package/src/ui/components/OxyAccountDialogScreen.tsx +10 -29
  273. package/src/ui/components/OxyAuthChooser.tsx +149 -50
  274. package/src/ui/components/OxyProvider.tsx +7 -5
  275. package/src/ui/components/SurfaceScreen.tsx +28 -4
  276. package/src/ui/components/authChooser/AccountsMenuView.tsx +246 -96
  277. package/src/ui/components/authChooser/SignInEntryView.tsx +31 -8
  278. package/src/ui/components/authChooser/primitives.tsx +37 -23
  279. package/src/ui/components/authChooser/types.ts +34 -1
  280. package/src/ui/context/OxyContext.tsx +269 -324
  281. package/src/ui/context/hooks/useAuthOperations.ts +32 -35
  282. package/src/ui/context/oxyContextHelpers.ts +0 -30
  283. package/src/ui/context/oxyContextTypes.ts +9 -1
  284. package/src/ui/context/useOxyAccountGraph.ts +5 -19
  285. package/src/ui/hooks/accountDialogSnapshot.ts +37 -0
  286. package/src/ui/hooks/mutations/useAccountMutations.ts +0 -19
  287. package/src/ui/hooks/queries/__tests__/userCache.test.ts +12 -1
  288. package/src/ui/hooks/useDeviceSwitcher.ts +110 -0
  289. package/src/ui/hooks/useI18n.ts +23 -3
  290. package/src/ui/hooks/useSessionManagement.ts +62 -211
  291. package/src/ui/index.ts +1 -2
  292. package/src/ui/navigation/routes.ts +85 -75
  293. package/src/ui/navigation/surfaces.ts +6 -9
  294. package/src/ui/runtime/createOxyRuntime.ts +623 -0
  295. package/src/ui/runtime/index.ts +34 -0
  296. package/src/ui/runtime/runtimeContext.tsx +111 -0
  297. package/src/ui/runtime/types.ts +110 -0
  298. package/src/ui/runtime/useRuntimeSnapshot.ts +52 -0
  299. package/src/ui/server.ts +0 -1
  300. package/src/ui/session/__tests__/authStore.test.ts +90 -0
  301. package/src/ui/session/__tests__/sharedDeviceCredentialStore.test.ts +289 -0
  302. package/src/ui/session/authStore.ts +78 -9
  303. package/src/ui/session/sharedDeviceCredentialStore.ts +318 -0
  304. package/src/ui/stores/authStore.ts +171 -43
  305. package/src/ui/types/navigation.ts +26 -0
  306. package/src/ui/utils/avatarUtils.ts +2 -37
  307. package/lib/commonjs/ui/hooks/useSwitchableAccounts.js +0 -64
  308. package/lib/commonjs/ui/hooks/useSwitchableAccounts.js.map +0 -1
  309. package/lib/commonjs/ui/stores/accountStore.js +0 -221
  310. package/lib/commonjs/ui/stores/accountStore.js.map +0 -1
  311. package/lib/module/ui/hooks/useSwitchableAccounts.js +0 -60
  312. package/lib/module/ui/hooks/useSwitchableAccounts.js.map +0 -1
  313. package/lib/module/ui/stores/accountStore.js +0 -213
  314. package/lib/module/ui/stores/accountStore.js.map +0 -1
  315. package/lib/typescript/commonjs/ui/hooks/useSwitchableAccounts.d.ts +0 -23
  316. package/lib/typescript/commonjs/ui/hooks/useSwitchableAccounts.d.ts.map +0 -1
  317. package/lib/typescript/commonjs/ui/stores/accountStore.d.ts +0 -27
  318. package/lib/typescript/commonjs/ui/stores/accountStore.d.ts.map +0 -1
  319. package/lib/typescript/module/ui/hooks/useSwitchableAccounts.d.ts +0 -23
  320. package/lib/typescript/module/ui/hooks/useSwitchableAccounts.d.ts.map +0 -1
  321. package/lib/typescript/module/ui/stores/accountStore.d.ts +0 -27
  322. package/lib/typescript/module/ui/stores/accountStore.d.ts.map +0 -1
  323. package/src/ui/hooks/useSwitchableAccounts.ts +0 -75
  324. package/src/ui/stores/accountStore.ts +0 -264
@@ -1,7 +1,7 @@
1
1
  import { useCallback } from 'react';
2
2
  import type { ApiError, AuthStateStore, IdentityBinding, SessionClient, User } from '@oxyhq/core';
3
- import type { AuthState } from '../../stores/authStore';
4
3
  import type { ClientSession, SessionLoginResponse } from '@oxyhq/core';
4
+ import type { OxyRuntime } from '../../runtime';
5
5
  import { DeviceManager } from '@oxyhq/core';
6
6
  import { fetchSessionsWithFallback } from '../../utils/sessionHelpers';
7
7
  import { handleAuthError, isInvalidSessionError } from '../../utils/errorHandlers';
@@ -20,9 +20,12 @@ export interface UseAuthOperationsOptions {
20
20
  * warm-restores without a redirect.
21
21
  */
22
22
  store: AuthStateStore;
23
- activeSessionId: string | null;
24
- setActiveSessionId: (sessionId: string | null) => void;
25
- updateSessions: (sessions: ClientSession[], options?: { merge?: boolean }) => void;
23
+ /**
24
+ * The one owner of the session facts. Reads (`activeSessionId`) and writes
25
+ * (the account, the session mirror, the loading/error gates) both go through
26
+ * it, so this hook holds no copy of anything to fall out of step.
27
+ */
28
+ runtime: OxyRuntime;
26
29
  saveActiveSessionId: (sessionId: string) => Promise<void>;
27
30
  clearSessionState: () => Promise<void>;
28
31
  /** Used only by `performSignIn`'s same-user duplicate-session dedup (legacy session-validate path; unrelated to the SessionClient device-account set). */
@@ -38,10 +41,6 @@ export interface UseAuthOperationsOptions {
38
41
  syncFromClient: () => Promise<void>;
39
42
  onAuthStateChange?: (user: User | null) => void;
40
43
  onError?: (error: ApiError) => void;
41
- loginSuccess: (user: User) => void;
42
- loginFailure: (message: string) => void;
43
- logoutStore: () => void;
44
- setAuthState: (state: Partial<AuthState>) => void;
45
44
  logger?: (message: string, error?: unknown) => void;
46
45
  /**
47
46
  * Identity-bound session (`sessionMode: 'identity'`). When set, a successful
@@ -91,9 +90,7 @@ export function clearPersistedAuthSafe(
91
90
  export const useAuthOperations = ({
92
91
  oxyServices,
93
92
  store,
94
- activeSessionId,
95
- setActiveSessionId,
96
- updateSessions,
93
+ runtime,
97
94
  saveActiveSessionId,
98
95
  clearSessionState,
99
96
  switchSession,
@@ -101,10 +98,6 @@ export const useAuthOperations = ({
101
98
  syncFromClient,
102
99
  onAuthStateChange,
103
100
  onError,
104
- loginSuccess,
105
- loginFailure,
106
- logoutStore,
107
- setAuthState,
108
101
  logger,
109
102
  identityBinding,
110
103
  refreshPinnedAccountId,
@@ -231,7 +224,7 @@ export const useAuthOperations = ({
231
224
  }
232
225
  }
233
226
  await switchSession(existingSession.sessionId);
234
- updateSessions(
227
+ runtime.mergeSessions(
235
228
  allDeviceSessions.filter((session) => session.sessionId !== sessionResponse.sessionId),
236
229
  { merge: false },
237
230
  );
@@ -239,27 +232,28 @@ export const useAuthOperations = ({
239
232
  return fullUser;
240
233
  }
241
234
 
242
- setActiveSessionId(sessionResponse.sessionId);
235
+ // One transition: the active id, the session mirror and the account all
236
+ // land together, so nothing is woken holding two of the three.
237
+ runtime.batch(() => {
238
+ runtime.setActiveSessionId(sessionResponse.sessionId);
239
+ runtime.mergeSessions(allDeviceSessions, { merge: true });
240
+ runtime.setAccount(fullUser);
241
+ });
243
242
  await saveActiveSessionId(sessionResponse.sessionId);
244
- updateSessions(allDeviceSessions, { merge: true });
245
-
246
- loginSuccess(fullUser);
247
243
  onAuthStateChange?.(fullUser);
248
244
 
249
245
  return fullUser;
250
246
  },
251
247
  [
252
248
  logger,
253
- loginSuccess,
254
249
  onAuthStateChange,
255
250
  oxyServices,
251
+ runtime,
256
252
  saveActiveSessionId,
257
253
  sessionClient,
258
- setActiveSessionId,
259
254
  store,
260
255
  switchSession,
261
256
  syncFromClient,
262
- updateSessions,
263
257
  identityBinding,
264
258
  refreshPinnedAccountId,
265
259
  ],
@@ -270,7 +264,10 @@ export const useAuthOperations = ({
270
264
  */
271
265
  const signIn = useCallback(
272
266
  async (publicKey: string, deviceName?: string): Promise<User> => {
273
- setAuthState({ isLoading: true, error: null });
267
+ runtime.batch(() => {
268
+ runtime.setLoading(true);
269
+ runtime.setError(null);
270
+ });
274
271
 
275
272
  try {
276
273
  return await performSignIn(publicKey);
@@ -279,16 +276,16 @@ export const useAuthOperations = ({
279
276
  defaultMessage: 'Sign in failed',
280
277
  code: LOGIN_ERROR_CODE,
281
278
  onError,
282
- setAuthError: (msg: string) => setAuthState({ error: msg }),
279
+ setAuthError: (msg: string) => runtime.setError(msg),
283
280
  logger,
284
281
  });
285
- loginFailure(message);
282
+ runtime.setError(message);
286
283
  throw error;
287
284
  } finally {
288
- setAuthState({ isLoading: false });
285
+ runtime.setLoading(false);
289
286
  }
290
287
  },
291
- [setAuthState, performSignIn, loginFailure, onError, logger],
288
+ [runtime, performSignIn, onError, logger],
292
289
  );
293
290
 
294
291
  /**
@@ -296,6 +293,7 @@ export const useAuthOperations = ({
296
293
  */
297
294
  const logout = useCallback(
298
295
  async (targetSessionId?: string): Promise<void> => {
296
+ const activeSessionId = runtime.getSnapshot().activeSessionId;
299
297
  if (!activeSessionId) return;
300
298
 
301
299
  try {
@@ -342,20 +340,19 @@ export const useAuthOperations = ({
342
340
  defaultMessage: 'Logout failed',
343
341
  code: LOGOUT_ERROR_CODE,
344
342
  onError,
345
- setAuthError: (msg: string) => setAuthState({ error: msg }),
343
+ setAuthError: (msg: string) => runtime.setError(msg),
346
344
  logger,
347
345
  status: isInvalid ? 401 : undefined,
348
346
  });
349
347
  }
350
348
  },
351
349
  [
352
- activeSessionId,
353
350
  clearSessionState,
354
351
  store,
355
352
  logger,
356
353
  onError,
354
+ runtime,
357
355
  sessionClient,
358
- setAuthState,
359
356
  syncFromClient,
360
357
  ],
361
358
  );
@@ -364,9 +361,9 @@ export const useAuthOperations = ({
364
361
  * Logout from all sessions
365
362
  */
366
363
  const logoutAll = useCallback(async (): Promise<void> => {
367
- if (!activeSessionId) {
364
+ if (!runtime.getSnapshot().activeSessionId) {
368
365
  const error = new Error('No active session found');
369
- setAuthState({ error: error.message });
366
+ runtime.setError(error.message);
370
367
  onError?.({ message: error.message, code: LOGOUT_ALL_ERROR_CODE, status: 404 });
371
368
  throw error;
372
369
  }
@@ -387,12 +384,12 @@ export const useAuthOperations = ({
387
384
  defaultMessage: 'Logout all failed',
388
385
  code: LOGOUT_ALL_ERROR_CODE,
389
386
  onError,
390
- setAuthError: (msg: string) => setAuthState({ error: msg }),
387
+ setAuthError: (msg: string) => runtime.setError(msg),
391
388
  logger,
392
389
  });
393
390
  throw error instanceof Error ? error : new Error('Logout all failed');
394
391
  }
395
- }, [activeSessionId, clearSessionState, store, logger, onError, sessionClient, setAuthState]);
392
+ }, [clearSessionState, store, logger, onError, runtime, sessionClient]);
396
393
 
397
394
  return {
398
395
  signIn,
@@ -1,6 +1,3 @@
1
- import type { UseFollowHook } from '../hooks/useFollow.types';
2
- import { logger as loggerUtil } from '@oxyhq/core';
3
-
4
1
  /** Local display hint when a session lacks explicit `expiresAt` (7 days). */
5
2
  export const DEFAULT_SESSION_VALIDITY_MS = 7 * 24 * 60 * 60 * 1000;
6
3
 
@@ -29,30 +26,3 @@ export function getHttpStatus(error: unknown): number | undefined {
29
26
  export function isUnauthorizedStatus(error: unknown): boolean {
30
27
  return getHttpStatus(error) === 401;
31
28
  }
32
-
33
- let cachedUseFollowHook: UseFollowHook | null = null;
34
-
35
- export const loadUseFollowHook = (): UseFollowHook => {
36
- if (cachedUseFollowHook) {
37
- return cachedUseFollowHook;
38
- }
39
- try {
40
- // eslint-disable-next-line @typescript-eslint/no-var-requires
41
- const { useFollow } = require('../hooks/useFollow');
42
- cachedUseFollowHook = useFollow as UseFollowHook;
43
- return cachedUseFollowHook;
44
- } catch (error) {
45
- if (__DEV__) {
46
- loggerUtil.warn(
47
- 'useFollow hook is not available. Please import useFollow from @oxyhq/services directly.',
48
- { component: 'OxyContext', method: 'loadUseFollowHook' },
49
- error,
50
- );
51
- }
52
- const fallback: UseFollowHook = () => {
53
- throw new Error('useFollow hook is only available in the UI bundle. Import it from @oxyhq/services.');
54
- };
55
- cachedUseFollowHook = fallback;
56
- return cachedUseFollowHook;
57
- }
58
- };
@@ -151,7 +151,7 @@ export interface OxyContextState {
151
151
  createAccount: (data: CreateAccountInput) => Promise<AccountNode>;
152
152
  }
153
153
 
154
- export interface OxyContextProviderProps {
154
+ export interface OxyRuntimeProviderProps {
155
155
  children: ReactNode;
156
156
  oxyServices?: OxyServices;
157
157
  baseURL?: string;
@@ -198,6 +198,14 @@ export interface OxyContextProviderProps {
198
198
  * @default false
199
199
  */
200
200
  backgroundSession?: boolean;
201
+ /**
202
+ * Whether this origin/app persists the durable device credential at all. See
203
+ * `OxyProviderProps.deviceCredentialStorage` for the full rule and the one
204
+ * caller of `'ephemeral'`.
205
+ *
206
+ * @default 'persistent'
207
+ */
208
+ deviceCredentialStorage?: 'persistent' | 'ephemeral';
201
209
  onAuthStateChange?: (user: User | null) => void;
202
210
  onError?: (error: ApiError) => void;
203
211
  }
@@ -1,11 +1,7 @@
1
1
  import { useCallback, useEffect, useState, type RefObject } from 'react';
2
- import type { QueryClient } from '@tanstack/react-query';
3
2
  import type { OxyServices, AccountNode, CreateAccountInput, AccountDialogController, SessionClient } from '@oxyhq/core';
4
3
  import { logger as loggerUtil } from '@oxyhq/core';
5
- import { useAuthStore } from '../stores/authStore';
6
4
  import { isUnauthorizedStatus } from './oxyContextHelpers';
7
- import { resetSessionScopedStores } from '../stores/resetSessionScopedStores';
8
- import { ASSET_DOWNLOAD_URLS_QUERY_KEY } from '../hooks/useResolvedFileUrls';
9
5
  import { IdentityBoundSessionError } from '../session';
10
6
  import type { CommitInput } from './oxyContextTypes';
11
7
 
@@ -23,7 +19,6 @@ interface UseOxyAccountGraphParams {
23
19
  sessionClient: SessionClient;
24
20
  syncFromClient: () => Promise<void>;
25
21
  commitSession: (input: CommitInput, options: { activate: boolean }) => Promise<void>;
26
- queryClient: QueryClient;
27
22
  accountDialogControllerRef: RefObject<AccountDialogController | null>;
28
23
  clearSessionStateRef: RefObject<() => Promise<void>>;
29
24
  }
@@ -37,7 +32,6 @@ export function useOxyAccountGraph({
37
32
  sessionClient,
38
33
  syncFromClient,
39
34
  commitSession,
40
- queryClient,
41
35
  accountDialogControllerRef,
42
36
  clearSessionStateRef,
43
37
  }: UseOxyAccountGraphParams) {
@@ -70,16 +64,6 @@ export function useOxyAccountGraph({
70
64
  }
71
65
  }, [identityBound, isAuthenticated, initialized, tokenReady, refreshAccounts, accountDialogControllerRef]);
72
66
 
73
- const runPostAccountSwitchSideEffects = useCallback(async (): Promise<void> => {
74
- resetSessionScopedStores();
75
- // Scoped media tokens are per-viewer — drop any cached URLs immediately so
76
- // `keepPreviousData`-style placeholders cannot flash the prior account's
77
- // private thumbnails while the new bearer mint lands.
78
- queryClient.removeQueries({ queryKey: [ASSET_DOWNLOAD_URLS_QUERY_KEY] });
79
- await refreshAccounts();
80
- queryClient.invalidateQueries();
81
- }, [refreshAccounts, queryClient]);
82
-
83
67
  const switchToAccount = useCallback(
84
68
  async (accountId: string): Promise<void> => {
85
69
  if (identityBound) {
@@ -92,7 +76,6 @@ export function useOxyAccountGraph({
92
76
  if (deviceState?.accounts.some((account) => account.accountId === accountId)) {
93
77
  await sessionClient.switchAccount(accountId);
94
78
  await syncFromClient();
95
- await runPostAccountSwitchSideEffects();
96
79
  return;
97
80
  }
98
81
 
@@ -114,9 +97,12 @@ export function useOxyAccountGraph({
114
97
  // `session_state` socket.
115
98
  { activate: true },
116
99
  );
117
- await runPostAccountSwitchSideEffects();
118
100
  },
119
- [identityBound, oxyServices, sessionClient, syncFromClient, commitSession, runPostAccountSwitchSideEffects],
101
+ // The account-scoped cache reset that used to trail both branches here now
102
+ // runs inside the runtime's subject transition, BEFORE any subscriber is
103
+ // woken — so a socket-pushed switch (which never reached this function at
104
+ // all) gets it too.
105
+ [identityBound, oxyServices, sessionClient, syncFromClient, commitSession],
120
106
  );
121
107
 
122
108
  const createAccount = useCallback(
@@ -0,0 +1,37 @@
1
+ import type { AccountDialogSnapshot } from '@oxyhq/core';
2
+
3
+ /**
4
+ * The snapshot `useSyncExternalStore` reads before a controller exists.
5
+ *
6
+ * `useSyncExternalStore` calls `getSnapshot` several times per commit and
7
+ * compares by identity, so the no-controller answer has to be one constant
8
+ * reference — a fresh object literal loops.
9
+ *
10
+ * It is ONE constant because there were three, byte-identical, in the hook and
11
+ * both dialog surfaces. Adding a field to `AccountDialogSnapshot` broke all
12
+ * three at once and would have gone on doing so; a shared constant makes the
13
+ * compiler point at one place.
14
+ */
15
+ export const EMPTY_ACCOUNT_DIALOG_SNAPSHOT: AccountDialogSnapshot = {
16
+ view: 'accounts',
17
+ directory: null,
18
+ activeContext: null,
19
+ activatingContextId: null,
20
+ removingContextId: null,
21
+ removingPrincipalId: null,
22
+ loading: false,
23
+ error: null,
24
+ signIn: {
25
+ phase: 'idle',
26
+ authorizeCode: null,
27
+ qrPayload: null,
28
+ expiresAt: null,
29
+ error: null,
30
+ route: null,
31
+ routeFailed: false,
32
+ pushSentAt: null,
33
+ openedAt: null,
34
+ progress: 'idle',
35
+ },
36
+ commonsAvailability: 'unknown',
37
+ };
@@ -18,9 +18,7 @@ import {
18
18
  import { mutationKeys } from './mutationKeys';
19
19
  import { useOxy } from '../../context/OxyContext';
20
20
  import { toast } from '@oxyhq/bloom/toast';
21
- import { refreshAvatarInStore } from '../../utils/avatarUtils';
22
21
  import { useAuthStore } from '../../stores/authStore';
23
- import { useAccountStore } from '../../stores/accountStore';
24
22
  import {
25
23
  clearedFieldsFromProfileUpdate,
26
24
  upsertCachedUser,
@@ -114,18 +112,6 @@ export const useUpdateProfile = () => {
114
112
  cleared: cleared.length > 0 ? cleared : undefined,
115
113
  });
116
114
 
117
- // Avatar add/remove: keep the account switcher in sync with profile writes
118
- if (typeof updates.avatar === 'string' && activeSessionId) {
119
- if (updates.avatar && oxyServices) {
120
- refreshAvatarInStore(activeSessionId, updates.avatar, oxyServices);
121
- } else {
122
- useAccountStore.getState().updateAccount(activeSessionId, {
123
- avatar: undefined,
124
- avatarUrl: undefined,
125
- });
126
- }
127
- }
128
-
129
115
  // Invalidate all related queries so every consumer (the account dialog,
130
116
  // session lists, managed accounts, etc.) refetches the fresh profile.
131
117
  // This is critical right after `username` is set the first time, when
@@ -209,11 +195,6 @@ export const useUploadAvatar = () => {
209
195
  // Update authStore so frontend components see the changes immediately
210
196
  useAuthStore.getState().setUser(data);
211
197
 
212
- // Refresh accountStore with cache-busted URL if avatar was updated
213
- if (data?.avatar && activeSessionId && oxyServices) {
214
- refreshAvatarInStore(activeSessionId, data.avatar, oxyServices);
215
- }
216
-
217
198
  // Invalidate all related queries to refresh everywhere, including the
218
199
  // sessions cache so other-account avatars update too.
219
200
  invalidateUserQueries(queryClient);
@@ -17,7 +17,8 @@ import { upsertCachedUser, upsertCachedUsers } from '../userCache';
17
17
  import { patchCachedUserRelationship } from '../userCacheRelationship';
18
18
  import type { CacheableUser } from '../userCache';
19
19
  import { queryKeys } from '../queryKeys';
20
- import { useAuthStore } from '../../../stores/authStore';
20
+ import { bindAuthStoreToRuntime, useAuthStore } from '../../../stores/authStore';
21
+ import { createTestRuntime } from '../../../../../__tests__/helpers/runtimeHarness';
21
22
 
22
23
  function makeClient(): QueryClient {
23
24
  return new QueryClient({
@@ -35,11 +36,21 @@ function readByUsername(qc: QueryClient, username: string, viewerId: string): Ca
35
36
  return qc.getQueryData<CacheableUser>(queryKeys.users.byUsername(username, viewerId));
36
37
  }
37
38
 
39
+ // `useAuthStore` projects the runtime now, so the viewer these cases read has
40
+ // to live somewhere: bind a runtime per test and let the writes go through it.
41
+ let unbindAuthStore: (() => void) | null = null;
42
+
38
43
  beforeEach(() => {
44
+ unbindAuthStore = bindAuthStoreToRuntime(createTestRuntime());
39
45
  // Default to anonymous viewer unless a test sets one.
40
46
  useAuthStore.setState({ user: null });
41
47
  });
42
48
 
49
+ afterEach(() => {
50
+ unbindAuthStore?.();
51
+ unbindAuthStore = null;
52
+ });
53
+
43
54
  describe('upsertCachedUser — cold slot', () => {
44
55
  it('seeds the full object and marks it STALE (updatedAt: 0) under both keys', () => {
45
56
  const qc = makeClient();
@@ -0,0 +1,110 @@
1
+ import { useCallback, useMemo, useSyncExternalStore } from 'react';
2
+ import {
3
+ buildSwitcherRows,
4
+ projectDevicePrincipals,
5
+ type DeviceContext,
6
+ type SwitcherPrincipalRow,
7
+ } from '@oxyhq/core';
8
+ import { useOxy } from '../context/OxyContext';
9
+ import { useI18n } from './useI18n';
10
+ import { EMPTY_ACCOUNT_DIALOG_SNAPSHOT } from './accountDialogSnapshot';
11
+
12
+ export interface UseDeviceSwitcherResult {
13
+ /**
14
+ * Everyone signed in on this device, each with the accounts they may act as,
15
+ * with names, handles and avatar URLs already resolved.
16
+ *
17
+ * Grouped by PERSON, which is the whole point: the same organization
18
+ * reachable through two people is two rows under two humans. The flat list
19
+ * this replaced was keyed by account id and could show only one of them.
20
+ */
21
+ principals: SwitcherPrincipalRow[];
22
+ /** The active `principal acting as account` pair, or `null`. */
23
+ activeContext: DeviceContext | null;
24
+ /** True until the first directory read settles with nothing to show. */
25
+ isLoading: boolean;
26
+ /** The `contextId` of an activation in flight, or `null`. */
27
+ activatingContextId: string | null;
28
+ /** The `contextId` of a context removal in flight, or `null`. */
29
+ removingContextId: string | null;
30
+ /** The `principalId` of a principal removal in flight, or `null`. */
31
+ removingPrincipalId: string | null;
32
+ /**
33
+ * Make one pair active. Resolves `false` when it was refused — the reason is
34
+ * on the dialog controller's `error`, and the row is simply not offered
35
+ * again after the refresh that follows.
36
+ */
37
+ activateContext: (contextId: string) => Promise<boolean>;
38
+ /** Drop ONE `principal → account` pair, leaving anyone else's route intact. */
39
+ signOutContext: (contextId: string) => Promise<boolean>;
40
+ /** Drop ONE PERSON and every context they reach, and nobody else's. */
41
+ signOutPrincipal: (principalId: string) => Promise<boolean>;
42
+ }
43
+
44
+ /**
45
+ * The device switcher, straight from `GET /session/device/directory` (ADR 0002).
46
+ *
47
+ * A thin binding over the shared `AccountDialogController` in `@oxyhq/core` —
48
+ * the SAME headless source `OxyAccountDialogScreen` renders — so the SDK's own
49
+ * dialog and the `auth.oxy.so` chooser show one list, built once, on the server.
50
+ *
51
+ * It used to hand back a flat `SwitchableAccount[]` assembled here from
52
+ * `listAccounts()` + `getUsersByIds()` unioned with the device's session set.
53
+ * That union was not merely redundant: the client holds ONE caller's account
54
+ * graph, so on a device holding two people it presented one person's answer as
55
+ * the device's, and switchability — an authorization question — was decided
56
+ * client-side. Both are the server's answers now, and selecting a row activates
57
+ * a `contextId`, never an `accountId`.
58
+ */
59
+ export function useDeviceSwitcher(): UseDeviceSwitcherResult {
60
+ const { accountDialogController: controller, oxyServices } = useOxy();
61
+ const { locale } = useI18n();
62
+
63
+ const subscribe = useCallback(
64
+ (listener: () => void) => (controller ? controller.subscribe(listener) : () => undefined),
65
+ [controller],
66
+ );
67
+ const getSnapshot = useCallback(
68
+ () => (controller ? controller.getSnapshot() : EMPTY_ACCOUNT_DIALOG_SNAPSHOT),
69
+ [controller],
70
+ );
71
+ const snapshot = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
72
+
73
+ // `directory` is a stable reference between reads (`SessionClient` holds the
74
+ // one it applied), so this recomputes only when the device actually moved.
75
+ const principals = useMemo(
76
+ () =>
77
+ buildSwitcherRows(
78
+ projectDevicePrincipals(snapshot.directory),
79
+ snapshot.activeContext?.contextId ?? null,
80
+ (avatar) => (avatar ? oxyServices.getFileDownloadUrl(avatar, 'thumb') : undefined),
81
+ locale,
82
+ ),
83
+ [snapshot.directory, snapshot.activeContext, oxyServices, locale],
84
+ );
85
+
86
+ const activateContext = useCallback(
87
+ async (contextId: string) => (controller ? controller.activateContext(contextId) : false),
88
+ [controller],
89
+ );
90
+ const signOutContext = useCallback(
91
+ async (contextId: string) => (controller ? controller.signOutContext(contextId) : false),
92
+ [controller],
93
+ );
94
+ const signOutPrincipal = useCallback(
95
+ async (principalId: string) => (controller ? controller.signOutPrincipal(principalId) : false),
96
+ [controller],
97
+ );
98
+
99
+ return {
100
+ principals,
101
+ activeContext: snapshot.activeContext,
102
+ isLoading: snapshot.loading,
103
+ activatingContextId: snapshot.activatingContextId,
104
+ removingContextId: snapshot.removingContextId,
105
+ removingPrincipalId: snapshot.removingPrincipalId,
106
+ activateContext,
107
+ signOutContext,
108
+ signOutPrincipal,
109
+ };
110
+ }
@@ -1,12 +1,32 @@
1
1
  import { useMemo } from 'react';
2
- import { useOxy } from '../context/OxyContext';
2
+ import { useOptionalOxy } from '../context/OxyContext';
3
3
  import { translate } from '@oxyhq/core';
4
4
 
5
+ /**
6
+ * The locale `@oxyhq/core`'s translator falls back to. Mirrors its own
7
+ * `FALLBACK`, so a surface rendered without a provider reads the same strings
8
+ * an unrecognised locale would.
9
+ */
10
+ const FALLBACK_LOCALE = 'en-US';
11
+
12
+ /**
13
+ * Display copy for SDK surfaces.
14
+ *
15
+ * This is the ONE hook that reads the runtime optionally. The locale is display
16
+ * metadata, not session state — ADR 0004 lists it among the things that come off
17
+ * the central value entirely — and purely presentational surfaces
18
+ * (`OxyConsentScreen`, `OxySignInRequestSurface`) are rendered outside a
19
+ * provider on purpose: `packages/auth`'s production-bundle probe builds the real
20
+ * Vite bundle and renders the surface standalone to prove every element beneath
21
+ * it links. Throwing there would report a linkage failure that is not one.
22
+ *
23
+ * Anything that reads or mutates session state uses `useOxy()`, which throws.
24
+ */
5
25
  export function useI18n() {
6
- const { currentLanguage } = useOxy();
26
+ const oxy = useOptionalOxy();
27
+ const currentLanguage = oxy?.currentLanguage ?? FALLBACK_LOCALE;
7
28
  const t = useMemo(() => {
8
29
  return (key: string, vars?: Record<string, string | number>) => translate(currentLanguage, key, vars);
9
30
  }, [currentLanguage]);
10
31
  return { t, locale: currentLanguage };
11
32
  }
12
-