@oxyhq/services 13.3.0 → 14.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/lib/commonjs/index.js +11 -16
  2. package/lib/commonjs/index.js.map +1 -1
  3. package/lib/commonjs/ui/components/AccountMenu.js +103 -65
  4. package/lib/commonjs/ui/components/AccountMenu.js.map +1 -1
  5. package/lib/commonjs/ui/components/AccountSwitcher.js +84 -126
  6. package/lib/commonjs/ui/components/AccountSwitcher.js.map +1 -1
  7. package/lib/commonjs/ui/components/ProfileMenu.js +140 -153
  8. package/lib/commonjs/ui/components/ProfileMenu.js.map +1 -1
  9. package/lib/commonjs/ui/components/SignInModal.js +4 -4
  10. package/lib/commonjs/ui/components/SignInModal.js.map +1 -1
  11. package/lib/commonjs/ui/context/OxyContext.js +506 -466
  12. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  13. package/lib/commonjs/ui/context/hooks/useAuthOperations.js +59 -57
  14. package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
  15. package/lib/commonjs/ui/context/inSessionTokenRefresh.js +23 -42
  16. package/lib/commonjs/ui/context/inSessionTokenRefresh.js.map +1 -1
  17. package/lib/commonjs/ui/context/silentSessionRestore.js +12 -21
  18. package/lib/commonjs/ui/context/silentSessionRestore.js.map +1 -1
  19. package/lib/commonjs/ui/hooks/useAuth.js +4 -3
  20. package/lib/commonjs/ui/hooks/useAuth.js.map +1 -1
  21. package/lib/commonjs/ui/hooks/useOxyAuthSession.js +12 -7
  22. package/lib/commonjs/ui/hooks/useOxyAuthSession.js.map +1 -1
  23. package/lib/commonjs/ui/hooks/useSessionManagement.js +17 -65
  24. package/lib/commonjs/ui/hooks/useSessionManagement.js.map +1 -1
  25. package/lib/commonjs/ui/hooks/useSwitchableAccounts.js +288 -0
  26. package/lib/commonjs/ui/hooks/useSwitchableAccounts.js.map +1 -0
  27. package/lib/commonjs/ui/hooks/useWebSSO.js +12 -191
  28. package/lib/commonjs/ui/hooks/useWebSSO.js.map +1 -1
  29. package/lib/commonjs/ui/screens/OxyAuthScreen.js +2 -2
  30. package/lib/commonjs/ui/screens/OxyAuthScreen.js.map +1 -1
  31. package/lib/commonjs/ui/session/createSessionClient.js +28 -0
  32. package/lib/commonjs/ui/session/createSessionClient.js.map +1 -0
  33. package/lib/commonjs/ui/session/index.js +52 -0
  34. package/lib/commonjs/ui/session/index.js.map +1 -0
  35. package/lib/commonjs/ui/session/projectSessionState.js +86 -0
  36. package/lib/commonjs/ui/session/projectSessionState.js.map +1 -0
  37. package/lib/commonjs/ui/session/sessionClientHost.js +30 -0
  38. package/lib/commonjs/ui/session/sessionClientHost.js.map +1 -0
  39. package/lib/commonjs/ui/session/tokenTransport.js +77 -0
  40. package/lib/commonjs/ui/session/tokenTransport.js.map +1 -0
  41. package/lib/commonjs/ui/utils/activeAuthuser.js +35 -79
  42. package/lib/commonjs/ui/utils/activeAuthuser.js.map +1 -1
  43. package/lib/commonjs/ui/utils/sessionHelpers.js +1 -51
  44. package/lib/commonjs/ui/utils/sessionHelpers.js.map +1 -1
  45. package/lib/commonjs/utils/deviceFlowSignIn.js +70 -15
  46. package/lib/commonjs/utils/deviceFlowSignIn.js.map +1 -1
  47. package/lib/module/index.js +5 -4
  48. package/lib/module/index.js.map +1 -1
  49. package/lib/module/ui/components/AccountMenu.js +103 -65
  50. package/lib/module/ui/components/AccountMenu.js.map +1 -1
  51. package/lib/module/ui/components/AccountSwitcher.js +85 -127
  52. package/lib/module/ui/components/AccountSwitcher.js.map +1 -1
  53. package/lib/module/ui/components/ProfileMenu.js +143 -156
  54. package/lib/module/ui/components/ProfileMenu.js.map +1 -1
  55. package/lib/module/ui/components/SignInModal.js +4 -4
  56. package/lib/module/ui/components/SignInModal.js.map +1 -1
  57. package/lib/module/ui/context/OxyContext.js +510 -470
  58. package/lib/module/ui/context/OxyContext.js.map +1 -1
  59. package/lib/module/ui/context/hooks/useAuthOperations.js +61 -60
  60. package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
  61. package/lib/module/ui/context/inSessionTokenRefresh.js +24 -43
  62. package/lib/module/ui/context/inSessionTokenRefresh.js.map +1 -1
  63. package/lib/module/ui/context/silentSessionRestore.js +12 -20
  64. package/lib/module/ui/context/silentSessionRestore.js.map +1 -1
  65. package/lib/module/ui/hooks/useAuth.js +4 -3
  66. package/lib/module/ui/hooks/useAuth.js.map +1 -1
  67. package/lib/module/ui/hooks/useOxyAuthSession.js +12 -7
  68. package/lib/module/ui/hooks/useOxyAuthSession.js.map +1 -1
  69. package/lib/module/ui/hooks/useSessionManagement.js +18 -66
  70. package/lib/module/ui/hooks/useSessionManagement.js.map +1 -1
  71. package/lib/module/ui/hooks/useSwitchableAccounts.js +283 -0
  72. package/lib/module/ui/hooks/useSwitchableAccounts.js.map +1 -0
  73. package/lib/module/ui/hooks/useWebSSO.js +13 -192
  74. package/lib/module/ui/hooks/useWebSSO.js.map +1 -1
  75. package/lib/module/ui/screens/OxyAuthScreen.js +2 -2
  76. package/lib/module/ui/screens/OxyAuthScreen.js.map +1 -1
  77. package/lib/module/ui/session/createSessionClient.js +25 -0
  78. package/lib/module/ui/session/createSessionClient.js.map +1 -0
  79. package/lib/module/ui/session/index.js +17 -0
  80. package/lib/module/ui/session/index.js.map +1 -0
  81. package/lib/module/ui/session/projectSessionState.js +79 -0
  82. package/lib/module/ui/session/projectSessionState.js.map +1 -0
  83. package/lib/module/ui/session/sessionClientHost.js +26 -0
  84. package/lib/module/ui/session/sessionClientHost.js.map +1 -0
  85. package/lib/module/ui/session/tokenTransport.js +74 -0
  86. package/lib/module/ui/session/tokenTransport.js.map +1 -0
  87. package/lib/module/ui/utils/activeAuthuser.js +34 -76
  88. package/lib/module/ui/utils/activeAuthuser.js.map +1 -1
  89. package/lib/module/ui/utils/sessionHelpers.js +0 -49
  90. package/lib/module/ui/utils/sessionHelpers.js.map +1 -1
  91. package/lib/module/utils/deviceFlowSignIn.js +71 -16
  92. package/lib/module/utils/deviceFlowSignIn.js.map +1 -1
  93. package/lib/typescript/commonjs/index.d.ts +2 -3
  94. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  95. package/lib/typescript/commonjs/ui/components/AccountMenu.d.ts +4 -2
  96. package/lib/typescript/commonjs/ui/components/AccountMenu.d.ts.map +1 -1
  97. package/lib/typescript/commonjs/ui/components/AccountSwitcher.d.ts +7 -8
  98. package/lib/typescript/commonjs/ui/components/AccountSwitcher.d.ts.map +1 -1
  99. package/lib/typescript/commonjs/ui/components/ProfileMenu.d.ts +12 -9
  100. package/lib/typescript/commonjs/ui/components/ProfileMenu.d.ts.map +1 -1
  101. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +16 -13
  102. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
  103. package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts +27 -3
  104. package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  105. package/lib/typescript/commonjs/ui/context/inSessionTokenRefresh.d.ts +22 -17
  106. package/lib/typescript/commonjs/ui/context/inSessionTokenRefresh.d.ts.map +1 -1
  107. package/lib/typescript/commonjs/ui/context/silentSessionRestore.d.ts +12 -18
  108. package/lib/typescript/commonjs/ui/context/silentSessionRestore.d.ts.map +1 -1
  109. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +1 -0
  110. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  111. package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts +1 -1
  112. package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts.map +1 -1
  113. package/lib/typescript/commonjs/ui/hooks/useOxyAuthSession.d.ts +2 -2
  114. package/lib/typescript/commonjs/ui/hooks/useOxyAuthSession.d.ts.map +1 -1
  115. package/lib/typescript/commonjs/ui/hooks/useSessionManagement.d.ts.map +1 -1
  116. package/lib/typescript/commonjs/ui/hooks/useSwitchableAccounts.d.ts +162 -0
  117. package/lib/typescript/commonjs/ui/hooks/useSwitchableAccounts.d.ts.map +1 -0
  118. package/lib/typescript/commonjs/ui/hooks/useWebSSO.d.ts +13 -52
  119. package/lib/typescript/commonjs/ui/hooks/useWebSSO.d.ts.map +1 -1
  120. package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts +15 -0
  121. package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts.map +1 -0
  122. package/lib/typescript/commonjs/ui/session/index.d.ts +15 -0
  123. package/lib/typescript/commonjs/ui/session/index.d.ts.map +1 -0
  124. package/lib/typescript/commonjs/ui/session/projectSessionState.d.ts +45 -0
  125. package/lib/typescript/commonjs/ui/session/projectSessionState.d.ts.map +1 -0
  126. package/lib/typescript/commonjs/ui/session/sessionClientHost.d.ts +14 -0
  127. package/lib/typescript/commonjs/ui/session/sessionClientHost.d.ts.map +1 -0
  128. package/lib/typescript/commonjs/ui/session/tokenTransport.d.ts +24 -0
  129. package/lib/typescript/commonjs/ui/session/tokenTransport.d.ts.map +1 -0
  130. package/lib/typescript/commonjs/ui/utils/activeAuthuser.d.ts +21 -29
  131. package/lib/typescript/commonjs/ui/utils/activeAuthuser.d.ts.map +1 -1
  132. package/lib/typescript/commonjs/ui/utils/sessionHelpers.d.ts +0 -19
  133. package/lib/typescript/commonjs/ui/utils/sessionHelpers.d.ts.map +1 -1
  134. package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts +51 -19
  135. package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts.map +1 -1
  136. package/lib/typescript/module/index.d.ts +2 -3
  137. package/lib/typescript/module/index.d.ts.map +1 -1
  138. package/lib/typescript/module/ui/components/AccountMenu.d.ts +4 -2
  139. package/lib/typescript/module/ui/components/AccountMenu.d.ts.map +1 -1
  140. package/lib/typescript/module/ui/components/AccountSwitcher.d.ts +7 -8
  141. package/lib/typescript/module/ui/components/AccountSwitcher.d.ts.map +1 -1
  142. package/lib/typescript/module/ui/components/ProfileMenu.d.ts +12 -9
  143. package/lib/typescript/module/ui/components/ProfileMenu.d.ts.map +1 -1
  144. package/lib/typescript/module/ui/context/OxyContext.d.ts +16 -13
  145. package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
  146. package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts +27 -3
  147. package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  148. package/lib/typescript/module/ui/context/inSessionTokenRefresh.d.ts +22 -17
  149. package/lib/typescript/module/ui/context/inSessionTokenRefresh.d.ts.map +1 -1
  150. package/lib/typescript/module/ui/context/silentSessionRestore.d.ts +12 -18
  151. package/lib/typescript/module/ui/context/silentSessionRestore.d.ts.map +1 -1
  152. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +1 -0
  153. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  154. package/lib/typescript/module/ui/hooks/useAuth.d.ts +1 -1
  155. package/lib/typescript/module/ui/hooks/useAuth.d.ts.map +1 -1
  156. package/lib/typescript/module/ui/hooks/useOxyAuthSession.d.ts +2 -2
  157. package/lib/typescript/module/ui/hooks/useOxyAuthSession.d.ts.map +1 -1
  158. package/lib/typescript/module/ui/hooks/useSessionManagement.d.ts.map +1 -1
  159. package/lib/typescript/module/ui/hooks/useSwitchableAccounts.d.ts +162 -0
  160. package/lib/typescript/module/ui/hooks/useSwitchableAccounts.d.ts.map +1 -0
  161. package/lib/typescript/module/ui/hooks/useWebSSO.d.ts +13 -52
  162. package/lib/typescript/module/ui/hooks/useWebSSO.d.ts.map +1 -1
  163. package/lib/typescript/module/ui/session/createSessionClient.d.ts +15 -0
  164. package/lib/typescript/module/ui/session/createSessionClient.d.ts.map +1 -0
  165. package/lib/typescript/module/ui/session/index.d.ts +15 -0
  166. package/lib/typescript/module/ui/session/index.d.ts.map +1 -0
  167. package/lib/typescript/module/ui/session/projectSessionState.d.ts +45 -0
  168. package/lib/typescript/module/ui/session/projectSessionState.d.ts.map +1 -0
  169. package/lib/typescript/module/ui/session/sessionClientHost.d.ts +14 -0
  170. package/lib/typescript/module/ui/session/sessionClientHost.d.ts.map +1 -0
  171. package/lib/typescript/module/ui/session/tokenTransport.d.ts +24 -0
  172. package/lib/typescript/module/ui/session/tokenTransport.d.ts.map +1 -0
  173. package/lib/typescript/module/ui/utils/activeAuthuser.d.ts +21 -29
  174. package/lib/typescript/module/ui/utils/activeAuthuser.d.ts.map +1 -1
  175. package/lib/typescript/module/ui/utils/sessionHelpers.d.ts +0 -19
  176. package/lib/typescript/module/ui/utils/sessionHelpers.d.ts.map +1 -1
  177. package/lib/typescript/module/utils/deviceFlowSignIn.d.ts +51 -19
  178. package/lib/typescript/module/utils/deviceFlowSignIn.d.ts.map +1 -1
  179. package/package.json +4 -4
  180. package/src/index.ts +9 -8
  181. package/src/ui/components/AccountMenu.tsx +117 -70
  182. package/src/ui/components/AccountSwitcher.tsx +88 -134
  183. package/src/ui/components/ProfileMenu.tsx +158 -162
  184. package/src/ui/components/SignInModal.tsx +5 -5
  185. package/src/ui/context/OxyContext.tsx +560 -475
  186. package/src/ui/context/hooks/useAuthOperations.ts +72 -63
  187. package/src/ui/context/inSessionTokenRefresh.ts +23 -42
  188. package/src/ui/context/silentSessionRestore.ts +12 -26
  189. package/src/ui/hooks/useAuth.ts +4 -3
  190. package/src/ui/hooks/useOxyAuthSession.ts +13 -8
  191. package/src/ui/hooks/useSessionManagement.ts +16 -87
  192. package/src/ui/hooks/useSwitchableAccounts.ts +397 -0
  193. package/src/ui/hooks/useWebSSO.ts +13 -227
  194. package/src/ui/screens/OxyAuthScreen.tsx +2 -2
  195. package/src/ui/session/__tests__/createSessionClient.test.ts +41 -0
  196. package/src/ui/session/__tests__/projectSessionState.test.ts +114 -0
  197. package/src/ui/session/__tests__/sessionClientHost.test.ts +43 -0
  198. package/src/ui/session/__tests__/tokenTransport.test.ts +142 -0
  199. package/src/ui/session/createSessionClient.ts +21 -0
  200. package/src/ui/session/index.ts +19 -0
  201. package/src/ui/session/projectSessionState.ts +85 -0
  202. package/src/ui/session/sessionClientHost.ts +27 -0
  203. package/src/ui/session/tokenTransport.ts +76 -0
  204. package/src/ui/utils/activeAuthuser.ts +34 -76
  205. package/src/ui/utils/sessionHelpers.ts +0 -66
  206. package/src/utils/__tests__/deviceFlowSignIn.test.ts +243 -34
  207. package/src/utils/deviceFlowSignIn.ts +98 -21
  208. package/lib/commonjs/ui/components/accountMenuRows.js +0 -33
  209. package/lib/commonjs/ui/components/accountMenuRows.js.map +0 -1
  210. package/lib/commonjs/ui/hooks/useDeviceAccounts.js +0 -280
  211. package/lib/commonjs/ui/hooks/useDeviceAccounts.js.map +0 -1
  212. package/lib/commonjs/ui/hooks/useSessionSocket.js +0 -203
  213. package/lib/commonjs/ui/hooks/useSessionSocket.js.map +0 -1
  214. package/lib/commonjs/utils/silentGuardKey.js +0 -54
  215. package/lib/commonjs/utils/silentGuardKey.js.map +0 -1
  216. package/lib/module/ui/components/accountMenuRows.js +0 -29
  217. package/lib/module/ui/components/accountMenuRows.js.map +0 -1
  218. package/lib/module/ui/hooks/useDeviceAccounts.js +0 -276
  219. package/lib/module/ui/hooks/useDeviceAccounts.js.map +0 -1
  220. package/lib/module/ui/hooks/useSessionSocket.js +0 -199
  221. package/lib/module/ui/hooks/useSessionSocket.js.map +0 -1
  222. package/lib/module/utils/silentGuardKey.js +0 -49
  223. package/lib/module/utils/silentGuardKey.js.map +0 -1
  224. package/lib/typescript/commonjs/ui/components/accountMenuRows.d.ts +0 -34
  225. package/lib/typescript/commonjs/ui/components/accountMenuRows.d.ts.map +0 -1
  226. package/lib/typescript/commonjs/ui/hooks/useDeviceAccounts.d.ts +0 -133
  227. package/lib/typescript/commonjs/ui/hooks/useDeviceAccounts.d.ts.map +0 -1
  228. package/lib/typescript/commonjs/ui/hooks/useSessionSocket.d.ts +0 -14
  229. package/lib/typescript/commonjs/ui/hooks/useSessionSocket.d.ts.map +0 -1
  230. package/lib/typescript/commonjs/utils/silentGuardKey.d.ts +0 -31
  231. package/lib/typescript/commonjs/utils/silentGuardKey.d.ts.map +0 -1
  232. package/lib/typescript/module/ui/components/accountMenuRows.d.ts +0 -34
  233. package/lib/typescript/module/ui/components/accountMenuRows.d.ts.map +0 -1
  234. package/lib/typescript/module/ui/hooks/useDeviceAccounts.d.ts +0 -133
  235. package/lib/typescript/module/ui/hooks/useDeviceAccounts.d.ts.map +0 -1
  236. package/lib/typescript/module/ui/hooks/useSessionSocket.d.ts +0 -14
  237. package/lib/typescript/module/ui/hooks/useSessionSocket.d.ts.map +0 -1
  238. package/lib/typescript/module/utils/silentGuardKey.d.ts +0 -31
  239. package/lib/typescript/module/utils/silentGuardKey.d.ts.map +0 -1
  240. package/src/ui/components/accountMenuRows.ts +0 -48
  241. package/src/ui/hooks/useDeviceAccounts.ts +0 -344
  242. package/src/ui/hooks/useSessionSocket.ts +0 -233
  243. package/src/ui/utils/__tests__/activeAuthuser.test.ts +0 -75
  244. package/src/utils/__tests__/silentGuardKey.test.ts +0 -82
  245. package/src/utils/silentGuardKey.ts +0 -46
@@ -17,13 +17,11 @@ import { Ionicons } from '@expo/vector-icons';
17
17
  import { toast, useDialogControl, Dialog } from '@oxyhq/bloom';
18
18
  import { Divider } from '@oxyhq/bloom/divider';
19
19
  import { useTheme } from '@oxyhq/bloom/theme';
20
- import { getAccountDisplayName, isDev, logger as loggerUtil } from '@oxyhq/core';
21
- import type { AccountNode } from '@oxyhq/core';
20
+ import { isDev, logger as loggerUtil } from '@oxyhq/core';
22
21
  import Avatar from './Avatar';
23
22
  import { useOxy } from '../context/OxyContext';
24
23
  import { useI18n } from '../hooks/useI18n';
25
- import { buildAccountRows, type AccountRow } from './accountMenuRows';
26
- import { useDeviceAccounts } from '../hooks/useDeviceAccounts';
24
+ import { useSwitchableAccounts, type SwitchableAccount } from '../hooks/useSwitchableAccounts';
27
25
  import type { AccountMenuAnchor } from './AccountMenu';
28
26
 
29
27
  const isWeb = Platform.OS === 'web';
@@ -50,18 +48,18 @@ export interface AccountSwitcherActions {
50
48
  }
51
49
 
52
50
  interface TreeEntry {
53
- root: AccountNode;
54
- children: AccountNode[];
51
+ root: SwitchableAccount;
52
+ children: SwitchableAccount[];
55
53
  }
56
54
 
57
55
  /**
58
- * Group flat account nodes into a 2-level tree (org → direct children). A node
59
- * whose parent is not in the set is treated as a root; any deeper node not
56
+ * Group flat account rows into a 2-level tree (org → direct children). A row
57
+ * whose parent is not in the set is treated as a root; any deeper row not
60
58
  * captured under a root is promoted to its own root row so nothing is hidden.
61
59
  */
62
- function toTree(nodes: AccountNode[]): TreeEntry[] {
60
+ function toTree(nodes: SwitchableAccount[]): TreeEntry[] {
63
61
  const byId = new Map(nodes.map((node) => [node.accountId, node]));
64
- const childrenOf = new Map<string, AccountNode[]>();
62
+ const childrenOf = new Map<string, SwitchableAccount[]>();
65
63
  for (const node of nodes) {
66
64
  if (node.parentAccountId && byId.has(node.parentAccountId)) {
67
65
  const arr = childrenOf.get(node.parentAccountId) ?? [];
@@ -91,14 +89,14 @@ function toTree(nodes: AccountNode[]): TreeEntry[] {
91
89
  /**
92
90
  * The presentational, chrome-agnostic body of the unified account switcher.
93
91
  *
94
- * Two levels, relationship-aware:
95
- * - Section A accounts signed in on THIS device (independent sign-ins). Tap
96
- * to switch session; per-row sign-out; "Add another account".
97
- * - Section Bthe account GRAPH under the active sign-in: "Your accounts"
92
+ * Two levels, relationship-aware, both sourced from the single
93
+ * {@link useSwitchableAccounts} hook and switched through the SINGLE
94
+ * `switchToAccount(accountId)` path:
95
+ * - Section Aaccounts signed in on THIS device (`onDevice` rows). Tap to
96
+ * switch; per-row sign-out; "Add another account".
97
+ * - Section B — the account GRAPH not yet signed in here: "Your accounts"
98
98
  * (self / owned) and "Shared with you" (member). 2-level tree (org →
99
- * children), role badge, search, one-tap to act-as.
100
- *
101
- * Reads everything from `useOxy()` / `useDeviceAccounts()`.
99
+ * children), role badge, search, one-tap to switch.
102
100
  */
103
101
  export const AccountSwitcherView: React.FC<AccountSwitcherActions> = ({
104
102
  onClose,
@@ -109,22 +107,16 @@ export const AccountSwitcherView: React.FC<AccountSwitcherActions> = ({
109
107
  onBeforeSessionChange,
110
108
  }) => {
111
109
  const {
112
- activeSessionId,
113
- switchSession,
114
110
  switchToAccount,
115
111
  removeSession,
116
112
  logout,
117
113
  logoutAll,
118
- accounts,
119
- user,
120
- oxyServices,
121
114
  } = useOxy();
122
- const { t, locale } = useI18n();
115
+ const { t } = useI18n();
123
116
  const { colors } = useTheme();
124
117
 
125
- const { accounts: deviceAccounts } = useDeviceAccounts();
118
+ const { accounts } = useSwitchableAccounts();
126
119
 
127
- const [busySessionId, setBusySessionId] = useState<string | null>(null);
128
120
  const [switchingAccountId, setSwitchingAccountId] = useState<string | null>(null);
129
121
  const [removingSessionId, setRemovingSessionId] = useState<string | null>(null);
130
122
  const [signingOut, setSigningOut] = useState(false);
@@ -134,65 +126,58 @@ export const AccountSwitcherView: React.FC<AccountSwitcherActions> = ({
134
126
  const signOutDialog = useDialogControl();
135
127
  const signOutAllDialog = useDialogControl();
136
128
 
137
- const deviceRows = useMemo<AccountRow[]>(() => {
138
- const built = buildAccountRows({ accounts: deviceAccounts });
139
- const current = built.filter((row) => row.isActive);
140
- const others = built.filter((row) => !row.isActive);
129
+ // Section A accounts signed in on THIS device, current one first.
130
+ const deviceRows = useMemo<SwitchableAccount[]>(() => {
131
+ const onDevice = accounts.filter((account) => account.onDevice);
132
+ const current = onDevice.filter((account) => account.isCurrent);
133
+ const others = onDevice.filter((account) => !account.isCurrent);
141
134
  return [...current, ...others];
142
- }, [deviceAccounts]);
143
-
144
- const isSwitching = busySessionId !== null || switchingAccountId !== null;
145
- const actionDisabled = isSwitching || signingOut || signingOutAll;
146
-
147
- // Account ids that are already signed in as REAL device sessions. After a
148
- // `switchToAccount` the target becomes a device session (and the personal
149
- // account always is one), so it would otherwise appear BOTH as a device row
150
- // (Section A) and a graph row (Section B). We dedupe by hiding such accounts
151
- // from the graph — they show in Section A, where the active one is flagged
152
- // current.
153
- const deviceUserIds = useMemo<Set<string>>(
154
- () => new Set(deviceAccounts.map((account) => account.user.id).filter((id): id is string => Boolean(id))),
155
- [deviceAccounts],
135
+ }, [accounts]);
136
+
137
+ // Section B linked graph accounts NOT already signed in on this device.
138
+ // (An account switched into becomes a device session, so it moves to Section
139
+ // A automatically — the unified hook already deduped it there.)
140
+ const graphAccounts = useMemo<SwitchableAccount[]>(
141
+ () => accounts.filter((account) => !account.onDevice),
142
+ [accounts],
156
143
  );
157
144
 
158
- // --- Account graph (Section B) ---
159
- // The switchable graph minus accounts that are already device sessions
160
- // (deduped into Section A). This is the set Section B renders and searches.
161
- const graphAccounts = useMemo<AccountNode[]>(
162
- () => accounts.filter((node) => !deviceUserIds.has(node.accountId)),
163
- [accounts, deviceUserIds],
164
- );
145
+ const isSwitching = switchingAccountId !== null;
146
+ const actionDisabled = isSwitching || signingOut || signingOutAll;
165
147
 
166
- const filtered = useMemo<AccountNode[]>(() => {
148
+ const filtered = useMemo<SwitchableAccount[]>(() => {
167
149
  const q = query.trim().toLowerCase();
168
150
  if (!q) return graphAccounts;
169
- return graphAccounts.filter((node) => {
170
- const name = getAccountDisplayName(node.account, locale).toLowerCase();
171
- const username = (node.account?.username ?? '').toLowerCase();
151
+ return graphAccounts.filter((account) => {
152
+ const name = account.displayName.toLowerCase();
153
+ const username = (account.user?.username ?? '').toLowerCase();
172
154
  return name.includes(q) || username.includes(q);
173
155
  });
174
- }, [graphAccounts, query, locale]);
156
+ }, [graphAccounts, query]);
175
157
 
176
158
  const yourAccounts = useMemo(
177
- () => toTree(filtered.filter((node) => node.relationship !== 'member')),
159
+ () => toTree(filtered.filter((account) => account.relationship !== 'member')),
178
160
  [filtered],
179
161
  );
180
162
  const sharedAccounts = useMemo(
181
- () => toTree(filtered.filter((node) => node.relationship === 'member')),
163
+ () => toTree(filtered.filter((account) => account.relationship === 'member')),
182
164
  [filtered],
183
165
  );
184
166
 
185
- const handleSwitchDevice = useCallback(async (sessionId: string) => {
186
- if (busySessionId) return;
187
- // Tapping the already-active sign-in just closes it IS the current account.
188
- if (sessionId === activeSessionId) {
167
+ // The ONE uniform switch path. Device rows AND graph rows both switch by
168
+ // account id: `switchToAccount` reuses an existing device session (no mint)
169
+ // and mints a real session only on the FIRST entry into a graph account.
170
+ const handleSwitch = useCallback(async (account: SwitchableAccount) => {
171
+ if (switchingAccountId) return;
172
+ // Tapping the already-active account just closes — it IS the current one.
173
+ if (account.isCurrent) {
189
174
  onClose();
190
175
  return;
191
176
  }
192
- setBusySessionId(sessionId);
177
+ setSwitchingAccountId(account.accountId);
193
178
  try {
194
179
  await onBeforeSessionChange?.();
195
- await switchSession(sessionId);
180
+ await switchToAccount(account.accountId);
196
181
  toast.success(t('accountSwitcher.toasts.switchSuccess') || 'Switched account');
197
182
  onClose();
198
183
  } catch (error) {
@@ -201,12 +186,12 @@ export const AccountSwitcherView: React.FC<AccountSwitcherActions> = ({
201
186
  }
202
187
  toast.error(t('accountSwitcher.toasts.switchFailed') || 'Failed to switch account');
203
188
  } finally {
204
- setBusySessionId(null);
189
+ setSwitchingAccountId(null);
205
190
  }
206
- }, [activeSessionId, busySessionId, switchSession, t, onClose, onBeforeSessionChange]);
191
+ }, [switchingAccountId, switchToAccount, t, onClose, onBeforeSessionChange]);
207
192
 
208
193
  const handleRemoveDevice = useCallback(async (sessionId: string) => {
209
- if (sessionId === activeSessionId || removingSessionId) return;
194
+ if (removingSessionId) return;
210
195
  setRemovingSessionId(sessionId);
211
196
  try {
212
197
  await removeSession(sessionId);
@@ -217,7 +202,7 @@ export const AccountSwitcherView: React.FC<AccountSwitcherActions> = ({
217
202
  } finally {
218
203
  setRemovingSessionId(null);
219
204
  }
220
- }, [activeSessionId, removingSessionId, removeSession, t]);
205
+ }, [removingSessionId, removeSession, t]);
221
206
 
222
207
  const performSignOut = useCallback(async () => {
223
208
  if (signingOut) return;
@@ -251,57 +236,26 @@ export const AccountSwitcherView: React.FC<AccountSwitcherActions> = ({
251
236
  }
252
237
  }, [signingOutAll, logoutAll, t, onClose, onBeforeSessionChange]);
253
238
 
254
- const handleSelectAccount = useCallback(async (node: AccountNode) => {
255
- if (switchingAccountId) return;
256
- // Already the active account → just close (it would also be deduped from
257
- // the graph, but guard defensively).
258
- if (node.accountId === user?.id) {
259
- onClose();
260
- return;
261
- }
262
- setSwitchingAccountId(node.accountId);
263
- try {
264
- await onBeforeSessionChange?.();
265
- // Switching INTO a graph account is a REAL session switch — the whole
266
- // app becomes that account (no delegation header).
267
- await switchToAccount(node.accountId);
268
- toast.success(t('accountSwitcher.toasts.switchSuccess') || 'Switched account');
269
- onClose();
270
- } catch (error) {
271
- if (!isDev()) {
272
- loggerUtil.warn('Switch account failed', { component: 'AccountSwitcher' }, error as unknown);
273
- }
274
- toast.error(t('accountSwitcher.toasts.switchFailed') || 'Failed to switch account');
275
- } finally {
276
- setSwitchingAccountId(null);
277
- }
278
- }, [switchingAccountId, user?.id, switchToAccount, t, onClose, onBeforeSessionChange]);
279
-
280
- const renderAccountNode = useCallback((node: AccountNode, isChild: boolean) => {
281
- const displayName = getAccountDisplayName(node.account, locale);
282
- const username = node.account?.username ? `@${node.account.username}` : null;
283
- // Current account = the active session's user. After a real-session
284
- // switch `user` IS this account (such accounts are normally deduped out of
285
- // the graph, but the check stays correct/defensive if one lingers).
286
- const active = node.accountId === user?.id;
287
- const isNodeSwitching = switchingAccountId === node.accountId;
288
- const role = node.callerMembership?.role;
289
- const avatarUri = node.account?.avatar
290
- ? oxyServices.getFileDownloadUrl(node.account.avatar, 'thumb')
291
- : undefined;
292
- const permissions = node.callerMembership?.permissions ?? [];
293
- const canManage = node.relationship !== 'member'
239
+ const renderAccountNode = useCallback((account: SwitchableAccount, isChild: boolean) => {
240
+ const displayName = account.displayName;
241
+ const secondary = account.email;
242
+ const active = account.isCurrent;
243
+ const isNodeSwitching = switchingAccountId === account.accountId;
244
+ const role = account.callerMembership?.role;
245
+ const avatarUri = account.avatarUrl;
246
+ const permissions = account.callerMembership?.permissions ?? [];
247
+ const canManage = account.relationship !== 'member'
294
248
  || permissions.includes('account:update')
295
249
  || permissions.includes('members:read');
296
- const showSettings = node.relationship !== 'self' && !!onOpenAccountSettings && canManage;
250
+ const showSettings = account.relationship !== 'self' && !!onOpenAccountSettings && canManage;
297
251
 
298
252
  return (
299
253
  <TouchableOpacity
300
- key={`node-${node.accountId}`}
254
+ key={`node-${account.accountId}`}
301
255
  accessibilityRole="menuitem"
302
256
  accessibilityLabel={displayName}
303
257
  accessibilityState={{ selected: active }}
304
- onPress={() => handleSelectAccount(node)}
258
+ onPress={() => handleSwitch(account)}
305
259
  disabled={isSwitching}
306
260
  activeOpacity={0.6}
307
261
  style={[
@@ -325,9 +279,9 @@ export const AccountSwitcherView: React.FC<AccountSwitcherActions> = ({
325
279
  </View>
326
280
  ) : null}
327
281
  </View>
328
- {username ? (
282
+ {secondary ? (
329
283
  <Text style={[styles.accountEmail, { color: colors.textSecondary }]} numberOfLines={1}>
330
- {username}
284
+ {secondary}
331
285
  </Text>
332
286
  ) : null}
333
287
  </View>
@@ -335,7 +289,7 @@ export const AccountSwitcherView: React.FC<AccountSwitcherActions> = ({
335
289
  <TouchableOpacity
336
290
  accessibilityRole="button"
337
291
  accessibilityLabel={t('accounts.settings.title') || 'Account settings'}
338
- onPress={() => onOpenAccountSettings?.(node.accountId)}
292
+ onPress={() => onOpenAccountSettings?.(account.accountId)}
339
293
  hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
340
294
  style={styles.settingsButton}
341
295
  >
@@ -349,7 +303,7 @@ export const AccountSwitcherView: React.FC<AccountSwitcherActions> = ({
349
303
  ) : null}
350
304
  </TouchableOpacity>
351
305
  );
352
- }, [user?.id, switchingAccountId, isSwitching, colors, handleSelectAccount, locale, onOpenAccountSettings, oxyServices, t]);
306
+ }, [switchingAccountId, isSwitching, colors, handleSwitch, onOpenAccountSettings, t]);
353
307
 
354
308
  const renderTree = useCallback((entries: TreeEntry[]) => (
355
309
  entries.map((entry) => (
@@ -371,38 +325,39 @@ export const AccountSwitcherView: React.FC<AccountSwitcherActions> = ({
371
325
  <Text style={[styles.sectionLabel, { color: colors.textSecondary }]}>
372
326
  {t('accountSwitcher.sections.thisDevice') || 'On this device'}
373
327
  </Text>
374
- {deviceRows.map((row) => {
375
- const isBusy = busySessionId === row.sessionId;
376
- const isRemoving = removingSessionId === row.sessionId;
328
+ {deviceRows.map((account) => {
329
+ const sessionId = account.sessionId;
330
+ const isBusy = switchingAccountId === account.accountId;
331
+ const isRemoving = sessionId ? removingSessionId === sessionId : false;
377
332
  // The active device session IS the current account — there is no
378
- // separate acting-as concept; switching makes `user` that account.
379
- const isCurrentAccount = row.isActive;
333
+ // separate acting-as concept; switching makes it the whole app.
334
+ const isCurrentAccount = account.isCurrent;
380
335
  return (
381
336
  <TouchableOpacity
382
- key={`device-${row.sessionId}`}
337
+ key={`device-${account.accountId}`}
383
338
  accessibilityRole="menuitem"
384
- accessibilityLabel={row.displayName}
339
+ accessibilityLabel={account.displayName}
385
340
  accessibilityState={{ selected: isCurrentAccount }}
386
- onPress={() => handleSwitchDevice(row.sessionId)}
341
+ onPress={() => handleSwitch(account)}
387
342
  disabled={isCurrentAccount || isBusy || isSwitching}
388
343
  activeOpacity={0.6}
389
344
  style={[
390
345
  styles.accountRow,
391
346
  isCurrentAccount && { backgroundColor: colors.primarySubtle },
392
- isSwitching && !row.isActive && styles.rowDisabled,
347
+ isSwitching && !isCurrentAccount && styles.rowDisabled,
393
348
  ]}
394
349
  >
395
- <Avatar uri={row.avatarUri} name={row.displayName} size={row.isActive ? 40 : 32} />
350
+ <Avatar uri={account.avatarUrl} name={account.displayName} size={isCurrentAccount ? 40 : 32} />
396
351
  <View style={styles.accountInfo}>
397
352
  <Text
398
- style={[styles.accountName, { color: colors.text }, row.isActive && styles.accountNameActive]}
353
+ style={[styles.accountName, { color: colors.text }, isCurrentAccount && styles.accountNameActive]}
399
354
  numberOfLines={1}
400
355
  >
401
- {row.displayName}
356
+ {account.displayName}
402
357
  </Text>
403
- {row.secondary ? (
358
+ {account.email ? (
404
359
  <Text style={[styles.accountEmail, { color: colors.textSecondary }]} numberOfLines={1}>
405
- {row.secondary}
360
+ {account.email}
406
361
  </Text>
407
362
  ) : null}
408
363
  </View>
@@ -412,21 +367,21 @@ export const AccountSwitcherView: React.FC<AccountSwitcherActions> = ({
412
367
  <Ionicons name="checkmark" size={20} color={colors.primary} />
413
368
  ) : isRemoving ? (
414
369
  <ActivityIndicator color={colors.textSecondary} size="small" />
415
- ) : (
370
+ ) : sessionId ? (
416
371
  <TouchableOpacity
417
372
  accessibilityRole="button"
418
373
  accessibilityLabel={
419
- t('accountMenu.signOutAccount', { name: row.displayName })
420
- || `Sign out ${row.displayName}`
374
+ t('accountMenu.signOutAccount', { name: account.displayName })
375
+ || `Sign out ${account.displayName}`
421
376
  }
422
- onPress={() => handleRemoveDevice(row.sessionId)}
377
+ onPress={() => handleRemoveDevice(sessionId)}
423
378
  disabled={isSwitching || removingSessionId !== null}
424
379
  hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
425
380
  style={styles.settingsButton}
426
381
  >
427
382
  <Ionicons name="log-out-outline" size={18} color={colors.textSecondary} />
428
383
  </TouchableOpacity>
429
- )}
384
+ ) : null}
430
385
  </TouchableOpacity>
431
386
  );
432
387
  })}
@@ -594,7 +549,6 @@ export interface AccountSwitcherProps extends AccountSwitcherActions {
594
549
  /**
595
550
  * Unified account switcher presented as a popover (web) / bottom-sheet style
596
551
  * modal (native). The canonical entry point opened by {@link AccountMenuButton}.
597
- * Supersedes `AccountMenu` (which remains exported as the device-only switcher).
598
552
  */
599
553
  const AccountSwitcher: React.FC<AccountSwitcherProps> = ({ open, anchor, ...actions }) => {
600
554
  const { t } = useI18n();