@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,5 +1,5 @@
1
- import React, { useCallback, useEffect, useMemo, useState, type ErrorInfo } from 'react';
2
- import { StyleSheet, Text, View, useWindowDimensions } from 'react-native';
1
+ import React, { Suspense, useCallback, useEffect, useMemo, useState, type ErrorInfo } from 'react';
2
+ import { ActivityIndicator, StyleSheet, Text, View, useWindowDimensions } from 'react-native';
3
3
  import { useStore } from 'zustand';
4
4
  import { useTheme } from '@oxyhq/bloom/theme';
5
5
  import {
@@ -25,7 +25,7 @@ import {
25
25
  } from '../hooks/useSurfaceHeader';
26
26
  import type { BaseScreenProps } from '../types/navigation';
27
27
 
28
- /** Error boundary catching screen render failures (e.g. a lazy `require()` throw). */
28
+ /** Error boundary catching screen render failures, so one bad screen cannot blank the surface. */
29
29
  interface ScreenErrorBoundaryState {
30
30
  error: Error | null;
31
31
  }
@@ -69,12 +69,34 @@ class ScreenErrorBoundary extends React.Component<
69
69
  }
70
70
  }
71
71
 
72
+ /** The body fill shared by the two states that replace a screen: crashed, and not loaded yet. */
72
73
  const errorStyles = StyleSheet.create({
73
74
  container: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 24 },
74
75
  title: { fontSize: 16, fontWeight: '600', marginBottom: 8 },
75
76
  message: { fontSize: 13, textAlign: 'center' },
76
77
  });
77
78
 
79
+ /**
80
+ * Shown while a route's screen module is still loading. Screens are registered
81
+ * as `lazy(() => import(...))` (see `../navigation/routes`), so the very first
82
+ * presentation of a surface can land here for a frame; the surface chrome is
83
+ * already on screen by then, so this fills only the body.
84
+ *
85
+ * Deliberately RN's `ActivityIndicator` rather than `@oxyhq/bloom/loading`,
86
+ * matching `authChooser/requestSurfaces`: Bloom still declares
87
+ * `sideEffects: false`, so its `Loading` can still tree-shake to `undefined` in
88
+ * a rolldown-vite production bundle. Removing that risk from the first frame of
89
+ * every route is not worth a themed spinner.
90
+ */
91
+ function ScreenPending() {
92
+ const theme = useTheme();
93
+ return (
94
+ <View style={errorStyles.container}>
95
+ <ActivityIndicator size="large" color={theme.colors.primary} />
96
+ </View>
97
+ );
98
+ }
99
+
78
100
  export interface SurfaceScreenProps {
79
101
  /** This surface's route history (the NAV-WITHIN axis). */
80
102
  navStack: SurfaceNavStack;
@@ -251,7 +273,9 @@ function SurfaceScreen({
251
273
  return (
252
274
  <SurfaceHeaderContext.Provider value={headerContext}>
253
275
  <ScreenErrorBoundary screenName={top.route}>
254
- <ScreenComponent {...screenProps} />
276
+ <Suspense fallback={<ScreenPending />}>
277
+ <ScreenComponent {...screenProps} />
278
+ </Suspense>
255
279
  </ScreenErrorBoundary>
256
280
  </SurfaceHeaderContext.Provider>
257
281
  );
@@ -7,8 +7,11 @@
7
7
  * avatar (with a camera badge to change the photo), the greeting, the account
8
8
  * address, and a "Manage your Oxy account" pill.
9
9
  * - SWITCH ACCOUNT: a disclosure row whose corner animates from pill to card as
10
- * the other switchable accounts + "Add another account" + "Manage accounts on
11
- * this device" reveal beneath it.
10
+ * the device's people + "Add another account" + "Manage accounts on this
11
+ * device" reveal beneath it. The list is grouped BY PERSON (ADR 0002): an
12
+ * account sits under the human who can operate it, and the same organization
13
+ * reachable through two people is two rows under two names — the thing a list
14
+ * keyed by account id structurally cannot say.
12
15
  * - OXY STORAGE: cloud + used/total + a composition meter + Upgrade / Manage chips.
13
16
  * - MENU rows: Your data in Oxy · Oxy settings · Help & feedback · Sign out —
14
17
  * Bloom's grouped section, the same component every other SDK settings surface
@@ -41,13 +44,18 @@ import { PressableScale } from '@oxyhq/bloom/pressable-scale';
41
44
  import { CompositionBar, type CompositionCategory } from '@oxyhq/bloom/composition-bar';
42
45
  import { SettingsListGroup, SettingsListItem } from '@oxyhq/bloom/settings-list';
43
46
  import { Text } from '@oxyhq/bloom/typography';
44
- import type { AccountDialogSnapshot, SwitchableAccount } from '@oxyhq/core';
45
- import { getNormalizedUserHandle } from '@oxyhq/core';
47
+ import {
48
+ showsPrincipalHeaders,
49
+ type AccountDialogSnapshot,
50
+ type SwitcherContextRow,
51
+ type SwitcherPrincipalRow,
52
+ } from '@oxyhq/core';
46
53
  import AvatarCameraBadge from '../AvatarCameraBadge';
47
54
  import { HoverPressable } from './primitives';
48
55
  import { authChooserStyles as styles } from './styles';
49
56
  import {
50
57
  resolveAccentHex,
58
+ type AccountHeroModel,
51
59
  type AccountStorageModel,
52
60
  type AccountsMenuActions,
53
61
  type OxyAuthChooserHandlers,
@@ -84,6 +92,11 @@ const SWITCH_CARD_RADIUS_SETTLE = 0.2;
84
92
  const SWITCH_REVEAL_MS = 240;
85
93
  /** Diameter of an account row's avatar in the switch list (own markup). */
86
94
  const SWITCH_AVATAR_SIZE = 36;
95
+ /**
96
+ * The person's avatar above their block of accounts. Smaller than the account
97
+ * rows beneath it: the header labels the group, the rows are the targets.
98
+ */
99
+ const PRINCIPAL_AVATAR_SIZE = 24;
87
100
  /** Glyph inside a bordered affordance badge, sized to the avatar it stands in for. */
88
101
  const BADGE_GLYPH_SIZE = 19;
89
102
  /** The hero block's large current-account avatar. */
@@ -99,20 +112,6 @@ const FACEPILE_AVATAR_SIZE = 32;
99
112
  */
100
113
  const FACEPILE_OVERLAP = 4;
101
114
 
102
- /**
103
- * The hero's address line, shown under the "Hi, <name>!" greeting: the account's
104
- * canonical `@oxy.so` email when it has one, else its normalized `@handle`. Never
105
- * synthesizes a `username@oxy.so` address (the identity contract forbids it) — a
106
- * non-Oxy or missing email falls back to `getNormalizedUserHandle`.
107
- */
108
- function heroAddressLine(account: SwitchableAccount): string | null {
109
- if (account.email?.toLowerCase().endsWith('@oxy.so')) {
110
- return account.email;
111
- }
112
- const handle = getNormalizedUserHandle(account.user);
113
- return handle ? `@${handle}` : null;
114
- }
115
-
116
115
  /**
117
116
  * `bytes` → `"11.6 GB"`. Whole plan sizes read as whole numbers (`"17 GB"`, not
118
117
  * `"17.0 GB"`), and anything ≥ 100 GB drops the decimal entirely.
@@ -125,6 +124,10 @@ function formatStorageGb(bytes: number): string {
125
124
 
126
125
  interface AccountsMenuViewProps {
127
126
  snapshot: AccountDialogSnapshot;
127
+ /** The device's people, each with the accounts they may act as. */
128
+ principals: SwitcherPrincipalRow[];
129
+ /** The current account, rendered large. `null` before the session resolves. */
130
+ hero: AccountHeroModel | null;
128
131
  theme: Theme;
129
132
  t: Translate;
130
133
  handlers: OxyAuthChooserHandlers;
@@ -134,6 +137,8 @@ interface AccountsMenuViewProps {
134
137
 
135
138
  const AccountsMenuView: React.FC<AccountsMenuViewProps> = ({
136
139
  snapshot,
140
+ principals,
141
+ hero,
137
142
  theme,
138
143
  t,
139
144
  handlers,
@@ -188,7 +193,7 @@ const AccountsMenuView: React.FC<AccountsMenuViewProps> = ({
188
193
  listProgress.value = withTiming(next ? 1 : 0, { duration: SWITCH_REVEAL_MS });
189
194
  }, [expanded, listProgress]);
190
195
 
191
- if (snapshot.loading && snapshot.accounts.length === 0) {
196
+ if (snapshot.loading && principals.length === 0) {
192
197
  return (
193
198
  <View className="items-center justify-center gap-space-12 py-space-24">
194
199
  <MaterialCommunityIcons name="loading" size={24} color={theme.colors.textSecondary} />
@@ -199,24 +204,26 @@ const AccountsMenuView: React.FC<AccountsMenuViewProps> = ({
199
204
  );
200
205
  }
201
206
 
202
- const switchingDisabled = snapshot.switchingAccountId !== null;
203
- const current = snapshot.accounts.find((account) => account.isCurrent) ?? snapshot.accounts[0];
204
- const others = current
205
- ? snapshot.accounts.filter((account) => account.accountId !== current.accountId)
206
- : snapshot.accounts;
207
- const currentAccent = current
208
- ? resolveAccentHex(current.color, theme.colors.primary)
209
- : theme.colors.primary;
210
- const currentAddress = current ? heroAddressLine(current) : null;
211
- // The switch list repeats the CURRENT account (first, ringed) unlike the old
212
- // compact header it is not represented anywhere else once the hero moved out
213
- // of the card, and the list is where you switch back to it.
214
- const switchableAccounts = current ? [current, ...others] : others;
215
- const switchableAccountKey = switchableAccounts.map((account) => account.accountId).join(':');
216
- const facepile: AvatarGroupItem[] = switchableAccounts.map((account) => ({
217
- id: account.accountId,
218
- uri: account.avatarUrl ?? undefined,
219
- displayName: account.displayName,
207
+ // Activation and the two removals all mutate the same device, so any one of
208
+ // them in flight disables every row: a second press would race the first for
209
+ // which context the server ends up electing as active.
210
+ const busy =
211
+ snapshot.activatingContextId !== null ||
212
+ snapshot.removingContextId !== null ||
213
+ snapshot.removingPrincipalId !== null;
214
+ const currentAccent = hero?.accentHex ?? theme.colors.primary;
215
+ // Naming the person above each block earns its place only when the grouping
216
+ // carries information a second person, or somebody with more than one
217
+ // account. On a one-person, one-account device it would just repeat the hero.
218
+ const showPrincipalHeaders = showsPrincipalHeaders(principals);
219
+ const contextRows = principals.flatMap((principal) => principal.contexts);
220
+ // Remounts the measured wrapper when the rows change, so the collapse animates
221
+ // to the new natural height instead of the previous one.
222
+ const switchListKey = contextRows.map((context) => context.contextId).join(':');
223
+ const facepile: AvatarGroupItem[] = contextRows.map((context) => ({
224
+ id: context.contextId,
225
+ uri: context.avatarUrl ?? undefined,
226
+ displayName: context.displayName,
220
227
  }));
221
228
 
222
229
  // Used vs free is a two-part COMPOSITION (the parts always fill the bar), not
@@ -253,62 +260,47 @@ const AccountsMenuView: React.FC<AccountsMenuViewProps> = ({
253
260
  // strands one against the header row.
254
261
  const listItems = (
255
262
  <>
256
- {switchableAccounts.map((account, index) => {
257
- const accent = resolveAccentHex(account.color, theme.colors.primary);
258
- const isSwitching = snapshot.switchingAccountId === account.accountId;
259
- return (
260
- <Fragment key={account.accountId}>
261
- {index > 0 ? <View className="h-px bg-border opacity-30 ml-space-12" /> : null}
262
- <HoverPressable
263
- baseClassName={`flex-row items-center gap-space-12 px-space-12 py-[10px]${
264
- switchingDisabled ? ' opacity-60' : ''
265
- }`}
266
- hoverClassName="bg-fill-secondary"
267
- onPress={() => handlers.onSwitch(account.accountId)}
268
- disabled={switchingDisabled}
269
- accessibilityRole="button"
270
- accessibilityLabel={account.displayName}
271
- >
272
- <View>
273
- <Avatar
274
- source={account.avatarUrl ?? undefined}
275
- variant="thumb"
276
- name={account.displayName}
277
- size={SWITCH_AVATAR_SIZE}
278
- />
279
- {/* The accent ring is an OVERLAY: it costs the row no width, so
280
- every avatar in the list starts on the same content line. */}
281
- {account.isCurrent ? (
282
- <View
283
- style={[styles.currentAvatarRing, { borderColor: accent }]}
284
- pointerEvents="none"
285
- />
286
- ) : null}
287
- </View>
288
- <View className="flex-1 min-w-0">
289
- <Text className="text-body font-semibold text-text" numberOfLines={1}>
290
- {account.displayName}
291
- </Text>
292
- {account.email ? (
293
- <Text className="text-caption text-text-secondary" numberOfLines={1}>
294
- {account.email}
295
- </Text>
296
- ) : null}
297
- </View>
298
- {isSwitching ? (
299
- <MaterialCommunityIcons name="loading" size={20} color={accent} />
263
+ {principals.map((principal, principalIndex) => (
264
+ <Fragment key={principal.principalId}>
265
+ {principalIndex > 0 ? <View className="h-px bg-border opacity-30" /> : null}
266
+ {showPrincipalHeaders ? (
267
+ <PrincipalHeaderRow
268
+ principal={principal}
269
+ removing={snapshot.removingPrincipalId === principal.principalId}
270
+ disabled={busy}
271
+ onSignOut={() => handlers.onRemovePrincipal(principal.principalId)}
272
+ theme={theme}
273
+ t={t}
274
+ />
275
+ ) : null}
276
+ {principal.contexts.map((context, contextIndex) => (
277
+ <Fragment key={context.contextId}>
278
+ {contextIndex > 0 || showPrincipalHeaders ? (
279
+ <View className="h-px bg-border opacity-30 ml-space-12" />
300
280
  ) : null}
301
- </HoverPressable>
302
- </Fragment>
303
- );
304
- })}
281
+ <ContextRow
282
+ context={context}
283
+ principalName={principal.displayName}
284
+ accent={resolveAccentHex(context.color, theme.colors.primary)}
285
+ activating={snapshot.activatingContextId === context.contextId}
286
+ removing={snapshot.removingContextId === context.contextId}
287
+ disabled={busy}
288
+ onActivate={() => handlers.onActivate(context.contextId)}
289
+ onRemove={() => handlers.onRemoveContext(context.contextId)}
290
+ theme={theme}
291
+ t={t}
292
+ />
293
+ </Fragment>
294
+ ))}
295
+ </Fragment>
296
+ ))}
305
297
 
306
298
  <View className="h-px bg-border opacity-30 ml-space-12" />
307
299
  <AccountAffordanceRow
308
300
  icon="plus"
309
301
  label={t('accountMenu.addAnother')}
310
302
  onPress={handlers.onAdd}
311
- disabled={switchingDisabled}
303
+ disabled={busy}
312
304
  theme={theme}
313
305
  />
314
306
  <View className="h-px bg-border opacity-30 ml-space-12" />
@@ -316,7 +308,7 @@ const AccountsMenuView: React.FC<AccountsMenuViewProps> = ({
316
308
  icon="account-multiple-outline"
317
309
  label={t('accountSwitcher.manageOnDevice')}
318
310
  onPress={handlers.onManage}
319
- disabled={switchingDisabled}
311
+ disabled={busy}
320
312
  theme={theme}
321
313
  />
322
314
  </>
@@ -332,7 +324,7 @@ const AccountsMenuView: React.FC<AccountsMenuViewProps> = ({
332
324
  large pressable avatar (with a camera badge to change the photo), then
333
325
  the greeting, then the account address under it, then the manage action. */}
334
326
  <View style={styles.hero}>
335
- {current ? (
327
+ {hero ? (
336
328
  <>
337
329
  <PressableScale
338
330
  onPress={handlers.onEditAvatar}
@@ -341,8 +333,8 @@ const AccountsMenuView: React.FC<AccountsMenuViewProps> = ({
341
333
  style={styles.heroAvatarPressable}
342
334
  >
343
335
  <Avatar
344
- source={current.avatarUrl ?? undefined}
345
- name={current.displayName}
336
+ source={hero.avatarUrl ?? undefined}
337
+ name={hero.displayName}
346
338
  size={HERO_AVATAR_SIZE}
347
339
  />
348
340
  <View
@@ -353,14 +345,14 @@ const AccountsMenuView: React.FC<AccountsMenuViewProps> = ({
353
345
  </PressableScale>
354
346
  <View style={styles.heroNameBlock}>
355
347
  <Text style={[styles.heroGreeting, { color: theme.colors.text }]} numberOfLines={2}>
356
- {t('accountMenu.greeting', { name: current.displayName })}
348
+ {t('accountMenu.greeting', { name: hero.displayName })}
357
349
  </Text>
358
- {currentAddress ? (
350
+ {hero.addressLine ? (
359
351
  <Text
360
352
  style={[styles.heroAddress, { color: theme.colors.textSecondary }]}
361
353
  numberOfLines={1}
362
354
  >
363
- {currentAddress}
355
+ {hero.addressLine}
364
356
  </Text>
365
357
  ) : null}
366
358
  </View>
@@ -387,11 +379,11 @@ const AccountsMenuView: React.FC<AccountsMenuViewProps> = ({
387
379
  <View className="bg-fill">
388
380
  <HoverPressable
389
381
  baseClassName={`flex-row items-center gap-space-12 px-space-12 py-[10px] min-h-[44px]${
390
- switchingDisabled ? ' opacity-60' : ''
382
+ busy ? ' opacity-60' : ''
391
383
  }`}
392
384
  hoverClassName="bg-fill-secondary"
393
385
  onPress={toggleExpanded}
394
- disabled={switchingDisabled}
386
+ disabled={busy}
395
387
  accessibilityRole="button"
396
388
  // The ARIA prop, not `accessibilityState`: react-native-web 0.21
397
389
  // forwards only this one, and RN maps it to
@@ -432,7 +424,7 @@ const AccountsMenuView: React.FC<AccountsMenuViewProps> = ({
432
424
  pointerEvents={expanded ? 'auto' : 'none'}
433
425
  >
434
426
  <View
435
- key={switchableAccountKey}
427
+ key={switchListKey}
436
428
  style={styles.collapseMeasure}
437
429
  onLayout={(event) => setListContentHeight(event.nativeEvent.layout.height)}
438
430
  >
@@ -520,6 +512,164 @@ const AccountsMenuView: React.FC<AccountsMenuViewProps> = ({
520
512
  );
521
513
  };
522
514
 
515
+ /**
516
+ * The person a block of accounts hangs off, with the one action that removes
517
+ * THEM rather than one of their accounts.
518
+ *
519
+ * The distinction is the reason this row exists: dropping Alice's route to an
520
+ * organization is not the same as dropping Alice, and on a shared device the
521
+ * second must not touch Nate's route to the same organization. Two different
522
+ * server calls, two different rows, never one control doing both.
523
+ */
524
+ const PrincipalHeaderRow: React.FC<{
525
+ principal: SwitcherPrincipalRow;
526
+ removing: boolean;
527
+ disabled: boolean;
528
+ onSignOut: () => void;
529
+ theme: Theme;
530
+ t: Translate;
531
+ }> = ({ principal, removing, disabled, onSignOut, theme, t }) => {
532
+ const signOutLabel = t('accountSwitcher.principal.signOut', { name: principal.displayName });
533
+ return (
534
+ <View className="flex-row items-center gap-space-12 px-space-12 pt-space-12 pb-space-8">
535
+ <Avatar
536
+ source={principal.avatarUrl ?? undefined}
537
+ variant="thumb"
538
+ name={principal.displayName}
539
+ size={PRINCIPAL_AVATAR_SIZE}
540
+ />
541
+ <View className="flex-1 min-w-0">
542
+ <Text className="text-caption font-semibold text-text" numberOfLines={1}>
543
+ {principal.displayName}
544
+ </Text>
545
+ {principal.handle ? (
546
+ <Text className="text-caption text-text-tertiary" numberOfLines={1}>
547
+ {`@${principal.handle}`}
548
+ </Text>
549
+ ) : null}
550
+ </View>
551
+ <HoverPressable
552
+ baseClassName={`rounded-radius-max px-space-8 py-[4px]${disabled ? ' opacity-60' : ''}`}
553
+ hoverClassName="bg-fill-secondary"
554
+ onPress={onSignOut}
555
+ disabled={disabled}
556
+ accessibilityRole="button"
557
+ accessibilityLabel={signOutLabel}
558
+ >
559
+ {removing ? (
560
+ <MaterialCommunityIcons name="loading" size={16} color={theme.colors.textSecondary} />
561
+ ) : (
562
+ <Text className="text-caption text-text-secondary">{t('accountMenu.signOut')}</Text>
563
+ )}
564
+ </HoverPressable>
565
+ </View>
566
+ );
567
+ };
568
+
569
+ /**
570
+ * One `principal acting as account` row.
571
+ *
572
+ * Pressing it activates the PAIR — a `contextId`, not an account id, because on
573
+ * this device the account below may also be reachable through somebody else.
574
+ * A row the server marks unavailable is rendered rather than hidden, so a
575
+ * membership going away is something the person can see instead of a row that
576
+ * silently stopped existing; it is disabled, because activating it is a request
577
+ * the server answers with a 403 and then heals the row away.
578
+ */
579
+ const ContextRow: React.FC<{
580
+ context: SwitcherContextRow;
581
+ principalName: string;
582
+ /**
583
+ * This ROW's own accent, resolved from `context.color` by the caller. Never
584
+ * the signed-in account's: two people on one device would then be drawn in
585
+ * whichever of them happens to be active.
586
+ */
587
+ accent: string;
588
+ activating: boolean;
589
+ removing: boolean;
590
+ disabled: boolean;
591
+ onActivate: () => void;
592
+ onRemove: () => void;
593
+ theme: Theme;
594
+ t: Translate;
595
+ }> = ({
596
+ context,
597
+ principalName,
598
+ accent,
599
+ activating,
600
+ removing,
601
+ disabled,
602
+ onActivate,
603
+ onRemove,
604
+ theme,
605
+ t,
606
+ }) => {
607
+ const unavailable = !context.canActivate;
608
+ const rowDisabled = disabled || unavailable;
609
+ const removeLabel = t('accountSwitcher.context.remove', {
610
+ account: context.displayName,
611
+ person: principalName,
612
+ });
613
+ return (
614
+ <View className="flex-row items-center">
615
+ <HoverPressable
616
+ baseClassName={`flex-1 min-w-0 flex-row items-center gap-space-12 pl-space-12 py-[10px]${
617
+ rowDisabled ? ' opacity-60' : ''
618
+ }`}
619
+ hoverClassName="bg-fill-secondary"
620
+ onPress={onActivate}
621
+ disabled={rowDisabled}
622
+ accessibilityRole="button"
623
+ accessibilityState={{ selected: context.isActive, disabled: rowDisabled }}
624
+ accessibilityLabel={context.displayName}
625
+ >
626
+ <View>
627
+ <Avatar
628
+ source={context.avatarUrl ?? undefined}
629
+ variant="thumb"
630
+ name={context.displayName}
631
+ size={SWITCH_AVATAR_SIZE}
632
+ />
633
+ {/* The accent ring is an OVERLAY: it costs the row no width, so every
634
+ avatar in the list starts on the same content line. */}
635
+ {context.isActive ? (
636
+ <View style={[styles.currentAvatarRing, { borderColor: accent }]} pointerEvents="none" />
637
+ ) : null}
638
+ </View>
639
+ <View className="flex-1 min-w-0">
640
+ <Text className="text-body font-semibold text-text" numberOfLines={1}>
641
+ {context.displayName}
642
+ </Text>
643
+ <Text className="text-caption text-text-secondary" numberOfLines={1}>
644
+ {unavailable
645
+ ? t('accountSwitcher.context.unavailable')
646
+ : context.handle
647
+ ? `@${context.handle}`
648
+ : ''}
649
+ </Text>
650
+ </View>
651
+ {activating ? (
652
+ <MaterialCommunityIcons name="loading" size={20} color={accent} />
653
+ ) : null}
654
+ </HoverPressable>
655
+ <HoverPressable
656
+ baseClassName={`px-space-12 py-[10px]${disabled ? ' opacity-60' : ''}`}
657
+ hoverClassName="bg-fill-secondary"
658
+ onPress={onRemove}
659
+ disabled={disabled}
660
+ accessibilityRole="button"
661
+ accessibilityLabel={removeLabel}
662
+ >
663
+ <MaterialCommunityIcons
664
+ name={removing ? 'loading' : 'close'}
665
+ size={18}
666
+ color={theme.colors.textTertiary}
667
+ />
668
+ </HoverPressable>
669
+ </View>
670
+ );
671
+ };
672
+
523
673
  /**
524
674
  * An account-list AFFORDANCE row ("Add another account", "Manage accounts on
525
675
  * this device") — a bordered circle sized to the avatar it stands in for, so its
@@ -19,7 +19,11 @@
19
19
  import type React from 'react';
20
20
  import { View } from 'react-native';
21
21
  import { Button } from '@oxyhq/bloom/button';
22
- import type { AccountDialogSnapshot } from '@oxyhq/core';
22
+ import {
23
+ showsPrincipalHeaders,
24
+ type AccountDialogSnapshot,
25
+ type SwitcherPrincipalRow,
26
+ } from '@oxyhq/core';
23
27
  import TroubleDisclosure from './TroubleDisclosure';
24
28
  import { AccountRow, Dividerish, SubtleLink } from './primitives';
25
29
  import { authChooserStyles as styles } from './styles';
@@ -33,6 +37,8 @@ import type {
33
37
 
34
38
  interface SignInEntryViewProps {
35
39
  snapshot: AccountDialogSnapshot;
40
+ /** The device's people, each with the accounts they may act as. */
41
+ principals: SwitcherPrincipalRow[];
36
42
  theme: Theme;
37
43
  t: Translate;
38
44
  handlers: OxyAuthChooserHandlers;
@@ -43,6 +49,7 @@ interface SignInEntryViewProps {
43
49
 
44
50
  const SignInEntryView: React.FC<SignInEntryViewProps> = ({
45
51
  snapshot,
52
+ principals,
46
53
  theme,
47
54
  t,
48
55
  handlers,
@@ -76,18 +83,34 @@ const SignInEntryView: React.FC<SignInEntryViewProps> = ({
76
83
  },
77
84
  ];
78
85
 
86
+ // Whose route this is only needs naming when the list holds more than one
87
+ // person, or somebody with more than one account — the same rule the account
88
+ // menu's group headers follow, so the two surfaces agree on when the actor is
89
+ // worth stating.
90
+ const namesTheOperator = showsPrincipalHeaders(principals);
91
+ const rows = principals.flatMap((principal) =>
92
+ principal.contexts.map((context) => ({
93
+ context,
94
+ operatedBy:
95
+ namesTheOperator && context.isDelegated
96
+ ? t('accountSwitcher.context.operatedBy', { name: principal.displayName })
97
+ : null,
98
+ })),
99
+ );
100
+
79
101
  return (
80
102
  <View style={styles.signInBlock}>
81
- {snapshot.accounts.length > 0 ? (
103
+ {rows.length > 0 ? (
82
104
  <View style={styles.rows}>
83
- {snapshot.accounts.map((account) => (
105
+ {rows.map(({ context, operatedBy }) => (
84
106
  <AccountRow
85
- key={account.accountId}
86
- account={account}
107
+ key={context.contextId}
108
+ context={context}
109
+ operatedBy={operatedBy}
87
110
  theme={theme}
88
- switching={snapshot.switchingAccountId === account.accountId}
89
- disabled={snapshot.switchingAccountId !== null}
90
- onPress={() => handlers.onSwitch(account.accountId)}
111
+ activating={snapshot.activatingContextId === context.contextId}
112
+ disabled={snapshot.activatingContextId !== null}
113
+ onPress={() => handlers.onActivate(context.contextId)}
91
114
  />
92
115
  ))}
93
116
  <Dividerish theme={theme} label={t('signin.or')} />