@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
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Config plugin: withSharedDeviceSessionReader (every other official Oxy app).
3
+ *
4
+ * A reader app JOINS the device's existing session — it adopts the shared
5
+ * `deviceId` + `deviceSecret` a hub app published, so it lands signed in with no
6
+ * QR and, crucially, without ever asking for Commons's private identity key.
7
+ * This plugin wires the minimal Android side of `@oxyhq/services`:
8
+ *
9
+ * - Requests the `signature`-level permission
10
+ * `so.oxy.shared.permission.READ_DEVICE_SESSION` (defined by the hub apps).
11
+ * Because it is `signature`, it is granted only when this app is signed with
12
+ * the SAME certificate as the hub — that is the entire trust boundary, and it
13
+ * is what makes an incorrectly-signed app unable to read the slot.
14
+ * - Adds `<queries>` entries for the hub authorities so package-visibility
15
+ * filtering (Android 11+) never hides them from `ContentResolver.call`.
16
+ *
17
+ * The provider itself is declared only by the hub apps
18
+ * (`withSharedDeviceSessionProvider`) — see the note there about why hosting is
19
+ * restricted to apps inside the `so.oxy.shared` UID.
20
+ *
21
+ * Keep this list identical to `PROVIDER_AUTHORITIES` in
22
+ * `android/src/main/java/so/oxy/devicesession/OxyDeviceSessionModule.kt`.
23
+ */
24
+ const { withAndroidManifest } = require('@expo/config-plugins');
25
+
26
+ const READ_DEVICE_SESSION_PERMISSION = 'so.oxy.shared.permission.READ_DEVICE_SESSION';
27
+ const PROVIDER_AUTHORITIES = [
28
+ 'so.oxy.accounts.devicesession',
29
+ 'so.oxy.accounts.dev.devicesession',
30
+ 'so.oxy.commons.devicesession',
31
+ 'so.oxy.commons.dev.devicesession',
32
+ ];
33
+
34
+ module.exports = function withSharedDeviceSessionReader(config) {
35
+ return withAndroidManifest(config, (modConfig) => {
36
+ const manifest = modConfig.modResults.manifest;
37
+
38
+ manifest['uses-permission'] = manifest['uses-permission'] ?? [];
39
+ if (!manifest['uses-permission'].some((p) => p.$['android:name'] === READ_DEVICE_SESSION_PERMISSION)) {
40
+ manifest['uses-permission'].push({ $: { 'android:name': READ_DEVICE_SESSION_PERMISSION } });
41
+ }
42
+
43
+ manifest['queries'] = manifest['queries'] ?? [];
44
+ if (manifest['queries'].length === 0) {
45
+ manifest['queries'].push({});
46
+ }
47
+ const queries = manifest['queries'][0];
48
+ queries.provider = queries.provider ?? [];
49
+ for (const authority of PROVIDER_AUTHORITIES) {
50
+ if (!queries.provider.some((p) => p.$['android:authorities'] === authority)) {
51
+ queries.provider.push({ $: { 'android:authorities': authority } });
52
+ }
53
+ }
54
+
55
+ return modConfig;
56
+ });
57
+ };
@@ -0,0 +1,61 @@
1
+ /**
2
+ * `@oxyhq/services` ships an ESM build, and ESM has no `require`.
3
+ *
4
+ * This is not a style rule: a surviving `require()` puts web bundlers into
5
+ * CommonJS interop, which hands consumers `undefined` bindings with no build
6
+ * error and no warning. It blanked auth.oxy.so/authorize once — see the note on
7
+ * `screenComponents` in `../ui/navigation/routes` for the mechanism, and
8
+ * `README.md` for the incident.
9
+ *
10
+ * Deferring is still legitimate: a thunk over a static import defers a READ,
11
+ * `import()` defers a LOAD. Neither needs `require()`.
12
+ */
13
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
14
+ import { join, relative } from 'node:path';
15
+
16
+ const SRC_ROOT = join(__dirname, '..');
17
+
18
+ /** Source extensions that end up in the published ESM build. */
19
+ const SOURCE_EXTENSIONS = ['.ts', '.tsx'];
20
+
21
+ /** Tests are compiled to CommonJS by ts-jest and never ship, so they are exempt. */
22
+ const EXEMPT_SEGMENTS = ['__tests__'];
23
+
24
+ function collectSourceFiles(dir: string): string[] {
25
+ const found: string[] = [];
26
+ for (const entry of readdirSync(dir)) {
27
+ const path = join(dir, entry);
28
+ if (statSync(path).isDirectory()) {
29
+ if (EXEMPT_SEGMENTS.includes(entry)) continue;
30
+ found.push(...collectSourceFiles(path));
31
+ continue;
32
+ }
33
+ if (SOURCE_EXTENSIONS.some((ext) => path.endsWith(ext)) && !path.endsWith('.d.ts')) {
34
+ found.push(path);
35
+ }
36
+ }
37
+ return found;
38
+ }
39
+
40
+ /**
41
+ * Drop comments and string/template literals so prose about `require()` — such
42
+ * as the notes explaining this very rule — is not mistaken for a call.
43
+ */
44
+ function stripCommentsAndStrings(code: string): string {
45
+ return code
46
+ .replace(/\/\*[\s\S]*?\*\//g, '')
47
+ .replace(/(^|[^:])\/\/.*$/gm, '$1')
48
+ .replace(/'(?:[^'\\\n]|\\.)*'/g, "''")
49
+ .replace(/"(?:[^"\\\n]|\\.)*"/g, '""')
50
+ .replace(/`(?:[^`\\]|\\.)*`/g, '``');
51
+ }
52
+
53
+ describe('ESM source purity', () => {
54
+ it('never calls require() anywhere in the shipped source', () => {
55
+ const offenders = collectSourceFiles(SRC_ROOT)
56
+ .filter((file) => /\brequire\s*\(/.test(stripCommentsAndStrings(readFileSync(file, 'utf8'))))
57
+ .map((file) => relative(SRC_ROOT, file));
58
+
59
+ expect(offenders).toEqual([]);
60
+ });
61
+ });
package/src/index.ts CHANGED
@@ -33,6 +33,29 @@ setPlatformOS(Platform.OS as PlatformOS);
33
33
  // ---------------------------------------------------------------------------
34
34
  export { default as OxyProvider } from './ui/components/OxyProvider';
35
35
  export { useOxy } from './ui/context/OxyContext';
36
+ // `useOxy()` THROWS when no provider is mounted. `useOptionalOxy()` returns
37
+ // `null` instead, for the rare component that renders on both sides of the
38
+ // provider boundary; there is no fabricated forever-loading runtime.
39
+ export { useOptionalOxy, OxyProviderMissingError } from './ui/context/OxyContext';
40
+
41
+ // The runtime surface (ADR 0004). These subscribe to the ONE runtime with a
42
+ // selector, so — unlike `useOxy()`, whose value is rebuilt by any of its fifty
43
+ // members moving — a locale change or a dialog opening does not re-render them.
44
+ export {
45
+ useOxyRuntime,
46
+ useOxySnapshot,
47
+ useActiveAccount,
48
+ useDeviceDirectory,
49
+ OxyRuntimeMissingError,
50
+ } from './ui/runtime';
51
+ export type {
52
+ ActiveAccount,
53
+ OxyRuntime,
54
+ OxyRuntimeError,
55
+ OxyRuntimeSnapshot,
56
+ OxyRuntimeStatus,
57
+ OxyTokenStatus,
58
+ } from './ui/runtime';
36
59
  export type { OxyContextState } from './ui/context/OxyContext';
37
60
  export { useAuth } from './ui/hooks/useAuth';
38
61
  export type { AuthState, AuthActions, UseAuthReturn } from './ui/hooks/useAuth';
@@ -45,7 +68,6 @@ export { IdentityBoundSessionError } from './ui/session/identityBinding';
45
68
  // Zustand stores
46
69
  // ---------------------------------------------------------------------------
47
70
  export { useAuthStore } from './ui/stores/authStore';
48
- export { useAccountStore } from './ui/stores/accountStore';
49
71
  export {
50
72
  useAssetStore,
51
73
  useAssets as useAssetsStore,
@@ -343,14 +365,14 @@ export { OxySignInRequestSurface } from './ui/components/OxySignInRequestSurface
343
365
  export type { OxySignInRequestSurfaceProps } from './ui/components/OxySignInRequestSurface';
344
366
  export type { OxySignInSurfaceAction } from './ui/components/authChooser/types';
345
367
 
346
- // Unified switchable-accounts hook — the single source of everything the user
347
- // can switch into: device sign-ins AND linked graph accounts (owned orgs +
348
- // shared-with-you), deduped by account id and hydrated with real
349
- // name/email/avatar/color. Backed by the shared `AccountDialogController` in
350
- // `@oxyhq/core`. Every switch routes through `useOxy().switchToAccount`.
351
- // The `SwitchableAccount` type lives in `@oxyhq/core` — import it from there.
352
- export { useSwitchableAccounts } from './ui/hooks/useSwitchableAccounts';
353
- export type { UseSwitchableAccountsResult } from './ui/hooks/useSwitchableAccounts';
368
+ // The device switcher — the server's own directory (ADR 0002) of who is signed
369
+ // in here and what each of them may act as, grouped by PERSON, with the two
370
+ // removals an account id cannot name. Backed by the shared
371
+ // `AccountDialogController` in `@oxyhq/core`; selecting a row activates a
372
+ // `contextId`. `DevicePrincipalGroup` and `DeviceContext` live in `@oxyhq/core`
373
+ // — import them from there.
374
+ export { useDeviceSwitcher } from './ui/hooks/useDeviceSwitcher';
375
+ export type { UseDeviceSwitcherResult } from './ui/hooks/useDeviceSwitcher';
354
376
 
355
377
  // Unified "Manage your Oxy Account" screen (the caller's own personal account)
356
378
  export { default as ProfileScreen } from './ui/screens/ProfileScreen';
@@ -8,6 +8,7 @@ import {
8
8
  } from '@oxyhq/core';
9
9
  import type { SessionClient } from '@oxyhq/core';
10
10
  import { loadPersistedDeviceCredential } from '../utils/deviceCredential';
11
+ import { createPlatformSharedDeviceCredentialStore } from '../session/sharedDeviceCredentialStore';
11
12
  import { tryCompleteOAuthReturn } from '../utils/oauthReturn';
12
13
  import { isWebBrowser } from '../utils/isWebBrowser';
13
14
  import { isNetConnectivityExplicitlyOffline } from '../utils/netConnectivity';
@@ -161,6 +162,12 @@ export async function runProviderColdBoot(opts: RunProviderColdBootOptions): Pro
161
162
  platform: { isWeb: isWebBrowser(), isNative: !isWebBrowser() },
162
163
  sessionMode,
163
164
  identity,
165
+ // The device-wide shared credential slot, enabling the `shared-device-adopt`
166
+ // lane: a newly installed official app joins this device's existing session
167
+ // instead of falling back to signing a challenge with the Commons identity
168
+ // key. `null` on web, where each origin is its own device by design; the
169
+ // core lane is also skipped outright in `sessionMode: 'identity'`.
170
+ sharedDeviceCredential: createPlatformSharedDeviceCredentialStore() ?? undefined,
164
171
  overallDeadlineMs: COLD_BOOT_OVERALL_DEADLINE_MS,
165
172
  isOffline: () => offline,
166
173
  onStepDeadline: (stepId) => {
package/src/ui/client.ts CHANGED
@@ -19,7 +19,7 @@ export { default as FollowButton } from './components/FollowButton';
19
19
  export { default as OxyPayButton } from './components/OxyPayButton';
20
20
 
21
21
  // Context
22
- export { useOxy } from './context/OxyContext';
22
+ export { useOxy, useOptionalOxy, OxyProviderMissingError } from './context/OxyContext';
23
23
 
24
24
  // Hooks
25
25
  export { useAuth } from './hooks/useAuth';
@@ -34,7 +34,6 @@ export { default as ManageAccountScreen } from './screens/ManageAccountScreen';
34
34
 
35
35
  // Stores
36
36
  export { useAuthStore } from './stores/authStore';
37
- export { useAccountStore } from './stores/accountStore';
38
37
 
39
38
  // Error handler utilities
40
39
  export {
@@ -31,6 +31,7 @@ import { useSurfaceHeader } from '../hooks/useSurfaceHeader';
31
31
  import type { BaseScreenProps } from '../types/navigation';
32
32
  import LogoText from './logo/LogoText';
33
33
  import OxyAuthChooser from './OxyAuthChooser';
34
+ import { EMPTY_ACCOUNT_DIALOG_SNAPSHOT } from '../hooks/accountDialogSnapshot';
34
35
 
35
36
  type Translate = ReturnType<typeof useI18n>['t'];
36
37
 
@@ -61,20 +62,22 @@ const OxyAccountDialogScreen: React.FC<BaseScreenProps> = ({ canGoBack }) => {
61
62
 
62
63
  // A lightweight, header-only binding to the same controller `OxyAuthChooser`
63
64
  // binds independently — cheap, and the established pattern here (
64
- // `useSwitchableAccounts` also binds to this controller on its own).
65
+ // `useDeviceSwitcher` also binds to this controller on its own).
65
66
  const subscribe = useCallback(
66
67
  (listener: () => void) => (controller ? controller.subscribe(listener) : () => undefined),
67
68
  [controller],
68
69
  );
69
70
  const getSnapshot = useCallback(
70
- () => (controller ? controller.getSnapshot() : EMPTY_SNAPSHOT),
71
+ () => (controller ? controller.getSnapshot() : EMPTY_ACCOUNT_DIALOG_SNAPSHOT),
71
72
  [controller],
72
73
  );
73
74
  const snapshot = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
74
75
 
75
76
  const { view } = snapshot;
76
- const showBack =
77
- view === 'qr' || view === 'signup' || (view === 'add' && snapshot.accounts.length > 0);
77
+ // "Add another account" only makes sense as a back destination when there IS
78
+ // another i.e. somebody is already signed in on this device.
79
+ const hasSignedInAccounts = (snapshot.directory?.principals.length ?? 0) > 0;
80
+ const showBack = view === 'qr' || view === 'signup' || (view === 'add' && hasSignedInAccounts);
78
81
  const goToAccounts = useCallback(() => {
79
82
  if (!controller) return;
80
83
  const { view: currentView, signIn } = controller.getSnapshot();
@@ -100,7 +103,7 @@ const OxyAccountDialogScreen: React.FC<BaseScreenProps> = ({ canGoBack }) => {
100
103
  // by the bar. Every OTHER view keeps the SHARED Dialog nav header the rest of
101
104
  // the SDK uses — a large in-content title/subtitle that collapses into the bar
102
105
  // on scroll — because their copy is informative.
103
- const copy = view === 'accounts' ? null : headerCopy(view, snapshot.accounts.length, t);
106
+ const copy = view === 'accounts' ? null : headerCopy(view, hasSignedInAccounts, t);
104
107
 
105
108
  useSurfaceHeader({
106
109
  titleContent: copy ? undefined : NAV_LOGO,
@@ -126,7 +129,7 @@ const OxyAccountDialogScreen: React.FC<BaseScreenProps> = ({ canGoBack }) => {
126
129
 
127
130
  function headerCopy(
128
131
  view: Exclude<AccountDialogSnapshot['view'], 'accounts'>,
129
- accountCount: number,
132
+ hasSignedInAccounts: boolean,
130
133
  t: Translate,
131
134
  ): { title: string; subtitle: string | null } {
132
135
  switch (view) {
@@ -147,7 +150,7 @@ function headerCopy(
147
150
  subtitle: t('signup.subtitle') || 'One identity for the whole ecosystem.',
148
151
  };
149
152
  default:
150
- return accountCount > 0
153
+ return hasSignedInAccounts
151
154
  ? {
152
155
  title: t('signin.addAccountTitle') || 'Add another account',
153
156
  subtitle: t('signin.addAccountSubtitle') || 'Sign in with another account.',
@@ -159,28 +162,6 @@ function headerCopy(
159
162
  }
160
163
  }
161
164
 
162
- const EMPTY_SNAPSHOT: AccountDialogSnapshot = {
163
- view: 'accounts',
164
- accounts: [],
165
- activeAccountId: null,
166
- loading: false,
167
- error: null,
168
- switchingAccountId: null,
169
- signIn: {
170
- phase: 'idle',
171
- authorizeCode: null,
172
- qrPayload: null,
173
- expiresAt: null,
174
- error: null,
175
- route: null,
176
- routeFailed: false,
177
- pushSentAt: null,
178
- openedAt: null,
179
- progress: 'idle',
180
- },
181
- commonsAvailability: 'unknown',
182
- };
183
-
184
165
  /**
185
166
  * The screen gutter. In the Dialog's nav-header mode the surface adds NO content
186
167
  * padding of its own — the large title and each screen own theirs — so this must
@@ -37,11 +37,12 @@ import type React from 'react';
37
37
  import { useCallback, useMemo, useState, useSyncExternalStore } from 'react';
38
38
  import { Linking, Platform } from 'react-native';
39
39
  import { toast } from '@oxyhq/bloom/toast';
40
+ import { surfaces } from '@oxyhq/bloom/surfaces';
40
41
  import { useTheme } from '@oxyhq/bloom/theme';
41
- import type { AccountDialogSnapshot } from '@oxyhq/core';
42
- import { isOxyRpOrigin } from '@oxyhq/core';
42
+ import { getNormalizedUserHandle, isOxyRpOrigin, type User } from '@oxyhq/core';
43
43
  import { useQueryClient } from '@tanstack/react-query';
44
44
  import { useOxy } from '../context/OxyContext';
45
+ import { useDeviceSwitcher } from '../hooks/useDeviceSwitcher';
45
46
  import { useI18n } from '../hooks/useI18n';
46
47
  import {
47
48
  getAccountDialogConsumerHooks,
@@ -54,13 +55,16 @@ import AccountsMenuView from './authChooser/AccountsMenuView';
54
55
  import SignInEntryView from './authChooser/SignInEntryView';
55
56
  import SignInRequestView from './authChooser/SignInRequestView';
56
57
  import SignUpView from './authChooser/SignUpView';
57
- import type {
58
- AccountStorageModel,
59
- AccountsMenuActions,
60
- OxyAuthChooserHandlers,
61
- PasskeyMode,
62
- SignInAlternatives,
58
+ import {
59
+ resolveAccentHex,
60
+ type AccountHeroModel,
61
+ type AccountStorageModel,
62
+ type AccountsMenuActions,
63
+ type OxyAuthChooserHandlers,
64
+ type PasskeyMode,
65
+ type SignInAlternatives,
63
66
  } from './authChooser/types';
67
+ import { EMPTY_ACCOUNT_DIALOG_SNAPSHOT } from '../hooks/accountDialogSnapshot';
64
68
 
65
69
  /** Commons' own identity-creation deep link (mirrors the `approve`/`attest`/`card` intents). */
66
70
  const COMMONS_CREATE_IDENTITY_URL = 'oxycommons://create-identity';
@@ -96,16 +100,20 @@ const OxyAuthChooser: React.FC<OxyAuthChooserProps> = ({
96
100
  const {
97
101
  accountDialogController: controller,
98
102
  showBottomSheet,
99
- refreshAccounts,
100
103
  signInWithPasskey,
101
104
  registerWithPasskey,
102
105
  oxyServices,
103
106
  logout,
104
107
  openAvatarPicker,
108
+ user,
105
109
  } = useOxy();
106
110
  const theme = useTheme();
107
111
  const { t } = useI18n();
108
112
  const queryClient = useQueryClient();
113
+ // The device's people and what each may act as, resolved for rendering — the
114
+ // SAME hook (and the same one builder) the auth.oxy.so chooser renders from,
115
+ // so the two switchers cannot drift.
116
+ const { principals } = useDeviceSwitcher();
109
117
 
110
118
  // A credential minted for `oxy.so` can only be ASSERTED there (or a loopback
111
119
  // dev host) — a hard WebAuthn RP-ID boundary the browser enforces, not
@@ -217,7 +225,7 @@ const OxyAuthChooser: React.FC<OxyAuthChooserProps> = ({
217
225
  [controller, autoStartSignIn],
218
226
  );
219
227
  const getSnapshot = useCallback(
220
- () => (controller ? controller.getSnapshot() : EMPTY_SNAPSHOT),
228
+ () => (controller ? controller.getSnapshot() : EMPTY_ACCOUNT_DIALOG_SNAPSHOT),
221
229
  [controller],
222
230
  );
223
231
  const snapshot = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
@@ -228,31 +236,97 @@ const OxyAuthChooser: React.FC<OxyAuthChooserProps> = ({
228
236
  () => null,
229
237
  );
230
238
 
231
- const handleSwitch = useCallback(
232
- async (accountId: string) => {
239
+ const handleActivate = useCallback(
240
+ async (contextId: string) => {
233
241
  if (!controller) return;
234
- if (controller.getSnapshot().switchingAccountId) return;
235
- if (accountId === controller.getSnapshot().activeAccountId) {
242
+ if (controller.getSnapshot().activatingContextId) return;
243
+ // Already live: activating it again bumps nothing server-side, so treat
244
+ // the press as "yes, this one" and close.
245
+ if (contextId === controller.getSnapshot().activeContext?.contextId) {
236
246
  onComplete?.();
237
247
  return;
238
248
  }
239
249
  try {
240
- await controller.switchTo(accountId);
241
- // `switchTo` records failures on the controller snapshot. Read that
242
- // state after the operation so the same path works with both the real
243
- // controller and framework test doubles that return void.
250
+ await controller.activateContext(contextId);
251
+ // Failures are recorded on the controller snapshot rather than thrown,
252
+ // so the same path works with the real controller and with test doubles
253
+ // that resolve void.
244
254
  if (controller.getSnapshot().error) {
245
- toast.error(t('accountSwitcher.toasts.switchFailed'));
255
+ toast.error(t('accountSwitcher.toasts.activateFailed'));
246
256
  return;
247
257
  }
248
- void refreshAccounts();
258
+ // The subject changed, so every account-scoped query is now about
259
+ // somebody else. The runtime resets its own caches between the bearer
260
+ // commit and the notify; this drops the Query cache the same way the
261
+ // account switch did.
249
262
  queryClient.invalidateQueries();
250
263
  onComplete?.();
251
264
  } catch {
252
- toast.error(t('accountSwitcher.toasts.switchFailed'));
265
+ toast.error(t('accountSwitcher.toasts.activateFailed'));
266
+ }
267
+ },
268
+ [controller, onComplete, queryClient, t],
269
+ );
270
+
271
+ /**
272
+ * Remove ONE `principal → account` pair.
273
+ *
274
+ * Confirmed, and the confirmation says whose route is going: on a shared
275
+ * device the same organization is reachable through somebody else, and their
276
+ * access is untouched. That is the fact a single "remove account" control
277
+ * would get wrong.
278
+ */
279
+ const handleRemoveContext = useCallback(
280
+ async (contextId: string) => {
281
+ if (!controller) return;
282
+ const group = principals.find((principal) =>
283
+ principal.contexts.some((context) => context.contextId === contextId),
284
+ );
285
+ const context = group?.contexts.find((row) => row.contextId === contextId);
286
+ if (!group || !context) return;
287
+ const confirmed = await surfaces.confirm({
288
+ title: t('accountSwitcher.confirms.removeContextTitle'),
289
+ message: t('accountSwitcher.confirms.removeContext', {
290
+ person: group.displayName,
291
+ account: context.displayName,
292
+ }),
293
+ confirmLabel: t('common.remove'),
294
+ cancelLabel: t('common.cancel'),
295
+ destructive: true,
296
+ });
297
+ if (!confirmed) return;
298
+ const removed = await controller.signOutContext(contextId);
299
+ if (!removed) {
300
+ toast.error(t('accountSwitcher.toasts.contextRemoveFailed'));
301
+ return;
302
+ }
303
+ toast.success(t('accountSwitcher.toasts.contextRemoved', { account: context.displayName }));
304
+ },
305
+ [controller, principals, t],
306
+ );
307
+
308
+ /** Remove ONE PERSON and every account they reach here, and nobody else's. */
309
+ const handleRemovePrincipal = useCallback(
310
+ async (principalId: string) => {
311
+ if (!controller) return;
312
+ const group = principals.find((principal) => principal.principalId === principalId);
313
+ if (!group) return;
314
+ const confirmed = await surfaces.confirm({
315
+ title: t('accountSwitcher.confirms.removePrincipalTitle'),
316
+ message: t('accountSwitcher.confirms.removePrincipal', { name: group.displayName }),
317
+ confirmLabel: t('common.actions.signOut'),
318
+ cancelLabel: t('common.cancel'),
319
+ destructive: true,
320
+ });
321
+ if (!confirmed) return;
322
+ const removed = await controller.signOutPrincipal(principalId);
323
+ if (!removed) {
324
+ toast.error(t('accountSwitcher.toasts.principalRemoveFailed'));
325
+ return;
253
326
  }
327
+ toast.success(t('accountSwitcher.toasts.principalRemoved', { name: group.displayName }));
254
328
  },
255
- [controller, onComplete, refreshAccounts, queryClient, t],
329
+ [controller, principals, t],
256
330
  );
257
331
 
258
332
  const handleManage = useCallback(() => {
@@ -278,8 +352,14 @@ const OxyAuthChooser: React.FC<OxyAuthChooserProps> = ({
278
352
 
279
353
  const handlers = useMemo<OxyAuthChooserHandlers>(
280
354
  () => ({
281
- onSwitch: (accountId) => {
282
- void handleSwitch(accountId);
355
+ onActivate: (contextId) => {
356
+ void handleActivate(contextId);
357
+ },
358
+ onRemoveContext: (contextId) => {
359
+ void handleRemoveContext(contextId);
360
+ },
361
+ onRemovePrincipal: (principalId) => {
362
+ void handleRemovePrincipal(principalId);
283
363
  },
284
364
  onAdd: handleAdd,
285
365
  onManage: handleManage,
@@ -287,7 +367,21 @@ const OxyAuthChooser: React.FC<OxyAuthChooserProps> = ({
287
367
  // uploads / clears the picked photo — the single shared avatar write path.
288
368
  onEditAvatar: () => openAvatarPicker(),
289
369
  }),
290
- [handleSwitch, handleAdd, handleManage, openAvatarPicker],
370
+ [handleActivate, handleRemoveContext, handleRemovePrincipal, handleAdd, handleManage, openAvatarPicker],
371
+ );
372
+
373
+ /**
374
+ * The hero block — the account this client is signed in as, from its FULL
375
+ * profile rather than from a directory row.
376
+ *
377
+ * The directory carries the minimum that renders a row for every person on
378
+ * the device; this is the one account we legitimately hold everything for, so
379
+ * the hero keeps its real EMAIL, which the rows below do not pretend to have.
380
+ * The accent is not in that category — every row is drawn in its own account's
381
+ * (issue #961), this one included, from the same field.
382
+ */
383
+ const hero = useMemo<AccountHeroModel | null>(() => (user ? buildHero(user, theme.colors.primary, oxyServices) : null),
384
+ [user, theme.colors.primary, oxyServices],
291
385
  );
292
386
 
293
387
  // Everything that is NOT the surface's one primary action. Wired once here;
@@ -373,6 +467,8 @@ const OxyAuthChooser: React.FC<OxyAuthChooserProps> = ({
373
467
  return (
374
468
  <AccountsMenuView
375
469
  snapshot={snapshot}
470
+ principals={principals}
471
+ hero={hero}
376
472
  theme={theme}
377
473
  t={t}
378
474
  handlers={handlers}
@@ -417,6 +513,7 @@ const OxyAuthChooser: React.FC<OxyAuthChooserProps> = ({
417
513
  return (
418
514
  <SignInEntryView
419
515
  snapshot={snapshot}
516
+ principals={principals}
420
517
  theme={theme}
421
518
  t={t}
422
519
  handlers={handlers}
@@ -426,30 +523,32 @@ const OxyAuthChooser: React.FC<OxyAuthChooserProps> = ({
426
523
  );
427
524
  };
428
525
 
429
- // ---------------------------------------------------------------------------
430
- // Empty snapshot (no-provider loading state)
431
- // ---------------------------------------------------------------------------
432
-
433
- const EMPTY_SNAPSHOT: AccountDialogSnapshot = {
434
- view: 'accounts',
435
- accounts: [],
436
- activeAccountId: null,
437
- loading: false,
438
- error: null,
439
- switchingAccountId: null,
440
- signIn: {
441
- phase: 'idle',
442
- authorizeCode: null,
443
- qrPayload: null,
444
- expiresAt: null,
445
- error: null,
446
- route: null,
447
- routeFailed: false,
448
- pushSentAt: null,
449
- openedAt: null,
450
- progress: 'idle',
451
- },
452
- commonsAvailability: 'unknown',
453
- };
526
+ /**
527
+ * The hero model for the signed-in account.
528
+ *
529
+ * The address line is the account's canonical `@oxy.so` email when it has one,
530
+ * else its normalized `@handle`. It never synthesizes a `username@oxy.so`
531
+ * address — the identity contract forbids it, and a non-Oxy or missing email
532
+ * falls through to the handle.
533
+ */
534
+ function buildHero(
535
+ user: User,
536
+ fallbackAccent: string,
537
+ oxyServices: ReturnType<typeof useOxy>['oxyServices'],
538
+ ): AccountHeroModel {
539
+ const handle = getNormalizedUserHandle(user);
540
+ const displayName = user.name?.displayName?.trim() || handle || '';
541
+ const addressLine = user.email?.toLowerCase().endsWith('@oxy.so')
542
+ ? user.email
543
+ : handle
544
+ ? `@${handle}`
545
+ : null;
546
+ return {
547
+ displayName,
548
+ addressLine,
549
+ avatarUrl: user.avatar ? oxyServices.getFileDownloadUrl(user.avatar, 'thumb') : undefined,
550
+ accentHex: resolveAccentHex(user.color ?? null, fallbackAccent),
551
+ };
552
+ }
454
553
 
455
554
  export default OxyAuthChooser;
@@ -3,7 +3,7 @@ import { AppState, Platform, StyleSheet, View } from 'react-native';
3
3
  import { GestureHandlerRootView } from 'react-native-gesture-handler';
4
4
  import { SafeAreaProvider } from 'react-native-safe-area-context';
5
5
  import type { OxyProviderProps } from '../types/navigation';
6
- import { OxyContextProvider, type OxyContextProviderProps } from '../context/OxyContext';
6
+ import { OxyRuntimeProvider, type OxyRuntimeProviderProps } from '../context/OxyContext';
7
7
  import { QueryClientProvider, focusManager, onlineManager } from '@tanstack/react-query';
8
8
  import { BloomDialogProvider } from '@oxyhq/bloom/dialog';
9
9
  import { SurfaceProvider } from '@oxyhq/bloom/surfaces';
@@ -97,6 +97,7 @@ const OxyProvider: FC<OxyProviderProps> = ({
97
97
  queryClient: providedQueryClient,
98
98
  requireAuth = 'off',
99
99
  backgroundSession = false,
100
+ deviceCredentialStorage = 'persistent',
100
101
  }) => {
101
102
 
102
103
  // Dynamic KeyboardProvider for native. Uses variable indirection
@@ -284,8 +285,8 @@ const OxyProvider: FC<OxyProviderProps> = ({
284
285
  const coreContent = (
285
286
  <QueryClientProvider client={queryClient}>
286
287
  <BloomDialogProvider>
287
- <OxyContextProvider
288
- oxyServices={oxyServices as OxyContextProviderProps['oxyServices']}
288
+ <OxyRuntimeProvider
289
+ oxyServices={oxyServices as OxyRuntimeProviderProps['oxyServices']}
289
290
  baseURL={baseURL}
290
291
  authWebUrl={authWebUrl}
291
292
  authRedirectUri={authRedirectUri}
@@ -295,7 +296,8 @@ const OxyProvider: FC<OxyProviderProps> = ({
295
296
  sessionMode={sessionMode}
296
297
  webAuthMode={webAuthMode}
297
298
  backgroundSession={backgroundSession}
298
- onAuthStateChange={onAuthStateChange as OxyContextProviderProps['onAuthStateChange']}
299
+ deviceCredentialStorage={deviceCredentialStorage}
300
+ onAuthStateChange={onAuthStateChange as OxyRuntimeProviderProps['onAuthStateChange']}
299
301
  >
300
302
  <SurfaceProvider>
301
303
  {requireAuth === 'off' ? (
@@ -305,7 +307,7 @@ const OxyProvider: FC<OxyProviderProps> = ({
305
307
  )}
306
308
  </SurfaceProvider>
307
309
  <ToastOutlet />
308
- </OxyContextProvider>
310
+ </OxyRuntimeProvider>
309
311
  </BloomDialogProvider>
310
312
  </QueryClientProvider>
311
313
  );