@oxyhq/services 16.0.0 → 17.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 (251) hide show
  1. package/lib/commonjs/index.js +21 -51
  2. package/lib/commonjs/index.js.map +1 -1
  3. package/lib/commonjs/ui/client.js +0 -14
  4. package/lib/commonjs/ui/client.js.map +1 -1
  5. package/lib/commonjs/ui/components/OxyAccountDialog.js +764 -0
  6. package/lib/commonjs/ui/components/OxyAccountDialog.js.map +1 -0
  7. package/lib/commonjs/ui/components/OxyProvider.js +11 -6
  8. package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
  9. package/lib/commonjs/ui/components/OxySignInButton.js +10 -21
  10. package/lib/commonjs/ui/components/OxySignInButton.js.map +1 -1
  11. package/lib/commonjs/ui/components/ProfileButton.js +25 -112
  12. package/lib/commonjs/ui/components/ProfileButton.js.map +1 -1
  13. package/lib/commonjs/ui/components/RequireOxyAuth.js +299 -0
  14. package/lib/commonjs/ui/components/RequireOxyAuth.js.map +1 -0
  15. package/lib/commonjs/ui/context/OxyContext.js +106 -2
  16. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  17. package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js +1 -1
  18. package/lib/commonjs/ui/hooks/useAuth.js +19 -30
  19. package/lib/commonjs/ui/hooks/useAuth.js.map +1 -1
  20. package/lib/commonjs/ui/hooks/useSwitchableAccounts.js +37 -267
  21. package/lib/commonjs/ui/hooks/useSwitchableAccounts.js.map +1 -1
  22. package/lib/commonjs/ui/index.js +5 -40
  23. package/lib/commonjs/ui/index.js.map +1 -1
  24. package/lib/commonjs/ui/navigation/accountDialogManager.js +91 -0
  25. package/lib/commonjs/ui/navigation/accountDialogManager.js.map +1 -0
  26. package/lib/commonjs/ui/navigation/routes.js +0 -2
  27. package/lib/commonjs/ui/navigation/routes.js.map +1 -1
  28. package/lib/commonjs/ui/screens/ManageAccountScreen.js +4 -3
  29. package/lib/commonjs/ui/screens/ManageAccountScreen.js.map +1 -1
  30. package/lib/commonjs/ui/session/createSessionClient.js +10 -2
  31. package/lib/commonjs/ui/session/createSessionClient.js.map +1 -1
  32. package/lib/module/index.js +18 -15
  33. package/lib/module/index.js.map +1 -1
  34. package/lib/module/ui/client.js +1 -2
  35. package/lib/module/ui/client.js.map +1 -1
  36. package/lib/module/ui/components/OxyAccountDialog.js +761 -0
  37. package/lib/module/ui/components/OxyAccountDialog.js.map +1 -0
  38. package/lib/module/ui/components/OxyProvider.js +11 -6
  39. package/lib/module/ui/components/OxyProvider.js.map +1 -1
  40. package/lib/module/ui/components/OxySignInButton.js +10 -21
  41. package/lib/module/ui/components/OxySignInButton.js.map +1 -1
  42. package/lib/module/ui/components/ProfileButton.js +26 -112
  43. package/lib/module/ui/components/ProfileButton.js.map +1 -1
  44. package/lib/module/ui/components/RequireOxyAuth.js +294 -0
  45. package/lib/module/ui/components/RequireOxyAuth.js.map +1 -0
  46. package/lib/module/ui/context/OxyContext.js +107 -3
  47. package/lib/module/ui/context/OxyContext.js.map +1 -1
  48. package/lib/module/ui/hooks/mutations/useAccountMutations.js +1 -1
  49. package/lib/module/ui/hooks/useAuth.js +19 -30
  50. package/lib/module/ui/hooks/useAuth.js.map +1 -1
  51. package/lib/module/ui/hooks/useSwitchableAccounts.js +38 -267
  52. package/lib/module/ui/hooks/useSwitchableAccounts.js.map +1 -1
  53. package/lib/module/ui/index.js +1 -5
  54. package/lib/module/ui/index.js.map +1 -1
  55. package/lib/module/ui/navigation/accountDialogManager.js +81 -0
  56. package/lib/module/ui/navigation/accountDialogManager.js.map +1 -0
  57. package/lib/module/ui/navigation/routes.js +0 -2
  58. package/lib/module/ui/navigation/routes.js.map +1 -1
  59. package/lib/module/ui/screens/ManageAccountScreen.js +4 -3
  60. package/lib/module/ui/screens/ManageAccountScreen.js.map +1 -1
  61. package/lib/module/ui/session/createSessionClient.js +10 -2
  62. package/lib/module/ui/session/createSessionClient.js.map +1 -1
  63. package/lib/typescript/commonjs/index.d.ts +4 -11
  64. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/ui/client.d.ts +0 -4
  66. package/lib/typescript/commonjs/ui/client.d.ts.map +1 -1
  67. package/lib/typescript/commonjs/ui/components/OxyAccountDialog.d.ts +35 -0
  68. package/lib/typescript/commonjs/ui/components/OxyAccountDialog.d.ts.map +1 -0
  69. package/lib/typescript/commonjs/ui/components/OxyProvider.d.ts +2 -2
  70. package/lib/typescript/commonjs/ui/components/OxyProvider.d.ts.map +1 -1
  71. package/lib/typescript/commonjs/ui/components/OxySignInButton.d.ts.map +1 -1
  72. package/lib/typescript/commonjs/ui/components/ProfileButton.d.ts +11 -23
  73. package/lib/typescript/commonjs/ui/components/ProfileButton.d.ts.map +1 -1
  74. package/lib/typescript/commonjs/ui/components/RequireOxyAuth.d.ts +55 -0
  75. package/lib/typescript/commonjs/ui/components/RequireOxyAuth.d.ts.map +1 -0
  76. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +13 -1
  77. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
  78. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  79. package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts.map +1 -1
  80. package/lib/typescript/commonjs/ui/hooks/useSwitchableAccounts.d.ts +13 -152
  81. package/lib/typescript/commonjs/ui/hooks/useSwitchableAccounts.d.ts.map +1 -1
  82. package/lib/typescript/commonjs/ui/index.d.ts +2 -5
  83. package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
  84. package/lib/typescript/commonjs/ui/navigation/accountDialogManager.d.ts +49 -0
  85. package/lib/typescript/commonjs/ui/navigation/accountDialogManager.d.ts.map +1 -0
  86. package/lib/typescript/commonjs/ui/navigation/routes.d.ts +1 -1
  87. package/lib/typescript/commonjs/ui/navigation/routes.d.ts.map +1 -1
  88. package/lib/typescript/commonjs/ui/screens/ManageAccountScreen.d.ts +1 -1
  89. package/lib/typescript/commonjs/ui/screens/ManageAccountScreen.d.ts.map +1 -1
  90. package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts +9 -2
  91. package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts.map +1 -1
  92. package/lib/typescript/commonjs/ui/types/navigation.d.ts +9 -0
  93. package/lib/typescript/commonjs/ui/types/navigation.d.ts.map +1 -1
  94. package/lib/typescript/module/index.d.ts +4 -11
  95. package/lib/typescript/module/index.d.ts.map +1 -1
  96. package/lib/typescript/module/ui/client.d.ts +0 -4
  97. package/lib/typescript/module/ui/client.d.ts.map +1 -1
  98. package/lib/typescript/module/ui/components/OxyAccountDialog.d.ts +35 -0
  99. package/lib/typescript/module/ui/components/OxyAccountDialog.d.ts.map +1 -0
  100. package/lib/typescript/module/ui/components/OxyProvider.d.ts +2 -2
  101. package/lib/typescript/module/ui/components/OxyProvider.d.ts.map +1 -1
  102. package/lib/typescript/module/ui/components/OxySignInButton.d.ts.map +1 -1
  103. package/lib/typescript/module/ui/components/ProfileButton.d.ts +11 -23
  104. package/lib/typescript/module/ui/components/ProfileButton.d.ts.map +1 -1
  105. package/lib/typescript/module/ui/components/RequireOxyAuth.d.ts +55 -0
  106. package/lib/typescript/module/ui/components/RequireOxyAuth.d.ts.map +1 -0
  107. package/lib/typescript/module/ui/context/OxyContext.d.ts +13 -1
  108. package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
  109. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  110. package/lib/typescript/module/ui/hooks/useAuth.d.ts.map +1 -1
  111. package/lib/typescript/module/ui/hooks/useSwitchableAccounts.d.ts +13 -152
  112. package/lib/typescript/module/ui/hooks/useSwitchableAccounts.d.ts.map +1 -1
  113. package/lib/typescript/module/ui/index.d.ts +2 -5
  114. package/lib/typescript/module/ui/index.d.ts.map +1 -1
  115. package/lib/typescript/module/ui/navigation/accountDialogManager.d.ts +49 -0
  116. package/lib/typescript/module/ui/navigation/accountDialogManager.d.ts.map +1 -0
  117. package/lib/typescript/module/ui/navigation/routes.d.ts +1 -1
  118. package/lib/typescript/module/ui/navigation/routes.d.ts.map +1 -1
  119. package/lib/typescript/module/ui/screens/ManageAccountScreen.d.ts +1 -1
  120. package/lib/typescript/module/ui/screens/ManageAccountScreen.d.ts.map +1 -1
  121. package/lib/typescript/module/ui/session/createSessionClient.d.ts +9 -2
  122. package/lib/typescript/module/ui/session/createSessionClient.d.ts.map +1 -1
  123. package/lib/typescript/module/ui/types/navigation.d.ts +9 -0
  124. package/lib/typescript/module/ui/types/navigation.d.ts.map +1 -1
  125. package/package.json +2 -2
  126. package/src/index.ts +21 -26
  127. package/src/ui/client.ts +0 -4
  128. package/src/ui/components/OxyAccountDialog.tsx +760 -0
  129. package/src/ui/components/OxyProvider.tsx +12 -6
  130. package/src/ui/components/OxySignInButton.tsx +10 -21
  131. package/src/ui/components/ProfileButton.tsx +23 -126
  132. package/src/ui/components/RequireOxyAuth.tsx +266 -0
  133. package/src/ui/context/OxyContext.tsx +146 -7
  134. package/src/ui/hooks/mutations/useAccountMutations.ts +1 -1
  135. package/src/ui/hooks/useAuth.ts +19 -31
  136. package/src/ui/hooks/useSwitchableAccounts.ts +42 -376
  137. package/src/ui/index.ts +2 -5
  138. package/src/ui/navigation/accountDialogManager.ts +84 -0
  139. package/src/ui/navigation/routes.ts +0 -4
  140. package/src/ui/screens/ManageAccountScreen.tsx +3 -2
  141. package/src/ui/session/createSessionClient.ts +9 -1
  142. package/src/ui/types/navigation.ts +9 -0
  143. package/lib/commonjs/ui/components/AccountMenu.js +0 -598
  144. package/lib/commonjs/ui/components/AccountMenu.js.map +0 -1
  145. package/lib/commonjs/ui/components/AccountMenuButton.js +0 -135
  146. package/lib/commonjs/ui/components/AccountMenuButton.js.map +0 -1
  147. package/lib/commonjs/ui/components/AccountSwitcher.js +0 -780
  148. package/lib/commonjs/ui/components/AccountSwitcher.js.map +0 -1
  149. package/lib/commonjs/ui/components/AnotherDeviceQR.js +0 -116
  150. package/lib/commonjs/ui/components/AnotherDeviceQR.js.map +0 -1
  151. package/lib/commonjs/ui/components/ProfileMenu.js +0 -518
  152. package/lib/commonjs/ui/components/ProfileMenu.js.map +0 -1
  153. package/lib/commonjs/ui/components/SignInAccountChooser.js +0 -189
  154. package/lib/commonjs/ui/components/SignInAccountChooser.js.map +0 -1
  155. package/lib/commonjs/ui/components/SignInModal.js +0 -600
  156. package/lib/commonjs/ui/components/SignInModal.js.map +0 -1
  157. package/lib/commonjs/ui/hooks/useOxyAuthSession.js +0 -501
  158. package/lib/commonjs/ui/hooks/useOxyAuthSession.js.map +0 -1
  159. package/lib/commonjs/ui/hooks/usePasswordSignIn.js +0 -170
  160. package/lib/commonjs/ui/hooks/usePasswordSignIn.js.map +0 -1
  161. package/lib/commonjs/ui/screens/AccountSwitcherScreen.js +0 -51
  162. package/lib/commonjs/ui/screens/AccountSwitcherScreen.js.map +0 -1
  163. package/lib/commonjs/ui/screens/OxyAuthScreen.js +0 -312
  164. package/lib/commonjs/ui/screens/OxyAuthScreen.js.map +0 -1
  165. package/lib/commonjs/utils/deviceFlowSignIn.js +0 -109
  166. package/lib/commonjs/utils/deviceFlowSignIn.js.map +0 -1
  167. package/lib/module/ui/components/AccountMenu.js +0 -593
  168. package/lib/module/ui/components/AccountMenu.js.map +0 -1
  169. package/lib/module/ui/components/AccountMenuButton.js +0 -130
  170. package/lib/module/ui/components/AccountMenuButton.js.map +0 -1
  171. package/lib/module/ui/components/AccountSwitcher.js +0 -774
  172. package/lib/module/ui/components/AccountSwitcher.js.map +0 -1
  173. package/lib/module/ui/components/AnotherDeviceQR.js +0 -110
  174. package/lib/module/ui/components/AnotherDeviceQR.js.map +0 -1
  175. package/lib/module/ui/components/ProfileMenu.js +0 -514
  176. package/lib/module/ui/components/ProfileMenu.js.map +0 -1
  177. package/lib/module/ui/components/SignInAccountChooser.js +0 -183
  178. package/lib/module/ui/components/SignInAccountChooser.js.map +0 -1
  179. package/lib/module/ui/components/SignInModal.js +0 -591
  180. package/lib/module/ui/components/SignInModal.js.map +0 -1
  181. package/lib/module/ui/hooks/useOxyAuthSession.js +0 -495
  182. package/lib/module/ui/hooks/useOxyAuthSession.js.map +0 -1
  183. package/lib/module/ui/hooks/usePasswordSignIn.js +0 -165
  184. package/lib/module/ui/hooks/usePasswordSignIn.js.map +0 -1
  185. package/lib/module/ui/screens/AccountSwitcherScreen.js +0 -48
  186. package/lib/module/ui/screens/AccountSwitcherScreen.js.map +0 -1
  187. package/lib/module/ui/screens/OxyAuthScreen.js +0 -306
  188. package/lib/module/ui/screens/OxyAuthScreen.js.map +0 -1
  189. package/lib/module/utils/deviceFlowSignIn.js +0 -106
  190. package/lib/module/utils/deviceFlowSignIn.js.map +0 -1
  191. package/lib/typescript/commonjs/ui/components/AccountMenu.d.ts +0 -47
  192. package/lib/typescript/commonjs/ui/components/AccountMenu.d.ts.map +0 -1
  193. package/lib/typescript/commonjs/ui/components/AccountMenuButton.d.ts +0 -26
  194. package/lib/typescript/commonjs/ui/components/AccountMenuButton.d.ts.map +0 -1
  195. package/lib/typescript/commonjs/ui/components/AccountSwitcher.d.ts +0 -46
  196. package/lib/typescript/commonjs/ui/components/AccountSwitcher.d.ts.map +0 -1
  197. package/lib/typescript/commonjs/ui/components/AnotherDeviceQR.d.ts +0 -34
  198. package/lib/typescript/commonjs/ui/components/AnotherDeviceQR.d.ts.map +0 -1
  199. package/lib/typescript/commonjs/ui/components/ProfileMenu.d.ts +0 -49
  200. package/lib/typescript/commonjs/ui/components/ProfileMenu.d.ts.map +0 -1
  201. package/lib/typescript/commonjs/ui/components/SignInAccountChooser.d.ts +0 -29
  202. package/lib/typescript/commonjs/ui/components/SignInAccountChooser.d.ts.map +0 -1
  203. package/lib/typescript/commonjs/ui/components/SignInModal.d.ts +0 -28
  204. package/lib/typescript/commonjs/ui/components/SignInModal.d.ts.map +0 -1
  205. package/lib/typescript/commonjs/ui/hooks/useOxyAuthSession.d.ts +0 -105
  206. package/lib/typescript/commonjs/ui/hooks/useOxyAuthSession.d.ts.map +0 -1
  207. package/lib/typescript/commonjs/ui/hooks/usePasswordSignIn.d.ts +0 -31
  208. package/lib/typescript/commonjs/ui/hooks/usePasswordSignIn.d.ts.map +0 -1
  209. package/lib/typescript/commonjs/ui/screens/AccountSwitcherScreen.d.ts +0 -11
  210. package/lib/typescript/commonjs/ui/screens/AccountSwitcherScreen.d.ts.map +0 -1
  211. package/lib/typescript/commonjs/ui/screens/OxyAuthScreen.d.ts +0 -22
  212. package/lib/typescript/commonjs/ui/screens/OxyAuthScreen.d.ts.map +0 -1
  213. package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts +0 -97
  214. package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts.map +0 -1
  215. package/lib/typescript/module/ui/components/AccountMenu.d.ts +0 -47
  216. package/lib/typescript/module/ui/components/AccountMenu.d.ts.map +0 -1
  217. package/lib/typescript/module/ui/components/AccountMenuButton.d.ts +0 -26
  218. package/lib/typescript/module/ui/components/AccountMenuButton.d.ts.map +0 -1
  219. package/lib/typescript/module/ui/components/AccountSwitcher.d.ts +0 -46
  220. package/lib/typescript/module/ui/components/AccountSwitcher.d.ts.map +0 -1
  221. package/lib/typescript/module/ui/components/AnotherDeviceQR.d.ts +0 -34
  222. package/lib/typescript/module/ui/components/AnotherDeviceQR.d.ts.map +0 -1
  223. package/lib/typescript/module/ui/components/ProfileMenu.d.ts +0 -49
  224. package/lib/typescript/module/ui/components/ProfileMenu.d.ts.map +0 -1
  225. package/lib/typescript/module/ui/components/SignInAccountChooser.d.ts +0 -29
  226. package/lib/typescript/module/ui/components/SignInAccountChooser.d.ts.map +0 -1
  227. package/lib/typescript/module/ui/components/SignInModal.d.ts +0 -28
  228. package/lib/typescript/module/ui/components/SignInModal.d.ts.map +0 -1
  229. package/lib/typescript/module/ui/hooks/useOxyAuthSession.d.ts +0 -105
  230. package/lib/typescript/module/ui/hooks/useOxyAuthSession.d.ts.map +0 -1
  231. package/lib/typescript/module/ui/hooks/usePasswordSignIn.d.ts +0 -31
  232. package/lib/typescript/module/ui/hooks/usePasswordSignIn.d.ts.map +0 -1
  233. package/lib/typescript/module/ui/screens/AccountSwitcherScreen.d.ts +0 -11
  234. package/lib/typescript/module/ui/screens/AccountSwitcherScreen.d.ts.map +0 -1
  235. package/lib/typescript/module/ui/screens/OxyAuthScreen.d.ts +0 -22
  236. package/lib/typescript/module/ui/screens/OxyAuthScreen.d.ts.map +0 -1
  237. package/lib/typescript/module/utils/deviceFlowSignIn.d.ts +0 -97
  238. package/lib/typescript/module/utils/deviceFlowSignIn.d.ts.map +0 -1
  239. package/src/ui/components/AccountMenu.tsx +0 -645
  240. package/src/ui/components/AccountMenuButton.tsx +0 -126
  241. package/src/ui/components/AccountSwitcher.tsx +0 -751
  242. package/src/ui/components/AnotherDeviceQR.tsx +0 -119
  243. package/src/ui/components/ProfileMenu.tsx +0 -564
  244. package/src/ui/components/SignInAccountChooser.tsx +0 -162
  245. package/src/ui/components/SignInModal.tsx +0 -530
  246. package/src/ui/hooks/useOxyAuthSession.ts +0 -635
  247. package/src/ui/hooks/usePasswordSignIn.ts +0 -207
  248. package/src/ui/screens/AccountSwitcherScreen.tsx +0 -43
  249. package/src/ui/screens/OxyAuthScreen.tsx +0 -324
  250. package/src/utils/__tests__/deviceFlowSignIn.test.ts +0 -231
  251. package/src/utils/deviceFlowSignIn.ts +0 -152
@@ -1,645 +0,0 @@
1
- import type React from 'react';
2
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
- import {
4
- View,
5
- Text,
6
- TouchableOpacity,
7
- StyleSheet,
8
- ScrollView,
9
- Modal,
10
- Pressable,
11
- Platform,
12
- ActivityIndicator,
13
- type ViewStyle,
14
- } from 'react-native';
15
- import { Ionicons } from '@expo/vector-icons';
16
- import { toast, useDialogControl, Dialog } from '@oxyhq/bloom';
17
- import { Divider } from '@oxyhq/bloom/divider';
18
- import { useTheme } from '@oxyhq/bloom/theme';
19
- import Avatar from './Avatar';
20
- import { useOxy } from '../context/OxyContext';
21
- import { useI18n } from '../hooks/useI18n';
22
- import { isDev, logger as loggerUtil } from '@oxyhq/core';
23
- import { useSwitchableAccounts, type SwitchableAccount } from '../hooks/useSwitchableAccounts';
24
-
25
- /**
26
- * Web-only anchor for the popover panel. Each field anchors the panel against
27
- * one viewport edge, so the popover can be placed against ANY corner: a
28
- * top-right avatar chip opens downward/right-aligned (`{ top, right }`), while a
29
- * bottom-left account button opens upward/left-aligned (`{ bottom, left }`).
30
- *
31
- * Callers MUST supply at most one vertical edge (`top` XOR `bottom`) and at most
32
- * one horizontal edge (`left` XOR `right`). The panel has a fixed width and
33
- * `maxHeight`, so a single vertical + single horizontal edge fully positions it.
34
- * Supplying both opposite edges (e.g. `top` AND `bottom`) would stretch the
35
- * panel on RN-Web and is unsupported.
36
- */
37
- export interface AccountMenuAnchor {
38
- /** Distance from the viewport top, when anchoring the panel's TOP edge. */
39
- top?: number;
40
- /** Distance from the viewport bottom, when anchoring the panel's BOTTOM edge (opens upward). */
41
- bottom?: number;
42
- /** Distance from the viewport left, when anchoring the panel's LEFT edge. */
43
- left?: number;
44
- /** Distance from the viewport right, when anchoring the panel's RIGHT edge. */
45
- right?: number;
46
- }
47
-
48
- export interface AccountMenuProps {
49
- open: boolean;
50
- onClose: () => void;
51
- onNavigateManage: () => void;
52
- onAddAccount: () => void;
53
- /** Optional anchor (web only). Native ignores this and uses bottom-sheet style. */
54
- anchor?: AccountMenuAnchor | null;
55
- /** Called before the active identity changes so apps can clear tenant-scoped state. */
56
- onBeforeSessionChange?: () => void | Promise<void>;
57
- }
58
-
59
- const isWeb = Platform.OS === 'web';
60
-
61
- /** Fixed popover width on web. Callers that compute an anchor (e.g. inbox's
62
- * MailboxDrawer) key their gutter math off this — keep them consistent. */
63
- const PANEL_WIDTH = 360;
64
-
65
- /**
66
- * Unified, canonical account switcher for the Oxy ecosystem. Gmail-style: the
67
- * accounts list sits at the top (current account first, with a checkmark), then
68
- * "Add another account", "Manage account", and the sign-out actions. Lists EVERY
69
- * switchable account (device sign-ins AND linked graph accounts) and routes every
70
- * switch through the single `switchToAccount(accountId)` path.
71
- *
72
- * Reads everything it needs from `useOxy()` / `useSwitchableAccounts()` — never
73
- * receives a session via props. Renders as a popover anchored to the trigger on
74
- * web, and as a full-width bottom-sheet style modal on native.
75
- */
76
- const AccountMenu: React.FC<AccountMenuProps> = ({
77
- open,
78
- onClose,
79
- onNavigateManage,
80
- onAddAccount,
81
- anchor,
82
- onBeforeSessionChange,
83
- }) => {
84
- const {
85
- switchToAccount,
86
- logout,
87
- logoutAll,
88
- removeSession,
89
- } = useOxy();
90
- const { t } = useI18n();
91
- const bloomTheme = useTheme();
92
- const colors = bloomTheme.colors;
93
-
94
- // Source EVERY switchable account — device sign-ins AND linked graph
95
- // accounts (owned orgs + shared-with-you) — from the single
96
- // `useSwitchableAccounts` hook, each hydrated with real
97
- // name/email/avatar/color. It also synthesises a live-user row when the
98
- // device set has not synced yet, so the signed-in user is always represented
99
- // (no "Not signed in" false negative).
100
- const { accounts } = useSwitchableAccounts();
101
-
102
- const [switchingAccountId, setSwitchingAccountId] = useState<string | null>(null);
103
- const [removingSessionId, setRemovingSessionId] = useState<string | null>(null);
104
- const [signingOut, setSigningOut] = useState(false);
105
- const [signingOutAll, setSigningOutAll] = useState(false);
106
-
107
- const signOutDialog = useDialogControl();
108
- const signOutAllDialog = useDialogControl();
109
-
110
- const containerRef = useRef<View | null>(null);
111
-
112
- // Current account first, then the remaining device sign-ins, then the linked
113
- // graph accounts — matching the Gmail-style chooser the inbox design ports.
114
- const rows = useMemo<SwitchableAccount[]>(() => {
115
- const device = accounts.filter((account) => account.onDevice);
116
- const graph = accounts.filter((account) => !account.onDevice);
117
- const current = device.filter((account) => account.isCurrent);
118
- const others = device.filter((account) => !account.isCurrent);
119
- return [...current, ...others, ...graph];
120
- }, [accounts]);
121
-
122
- // "Sign out of all accounts" only applies to real device sessions.
123
- const deviceCount = useMemo(
124
- () => accounts.filter((account) => account.onDevice).length,
125
- [accounts],
126
- );
127
-
128
- // Account ids currently listed — used to indent a graph-only row whose parent
129
- // account is also shown.
130
- const listedAccountIds = useMemo(
131
- () => new Set(rows.map((row) => row.accountId)),
132
- [rows],
133
- );
134
-
135
- const isSwitching = switchingAccountId !== null;
136
-
137
- // The ONE uniform switch path: every row (device sign-in or linked graph
138
- // account) routes through `useOxy().switchToAccount(accountId)`. It reuses
139
- // the device session when the account is already signed in here, and mints a
140
- // real session only on the first switch into a graph account — a single
141
- // source of truth for every switch.
142
- const handleSwitch = useCallback(async (account: SwitchableAccount) => {
143
- if (account.isCurrent || switchingAccountId) {
144
- return;
145
- }
146
- setSwitchingAccountId(account.accountId);
147
- try {
148
- await onBeforeSessionChange?.();
149
- await switchToAccount(account.accountId);
150
- toast.success(t('accountSwitcher.toasts.switchSuccess') || 'Switched account');
151
- onClose();
152
- } catch (error) {
153
- if (!isDev()) {
154
- loggerUtil.warn('Switch account failed', { component: 'AccountMenu' }, error as unknown);
155
- }
156
- toast.error(t('accountSwitcher.toasts.switchFailed') || 'Failed to switch account');
157
- } finally {
158
- setSwitchingAccountId(null);
159
- }
160
- }, [switchingAccountId, switchToAccount, t, onClose, onBeforeSessionChange]);
161
-
162
- // Sign out a SPECIFIC inactive account from its per-row icon. `removeSession`
163
- // is the SDK's canonical per-session sign-out: it targets the given session
164
- // id (cookie-cleared logout via `authuser` slot on web, bearer logout
165
- // otherwise) and removes ONLY that account without switching/clearing the
166
- // active session. The menu stays open so the user can keep managing accounts.
167
- const handleRemove = useCallback(async (sessionId: string) => {
168
- if (removingSessionId) {
169
- return;
170
- }
171
- setRemovingSessionId(sessionId);
172
- try {
173
- await removeSession(sessionId);
174
- toast.success(t('common.actions.signedOut') || 'Signed out');
175
- } catch (error) {
176
- loggerUtil.warn('Remove account failed', { component: 'AccountMenu' }, error as unknown);
177
- toast.error(t('common.errors.signOutFailed') || 'Failed to sign out');
178
- } finally {
179
- setRemovingSessionId(null);
180
- }
181
- }, [removingSessionId, removeSession, t]);
182
-
183
- const performSignOut = useCallback(async () => {
184
- if (signingOut) {
185
- return;
186
- }
187
- setSigningOut(true);
188
- try {
189
- await onBeforeSessionChange?.();
190
- await logout();
191
- toast.success(t('common.actions.signedOut') || 'Signed out');
192
- onClose();
193
- } catch (error) {
194
- loggerUtil.warn('Sign out failed', { component: 'AccountMenu' }, error as unknown);
195
- toast.error(t('common.errors.signOutFailed') || 'Failed to sign out');
196
- } finally {
197
- setSigningOut(false);
198
- }
199
- }, [signingOut, logout, t, onClose, onBeforeSessionChange]);
200
-
201
- const performSignOutAll = useCallback(async () => {
202
- if (signingOutAll) {
203
- return;
204
- }
205
- setSigningOutAll(true);
206
- try {
207
- await onBeforeSessionChange?.();
208
- await logoutAll();
209
- toast.success(t('accountSwitcher.toasts.signOutAllSuccess') || 'Signed out of all accounts');
210
- onClose();
211
- } catch (error) {
212
- loggerUtil.warn('Sign out all failed', { component: 'AccountMenu' }, error as unknown);
213
- toast.error(t('common.errors.signOutAllFailed') || 'Failed to sign out of all accounts');
214
- } finally {
215
- setSigningOutAll(false);
216
- }
217
- }, [signingOutAll, logoutAll, t, onClose, onBeforeSessionChange]);
218
-
219
- // Escape-to-close + focus management (web only).
220
- useEffect(() => {
221
- if (!open || !isWeb || typeof document === 'undefined') {
222
- return undefined;
223
- }
224
- const onKey = (event: KeyboardEvent) => {
225
- if (event.key === 'Escape') {
226
- event.stopPropagation();
227
- onClose();
228
- }
229
- };
230
- document.addEventListener('keydown', onKey, true);
231
- return () => document.removeEventListener('keydown', onKey, true);
232
- }, [open, onClose]);
233
-
234
- if (!open) {
235
- return null;
236
- }
237
-
238
- const overlayStyles: ViewStyle = isWeb
239
- ? styles.webOverlay
240
- : styles.nativeOverlay;
241
-
242
- // Apply ONLY the edges the anchor supplies (so the panel never sets
243
- // conflicting opposite edges). When no anchor is provided, fall back to the
244
- // historical top-right placement used by `AccountMenuButton`.
245
- const anchorStyle: ViewStyle = anchor
246
- ? {
247
- ...(anchor.top !== undefined ? { top: anchor.top } : null),
248
- ...(anchor.bottom !== undefined ? { bottom: anchor.bottom } : null),
249
- ...(anchor.left !== undefined ? { left: anchor.left } : null),
250
- ...(anchor.right !== undefined ? { right: anchor.right } : null),
251
- }
252
- : { top: 64, right: 16 };
253
-
254
- const panelStyles: ViewStyle[] = isWeb
255
- ? [
256
- styles.panelBase,
257
- styles.panelWeb,
258
- anchorStyle,
259
- { backgroundColor: colors.background, borderColor: colors.border },
260
- ]
261
- : [
262
- styles.panelBase,
263
- styles.panelNative,
264
- { backgroundColor: colors.background },
265
- ];
266
-
267
- const actionDisabled = isSwitching || signingOut || signingOutAll;
268
-
269
- const content = (
270
- <Pressable
271
- ref={containerRef}
272
- // Swallow taps inside the panel so they never reach the overlay's
273
- // outside-tap-to-close handler. (On web the panel is a direct,
274
- // absolutely-positioned child of the overlay so the anchor resolves
275
- // against the viewport.)
276
- onPress={() => undefined}
277
- style={panelStyles}
278
- accessibilityRole="menu"
279
- accessibilityLabel={t('accountMenu.label') || 'Account menu'}
280
- >
281
- <ScrollView
282
- style={styles.scroll}
283
- contentContainerStyle={styles.scrollContent}
284
- showsVerticalScrollIndicator={false}
285
- >
286
- {/* 1) Accounts list — current first (checkmark), then the other
287
- device sign-ins, then linked graph accounts (role badge,
288
- indented under their parent). Every row switches the same way. */}
289
- {rows.map((row) => {
290
- const sessionId = row.sessionId;
291
- const isBusy = switchingAccountId === row.accountId;
292
- const isRemoving = sessionId ? removingSessionId === sessionId : false;
293
- const isChild = Boolean(
294
- row.parentAccountId && listedAccountIds.has(row.parentAccountId),
295
- );
296
- const role = row.callerMembership?.role;
297
- return (
298
- <TouchableOpacity
299
- key={`account-${row.accountId}`}
300
- accessibilityRole="menuitem"
301
- accessibilityLabel={row.displayName}
302
- accessibilityState={{ selected: row.isCurrent }}
303
- onPress={() => handleSwitch(row)}
304
- disabled={row.isCurrent || isBusy || isSwitching}
305
- activeOpacity={0.6}
306
- style={[
307
- styles.accountRow,
308
- isChild && styles.childRow,
309
- row.isCurrent && { backgroundColor: colors.primarySubtle },
310
- isSwitching && !row.isCurrent && styles.rowDisabled,
311
- ]}
312
- >
313
- <Avatar
314
- uri={row.avatarUrl}
315
- name={row.displayName}
316
- size={row.isCurrent ? 40 : isChild ? 28 : 32}
317
- />
318
- <View style={styles.accountInfo}>
319
- <View style={styles.nameRow}>
320
- <Text
321
- style={[
322
- styles.accountName,
323
- { color: colors.text },
324
- row.isCurrent && styles.accountNameActive,
325
- ]}
326
- numberOfLines={1}
327
- >
328
- {row.displayName}
329
- </Text>
330
- {role ? (
331
- <View style={[styles.roleBadge, { backgroundColor: colors.card }]}>
332
- <Text style={[styles.roleBadgeText, { color: colors.textSecondary }]}>
333
- {t(`accounts.roles.${role}.label`) || role}
334
- </Text>
335
- </View>
336
- ) : null}
337
- </View>
338
- {row.email ? (
339
- <Text
340
- style={[styles.accountEmail, { color: colors.textSecondary }]}
341
- numberOfLines={1}
342
- >
343
- {row.email}
344
- </Text>
345
- ) : null}
346
- </View>
347
- {isBusy ? (
348
- <ActivityIndicator color={colors.primary} size="small" />
349
- ) : row.isCurrent ? (
350
- <Ionicons name="checkmark" size={20} color={colors.primary} />
351
- ) : isRemoving ? (
352
- <ActivityIndicator color={colors.textSecondary} size="small" />
353
- ) : sessionId ? (
354
- <TouchableOpacity
355
- accessibilityRole="button"
356
- accessibilityLabel={
357
- t('accountMenu.signOutAccount', { name: row.displayName })
358
- || `Sign out ${row.displayName}`
359
- }
360
- onPress={() => handleRemove(sessionId)}
361
- disabled={isSwitching || removingSessionId !== null}
362
- activeOpacity={0.6}
363
- hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
364
- style={styles.rowSignOutButton}
365
- >
366
- <Ionicons
367
- name="log-out-outline"
368
- size={18}
369
- color={colors.textSecondary}
370
- />
371
- </TouchableOpacity>
372
- ) : null}
373
- </TouchableOpacity>
374
- );
375
- })}
376
-
377
- {/* 2) Switching indicator. */}
378
- {isSwitching ? (
379
- <View style={styles.switchingRow}>
380
- <ActivityIndicator color={colors.textSecondary} size="small" />
381
- <Text style={[styles.switchingText, { color: colors.textSecondary }]}>
382
- {t('accountMenu.switching') || 'Switching account…'}
383
- </Text>
384
- </View>
385
- ) : null}
386
-
387
- <Divider color={colors.border} spacing={4} />
388
-
389
- {/* 3) Add another account. */}
390
- <TouchableOpacity
391
- accessibilityRole="menuitem"
392
- accessibilityLabel={t('accountMenu.addAnother') || 'Add another account'}
393
- onPress={() => {
394
- onClose();
395
- onAddAccount();
396
- }}
397
- disabled={actionDisabled}
398
- activeOpacity={0.6}
399
- style={[styles.actionRow, actionDisabled && styles.rowDisabled]}
400
- >
401
- <Ionicons name="person-add-outline" size={20} color={colors.icon} />
402
- <Text style={[styles.actionText, { color: colors.text }]}>
403
- {t('accountMenu.addAnother') || 'Add another account'}
404
- </Text>
405
- </TouchableOpacity>
406
-
407
- <Divider color={colors.border} spacing={4} />
408
-
409
- {/* 4) Manage account / Settings. */}
410
- <TouchableOpacity
411
- accessibilityRole="menuitem"
412
- accessibilityLabel={t('accountMenu.manage') || 'Manage your Oxy Account'}
413
- onPress={() => {
414
- onClose();
415
- onNavigateManage();
416
- }}
417
- disabled={actionDisabled}
418
- activeOpacity={0.6}
419
- style={[styles.actionRow, actionDisabled && styles.rowDisabled]}
420
- >
421
- <Ionicons name="settings-outline" size={18} color={colors.icon} />
422
- <Text style={[styles.actionText, { color: colors.text }]}>
423
- {t('accountMenu.manage') || 'Manage your Oxy Account'}
424
- </Text>
425
- </TouchableOpacity>
426
-
427
- {/* 5) Sign out (current). */}
428
- <TouchableOpacity
429
- accessibilityRole="menuitem"
430
- accessibilityLabel={t('common.actions.signOut') || 'Sign out'}
431
- onPress={() => signOutDialog.open()}
432
- disabled={actionDisabled}
433
- activeOpacity={0.6}
434
- style={[styles.actionRow, actionDisabled && styles.rowDisabled]}
435
- >
436
- {signingOut ? (
437
- <ActivityIndicator color={colors.error} size="small" />
438
- ) : (
439
- <Ionicons name="log-out-outline" size={18} color={colors.error} />
440
- )}
441
- <Text style={[styles.actionText, { color: colors.error }]}>
442
- {t('common.actions.signOut') || 'Sign out'}
443
- </Text>
444
- </TouchableOpacity>
445
-
446
- {/* 6) Sign out of all accounts (only when >1 device sign-in). */}
447
- {deviceCount > 1 ? (
448
- <TouchableOpacity
449
- accessibilityRole="menuitem"
450
- accessibilityLabel={t('accountMenu.signOutAll') || 'Sign out of all accounts'}
451
- onPress={() => signOutAllDialog.open()}
452
- disabled={actionDisabled}
453
- activeOpacity={0.6}
454
- style={[styles.actionRow, actionDisabled && styles.rowDisabled]}
455
- >
456
- {signingOutAll ? (
457
- <ActivityIndicator color={colors.error} size="small" />
458
- ) : (
459
- <Ionicons name="log-out-outline" size={18} color={colors.error} />
460
- )}
461
- <Text style={[styles.actionText, { color: colors.error }]}>
462
- {t('accountMenu.signOutAll') || 'Sign out of all accounts'}
463
- </Text>
464
- </TouchableOpacity>
465
- ) : null}
466
- </ScrollView>
467
- </Pressable>
468
- );
469
-
470
- return (
471
- <Modal
472
- visible={open}
473
- transparent
474
- animationType={isWeb ? 'fade' : 'slide'}
475
- onRequestClose={onClose}
476
- >
477
- <Pressable
478
- accessibilityRole="button"
479
- accessibilityLabel={t('common.actions.close') || 'Close'}
480
- onPress={onClose}
481
- style={overlayStyles}
482
- >
483
- {/*
484
- * Web: the panel is `position: absolute` and rendered as a DIRECT
485
- * child of the full-viewport overlay, so its anchor edges resolve
486
- * against the viewport. The panel swallows its own taps.
487
- *
488
- * Native: the overlay is `justify-content: flex-end`, so the
489
- * (statically positioned) panel docks to the bottom as a sheet.
490
- */}
491
- {content}
492
- </Pressable>
493
-
494
- <Dialog
495
- control={signOutDialog}
496
- title={t('common.actions.signOut') || 'Sign out'}
497
- description={t('common.confirms.signOut') || 'Are you sure you want to sign out?'}
498
- actions={[
499
- {
500
- label: t('common.actions.signOut') || 'Sign out',
501
- color: 'destructive',
502
- onPress: performSignOut,
503
- },
504
- { label: t('common.cancel') || 'Cancel', color: 'cancel' },
505
- ]}
506
- />
507
- <Dialog
508
- control={signOutAllDialog}
509
- title={t('accountMenu.signOutAll') || 'Sign out of all accounts'}
510
- description={t('common.confirms.signOutAll') || 'Are you sure you want to sign out of all accounts?'}
511
- actions={[
512
- {
513
- label: t('accountMenu.signOutAll') || 'Sign out of all accounts',
514
- color: 'destructive',
515
- onPress: performSignOutAll,
516
- },
517
- { label: t('common.cancel') || 'Cancel', color: 'cancel' },
518
- ]}
519
- />
520
- </Modal>
521
- );
522
- };
523
-
524
- const styles = StyleSheet.create({
525
- webOverlay: {
526
- flex: 1,
527
- backgroundColor: 'transparent',
528
- // Explicit positioning context: the absolutely-positioned panel is a
529
- // direct child and resolves its anchor edges (top/bottom/left/right)
530
- // against this full-viewport overlay — matching the trigger-rect math
531
- // callers compute from `window.innerWidth` / `window.innerHeight`.
532
- position: 'relative',
533
- },
534
- nativeOverlay: {
535
- flex: 1,
536
- backgroundColor: 'rgba(0,0,0,0.32)',
537
- justifyContent: 'flex-end',
538
- },
539
- panelBase: {
540
- borderRadius: 12,
541
- shadowColor: '#000',
542
- shadowOpacity: 0.18,
543
- shadowRadius: 24,
544
- shadowOffset: { width: 0, height: 8 },
545
- elevation: 12,
546
- overflow: 'hidden',
547
- },
548
- panelWeb: {
549
- position: 'absolute',
550
- width: PANEL_WIDTH,
551
- maxHeight: '85%',
552
- borderWidth: 1,
553
- },
554
- panelNative: {
555
- marginHorizontal: 0,
556
- borderTopLeftRadius: 20,
557
- borderTopRightRadius: 20,
558
- borderBottomLeftRadius: 0,
559
- borderBottomRightRadius: 0,
560
- paddingBottom: 12,
561
- maxHeight: '85%',
562
- },
563
- scroll: {
564
- flexGrow: 0,
565
- },
566
- scrollContent: {
567
- paddingVertical: 4,
568
- },
569
- accountRow: {
570
- flexDirection: 'row',
571
- alignItems: 'center',
572
- paddingHorizontal: 14,
573
- paddingVertical: 10,
574
- gap: 10,
575
- },
576
- childRow: {
577
- paddingLeft: 34,
578
- },
579
- rowDisabled: {
580
- opacity: 0.4,
581
- },
582
- accountInfo: {
583
- flex: 1,
584
- minWidth: 0,
585
- },
586
- nameRow: {
587
- flexDirection: 'row',
588
- alignItems: 'center',
589
- gap: 6,
590
- },
591
- accountName: {
592
- fontSize: 13,
593
- fontWeight: '500',
594
- flexShrink: 1,
595
- },
596
- accountNameActive: {
597
- fontWeight: '600',
598
- },
599
- roleBadge: {
600
- paddingHorizontal: 6,
601
- paddingVertical: 1,
602
- borderRadius: 8,
603
- },
604
- roleBadgeText: {
605
- fontSize: 10,
606
- fontWeight: '600',
607
- textTransform: 'capitalize',
608
- },
609
- accountEmail: {
610
- fontSize: 11,
611
- marginTop: 1,
612
- },
613
- rowSignOutButton: {
614
- width: 28,
615
- height: 28,
616
- alignItems: 'center',
617
- justifyContent: 'center',
618
- borderRadius: 14,
619
- opacity: 0.6,
620
- },
621
- switchingRow: {
622
- flexDirection: 'row',
623
- alignItems: 'center',
624
- justifyContent: 'center',
625
- paddingVertical: 8,
626
- gap: 8,
627
- },
628
- switchingText: {
629
- fontSize: 12,
630
- fontWeight: '500',
631
- },
632
- actionRow: {
633
- flexDirection: 'row',
634
- alignItems: 'center',
635
- paddingHorizontal: 14,
636
- paddingVertical: 12,
637
- gap: 10,
638
- },
639
- actionText: {
640
- fontSize: 13,
641
- fontWeight: '500',
642
- },
643
- });
644
-
645
- export default AccountMenu;