@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,119 +0,0 @@
1
- /**
2
- * AnotherDeviceQR — the collapsed "Sign in on another device" disclosure shared
3
- * by both sign-in containers (`SignInModal` on web, `OxyAuthScreen` on native).
4
- *
5
- * The QR code is always DEMOTED below the platform-primary "Continue with Oxy"
6
- * action: you cannot scan your own screen, so the QR is only useful for handing
7
- * sign-in to a SECOND device (scan with the Oxy Accounts app there). It lives
8
- * inside a tap-to-expand disclosure so it never competes with the primary CTA.
9
- *
10
- * The QR plate stays white intentionally — high contrast is required for
11
- * reliable scanning regardless of the app theme. That single plate (and its
12
- * fixed dimensions) is the only StyleSheet/hardcoded-color in this component;
13
- * everything else is composed from Bloom typography + centralized token classes.
14
- */
15
-
16
- import type React from 'react';
17
- import { useState, useCallback } from 'react';
18
- import { View, TouchableOpacity, StyleSheet } from 'react-native';
19
- import QRCode from 'react-native-qrcode-svg';
20
- import { useTheme } from '@oxyhq/bloom/theme';
21
- import { Text } from '@oxyhq/bloom/typography';
22
- import * as Icons from '@oxyhq/bloom/icons';
23
-
24
- export interface AnotherDeviceQRProps {
25
- /** The legacy QR payload (`oxyauth://<token>`). Empty string renders nothing. */
26
- qrData: string;
27
- /**
28
- * The secure "Sign in with Oxy" handoff payload (`oxycommons://approve?...`).
29
- * Preferred over `qrData` when present — it carries the PUBLIC authorize code
30
- * (never the secret session token), so the Oxy identity app on another device
31
- * can scan it to approve. Falls back to `qrData` when the handoff backend has
32
- * not returned a payload.
33
- */
34
- qrPayload?: string | null;
35
- }
36
-
37
- /** Size, in px, of the rendered QR symbol. */
38
- const QR_SIZE = 200;
39
-
40
- /** High-contrast QR colors — intentionally fixed (NOT themed) for scan reliability. */
41
- const QR_PLATE_BG = '#FFFFFF';
42
- const QR_FOREGROUND = '#000000';
43
-
44
- /**
45
- * Collapsed disclosure that reveals a high-contrast QR for signing in on a
46
- * second device with the Oxy Accounts app.
47
- */
48
- const AnotherDeviceQR: React.FC<AnotherDeviceQRProps> = ({ qrData, qrPayload }) => {
49
- const bloomTheme = useTheme();
50
- const [expanded, setExpanded] = useState(false);
51
-
52
- const toggle = useCallback(() => {
53
- setExpanded((prev) => !prev);
54
- }, []);
55
-
56
- // Prefer the secure handoff payload (public authorize code) over the legacy
57
- // token QR; fall back to `qrData` until the handoff backend is available.
58
- const value = qrPayload || qrData;
59
-
60
- if (!value) {
61
- return null;
62
- }
63
-
64
- return (
65
- <View className="w-full items-center">
66
- <TouchableOpacity
67
- className="flex-row items-center justify-center gap-space-8 py-space-12 px-space-8"
68
- onPress={toggle}
69
- accessibilityRole="button"
70
- accessibilityState={{ expanded }}
71
- accessibilityHint="Reveals a QR code to sign in using the Oxy Accounts app on another device"
72
- >
73
- <Icons.QrCode_Stroke2_Corner0_Rounded
74
- size="sm"
75
- style={{ color: bloomTheme.colors.textSecondary }}
76
- />
77
- <Text className="font-sansSemibold text-body text-text-secondary">
78
- Sign in on another device
79
- </Text>
80
- {expanded ? (
81
- <Icons.ChevronTop_Stroke2_Corner0_Rounded
82
- size="sm"
83
- style={{ color: bloomTheme.colors.textSecondary }}
84
- />
85
- ) : (
86
- <Icons.ChevronRight_Stroke2_Corner0_Rounded
87
- size="sm"
88
- style={{ color: bloomTheme.colors.textSecondary }}
89
- />
90
- )}
91
- </TouchableOpacity>
92
-
93
- {expanded && (
94
- <View className="items-center mt-space-8 gap-space-12">
95
- {/* The QR plate is intentionally white for scan reliability. */}
96
- <View style={styles.qrPlate}>
97
- <QRCode value={value} size={QR_SIZE} backgroundColor={QR_PLATE_BG} color={QR_FOREGROUND} />
98
- </View>
99
- <Text className="font-sans text-caption text-text-tertiary text-center">
100
- Scan with the Oxy app
101
- </Text>
102
- </View>
103
- )}
104
- </View>
105
- );
106
- };
107
-
108
- // Measured/positioned layout only (no theming): the high-contrast QR plate keeps
109
- // a fixed white background + padding so the symbol scans reliably regardless of
110
- // the app theme. This is the single intentional non-token surface in the file.
111
- const styles = StyleSheet.create({
112
- qrPlate: {
113
- padding: 16,
114
- borderRadius: 16,
115
- backgroundColor: QR_PLATE_BG,
116
- },
117
- });
118
-
119
- export default AnotherDeviceQR;
@@ -1,564 +0,0 @@
1
- import type React from 'react';
2
- import { useCallback, useEffect, useMemo, useState } from 'react';
3
- import {
4
- View,
5
- Pressable,
6
- Modal,
7
- ScrollView,
8
- ActivityIndicator,
9
- Platform,
10
- StyleSheet,
11
- type ViewStyle,
12
- } from 'react-native';
13
- import { MaterialCommunityIcons } from '@expo/vector-icons';
14
- import { Avatar } from '@oxyhq/bloom/avatar';
15
- import { Text } from '@oxyhq/bloom/typography';
16
- import { useTheme } from '@oxyhq/bloom/theme';
17
- import { toast, useDialogControl, Dialog } from '@oxyhq/bloom';
18
- import { Divider } from '@oxyhq/bloom/divider';
19
- import { isDev, logger as loggerUtil } from '@oxyhq/core';
20
- import { useOxy } from '../context/OxyContext';
21
- import { useI18n } from '../hooks/useI18n';
22
- import { useSwitchableAccounts, type SwitchableAccount } from '../hooks/useSwitchableAccounts';
23
-
24
- const isWeb = Platform.OS === 'web';
25
-
26
- /** Fixed popover width on web. `ProfileButton` mirrors this when anchoring. */
27
- const MENU_WIDTH = 300;
28
-
29
- /**
30
- * Web-only anchor. `ProfileButton` measures its trigger and anchors the panel to
31
- * one corner so the menu opens either UPWARD (footer trigger) or DOWNWARD (a
32
- * trigger at the top of a sidebar). Exactly ONE vertical edge is pinned:
33
- * - `bottom` set → the panel's BOTTOM edge is anchored, so it opens UPWARD.
34
- * - `top` set → the panel's TOP edge is anchored, so it opens DOWNWARD.
35
- * Native ignores the anchor and docks the panel to the bottom as a sheet.
36
- */
37
- export interface ProfileMenuAnchor {
38
- /** Distance from the viewport left, for the panel's LEFT edge. */
39
- left: number;
40
- /** Distance from the viewport bottom, for the panel's BOTTOM edge (opens upward). */
41
- bottom?: number;
42
- /** Distance from the viewport top, for the panel's TOP edge (opens downward). */
43
- top?: number;
44
- }
45
-
46
- export interface ProfileMenuProps {
47
- open: boolean;
48
- onClose: () => void;
49
- /** Web-only trigger anchor. Native ignores this (bottom-sheet style). */
50
- anchor?: ProfileMenuAnchor | null;
51
- /** Navigate to the "Manage account" surface (settings). */
52
- onNavigateManage: () => void;
53
- /** Start the add-account / sign-in flow for an additional account. */
54
- onAddAccount: () => void;
55
- /** Optional: navigate to the signed-in user's own profile. */
56
- onNavigateProfile?: () => void;
57
- /** Called before the active identity changes so apps can clear scoped state. */
58
- onBeforeSessionChange?: () => void | Promise<void>;
59
- }
60
-
61
- /**
62
- * Clean account switcher, written with react-native `StyleSheet` + Bloom
63
- * theme colors (the package convention — no NativeWind, which is only an
64
- * optional peer and absent in the primary `accounts` consumer). Lists EVERY
65
- * switchable account from {@link useSwitchableAccounts}
66
- * — device sign-ins AND linked graph accounts (owned orgs + shared-with-you) —
67
- * and routes EVERY switch through the context's single
68
- * `switchToAccount(accountId)` dispatcher (there is no separate device-only
69
- * path). Each on-device inactive row carries a per-account sign-out icon;
70
- * graph-only rows carry a role badge and indent under their parent. Below the
71
- * list: Add account, Manage account, optional View profile, and Sign out of all.
72
- *
73
- * Renders as an upward-opening popover anchored to the trigger on web, and a
74
- * bottom-sheet style modal on native.
75
- */
76
- const ProfileMenu: React.FC<ProfileMenuProps> = ({
77
- open,
78
- onClose,
79
- anchor,
80
- onNavigateManage,
81
- onAddAccount,
82
- onNavigateProfile,
83
- onBeforeSessionChange,
84
- }) => {
85
- const {
86
- switchToAccount,
87
- logoutAll,
88
- removeSession,
89
- } = useOxy();
90
- const { t } = useI18n();
91
- const { colors } = useTheme();
92
-
93
- const { accounts } = useSwitchableAccounts();
94
-
95
- const [busyAccountId, setBusyAccountId] = useState<string | null>(null);
96
- const [removingSessionId, setRemovingSessionId] = useState<string | null>(null);
97
- const [signingOutAll, setSigningOutAll] = useState(false);
98
-
99
- const signOutAllDialog = useDialogControl();
100
-
101
- const isSwitching = busyAccountId !== null;
102
- const actionDisabled = isSwitching || removingSessionId !== null || signingOutAll;
103
-
104
- // Account ids currently in the list — used to decide whether a graph-only
105
- // row is a CHILD (its parent is also shown) so it can be indented.
106
- const listedAccountIds = useMemo(
107
- () => new Set(accounts.map((account) => account.accountId)),
108
- [accounts],
109
- );
110
-
111
- // The ONE uniform switch path: every row (device or linked) routes through
112
- // `switchToAccount(accountId)`. It reuses the device session when the account
113
- // is already signed in here, and mints one only on first entry.
114
- const handleSwitch = useCallback(async (account: SwitchableAccount) => {
115
- if (account.isCurrent || busyAccountId) {
116
- return;
117
- }
118
- setBusyAccountId(account.accountId);
119
- try {
120
- await onBeforeSessionChange?.();
121
- await switchToAccount(account.accountId);
122
- toast.success(t('accountSwitcher.toasts.switchSuccess') || 'Switched account');
123
- onClose();
124
- } catch (error) {
125
- if (!isDev()) {
126
- loggerUtil.warn('Switch account failed', { component: 'ProfileMenu' }, error as unknown);
127
- }
128
- toast.error(t('accountSwitcher.toasts.switchFailed') || 'Failed to switch account');
129
- } finally {
130
- setBusyAccountId(null);
131
- }
132
- }, [busyAccountId, switchToAccount, t, onClose, onBeforeSessionChange]);
133
-
134
- // Sign out a specific inactive account without switching/clearing the active
135
- // one. The menu stays open so the user can keep managing accounts.
136
- const handleRemove = useCallback(async (sessionId: string) => {
137
- if (removingSessionId) {
138
- return;
139
- }
140
- setRemovingSessionId(sessionId);
141
- try {
142
- await removeSession(sessionId);
143
- toast.success(t('common.actions.signedOut') || 'Signed out');
144
- } catch (error) {
145
- loggerUtil.warn('Remove account failed', { component: 'ProfileMenu' }, error as unknown);
146
- toast.error(t('common.errors.signOutFailed') || 'Failed to sign out');
147
- } finally {
148
- setRemovingSessionId(null);
149
- }
150
- }, [removingSessionId, removeSession, t]);
151
-
152
- const performSignOutAll = useCallback(async () => {
153
- if (signingOutAll) {
154
- return;
155
- }
156
- setSigningOutAll(true);
157
- try {
158
- await onBeforeSessionChange?.();
159
- await logoutAll();
160
- toast.success(t('accountSwitcher.toasts.signOutAllSuccess') || 'Signed out of all accounts');
161
- onClose();
162
- } catch (error) {
163
- loggerUtil.warn('Sign out all failed', { component: 'ProfileMenu' }, error as unknown);
164
- toast.error(t('common.errors.signOutAllFailed') || 'Failed to sign out of all accounts');
165
- } finally {
166
- setSigningOutAll(false);
167
- }
168
- }, [signingOutAll, logoutAll, t, onClose, onBeforeSessionChange]);
169
-
170
- // Escape-to-close (web only).
171
- useEffect(() => {
172
- if (!open || !isWeb || typeof document === 'undefined') {
173
- return undefined;
174
- }
175
- const onKey = (event: KeyboardEvent) => {
176
- if (event.key === 'Escape') {
177
- event.stopPropagation();
178
- onClose();
179
- }
180
- };
181
- document.addEventListener('keydown', onKey, true);
182
- return () => document.removeEventListener('keydown', onKey, true);
183
- }, [open, onClose]);
184
-
185
- if (!open) {
186
- return null;
187
- }
188
-
189
- // Web: anchor the panel to the measured trigger. When the anchor pins `top`
190
- // the panel opens DOWNWARD from the trigger; otherwise it pins `bottom` and
191
- // opens UPWARD (the default footer behavior). With no anchor captured, fall
192
- // back to a bottom-left placement. Native ignores this (the panel docks to
193
- // the bottom via the overlay's flex-end).
194
- const panelAnchorStyle: ViewStyle | undefined = isWeb
195
- ? {
196
- position: 'absolute',
197
- width: MENU_WIDTH,
198
- left: anchor?.left ?? 8,
199
- ...(typeof anchor?.top === 'number'
200
- ? { top: anchor.top }
201
- : { bottom: anchor?.bottom ?? 8 }),
202
- }
203
- : undefined;
204
-
205
- const content = (
206
- <Pressable
207
- // Swallow taps inside the panel so they never reach the overlay's
208
- // outside-tap-to-close handler.
209
- onPress={() => undefined}
210
- style={[
211
- isWeb ? styles.panelWeb : styles.panelNative,
212
- { backgroundColor: colors.background },
213
- isWeb ? { borderColor: colors.border } : null,
214
- panelAnchorStyle,
215
- styles.panelBounds,
216
- styles.shadow,
217
- ]}
218
- accessibilityRole="menu"
219
- accessibilityLabel={t('accountMenu.label') || 'Account menu'}
220
- >
221
- <ScrollView
222
- style={styles.scroll}
223
- contentContainerStyle={styles.scrollContent}
224
- showsVerticalScrollIndicator={false}
225
- >
226
- {/* 1) Every switchable account — device sign-ins AND linked graph
227
- accounts. Active first (checkmark); on-device rows carry a
228
- per-account sign-out icon; graph-only rows show a role badge
229
- and indent under their parent. Every row switches through the
230
- SAME `switchToAccount(accountId)` path. */}
231
- {accounts.map((account) => {
232
- const isActive = account.isCurrent;
233
- const isBusy = busyAccountId === account.accountId;
234
- const sessionId = account.sessionId;
235
- const isRemoving = sessionId ? removingSessionId === sessionId : false;
236
- const isChild = Boolean(
237
- account.parentAccountId && listedAccountIds.has(account.parentAccountId),
238
- );
239
- const role = account.callerMembership?.role;
240
- return (
241
- <Pressable
242
- key={`account-${account.accountId}`}
243
- accessibilityRole="menuitem"
244
- accessibilityLabel={account.displayName}
245
- accessibilityState={{ selected: isActive }}
246
- onPress={() => handleSwitch(account)}
247
- disabled={isActive || isBusy || isSwitching}
248
- style={[
249
- styles.accountRow,
250
- isChild && styles.childRow,
251
- isActive && { backgroundColor: colors.backgroundSecondary },
252
- isSwitching && !isActive && styles.rowDimmed,
253
- ]}
254
- >
255
- <Avatar
256
- source={account.user.avatar ?? undefined}
257
- uri={account.avatarUrl}
258
- variant="thumb"
259
- name={account.displayName}
260
- size={isActive ? 40 : isChild ? 28 : 32}
261
- />
262
- <View style={styles.accountInfo}>
263
- <View style={styles.nameRow}>
264
- <Text
265
- style={[
266
- styles.accountName,
267
- { color: colors.text },
268
- isActive && styles.accountNameActive,
269
- ]}
270
- numberOfLines={1}
271
- >
272
- {account.displayName}
273
- </Text>
274
- {role ? (
275
- <View style={[styles.roleBadge, { backgroundColor: colors.card }]}>
276
- <Text style={[styles.roleBadgeText, { color: colors.textSecondary }]}>
277
- {t(`accounts.roles.${role}.label`) || role}
278
- </Text>
279
- </View>
280
- ) : null}
281
- </View>
282
- {account.email ? (
283
- <Text
284
- style={[styles.accountEmail, { color: colors.textSecondary }]}
285
- numberOfLines={1}
286
- >
287
- {account.email}
288
- </Text>
289
- ) : null}
290
- </View>
291
- {isBusy ? (
292
- <ActivityIndicator color={colors.primary} size="small" />
293
- ) : isActive ? (
294
- <MaterialCommunityIcons name="check" size={20} color={colors.primary} />
295
- ) : isRemoving ? (
296
- <ActivityIndicator color={colors.textSecondary} size="small" />
297
- ) : sessionId ? (
298
- <Pressable
299
- accessibilityRole="button"
300
- accessibilityLabel={
301
- t('accountMenu.signOutAccount', { name: account.displayName })
302
- || `Sign out ${account.displayName}`
303
- }
304
- onPress={() => handleRemove(sessionId)}
305
- disabled={actionDisabled}
306
- hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
307
- style={styles.rowSignOutButton}
308
- >
309
- <MaterialCommunityIcons
310
- name="logout"
311
- size={18}
312
- color={colors.textSecondary}
313
- />
314
- </Pressable>
315
- ) : null}
316
- </Pressable>
317
- );
318
- })}
319
-
320
- {/* 2) Switching indicator. */}
321
- {isSwitching ? (
322
- <View style={styles.switchingRow}>
323
- <ActivityIndicator color={colors.textSecondary} size="small" />
324
- <Text style={[styles.switchingText, { color: colors.textSecondary }]}>
325
- {t('accountMenu.switching') || 'Switching account…'}
326
- </Text>
327
- </View>
328
- ) : null}
329
-
330
- <Divider color={colors.border} spacing={4} />
331
-
332
- {/* 3) Add another account. */}
333
- <ActionRow
334
- icon="account-plus-outline"
335
- iconColor={colors.icon}
336
- textColor={colors.text}
337
- label={t('accountMenu.addAnother') || 'Add another account'}
338
- disabled={actionDisabled}
339
- onPress={() => {
340
- onClose();
341
- onAddAccount();
342
- }}
343
- />
344
-
345
- {/* 4) Manage account. */}
346
- <ActionRow
347
- icon="cog-outline"
348
- iconColor={colors.icon}
349
- textColor={colors.text}
350
- label={t('accountMenu.manage') || 'Manage your Oxy Account'}
351
- disabled={actionDisabled}
352
- onPress={() => {
353
- onClose();
354
- onNavigateManage();
355
- }}
356
- />
357
-
358
- {/* 5) View profile (optional). */}
359
- {onNavigateProfile ? (
360
- <ActionRow
361
- icon="account-outline"
362
- iconColor={colors.icon}
363
- textColor={colors.text}
364
- label={t('accountMenu.viewProfile') || 'View profile'}
365
- disabled={actionDisabled}
366
- onPress={() => {
367
- onClose();
368
- onNavigateProfile();
369
- }}
370
- />
371
- ) : null}
372
-
373
- {/* 6) Sign out of all accounts. */}
374
- <Divider color={colors.border} spacing={4} />
375
- <Pressable
376
- accessibilityRole="menuitem"
377
- accessibilityLabel={t('accountMenu.signOutAll') || 'Sign out of all accounts'}
378
- onPress={() => signOutAllDialog.open()}
379
- disabled={actionDisabled}
380
- style={[styles.actionRow, actionDisabled && styles.rowDimmed]}
381
- >
382
- {signingOutAll ? (
383
- <ActivityIndicator color={colors.error} size="small" />
384
- ) : (
385
- <MaterialCommunityIcons name="logout" size={18} color={colors.error} />
386
- )}
387
- <Text style={[styles.actionText, { color: colors.error }]}>
388
- {t('accountMenu.signOutAll') || 'Sign out of all accounts'}
389
- </Text>
390
- </Pressable>
391
- </ScrollView>
392
- </Pressable>
393
- );
394
-
395
- return (
396
- <Modal
397
- visible={open}
398
- transparent
399
- animationType={isWeb ? 'fade' : 'slide'}
400
- onRequestClose={onClose}
401
- >
402
- <Pressable
403
- accessibilityRole="button"
404
- accessibilityLabel={t('common.actions.close') || 'Close'}
405
- onPress={onClose}
406
- style={isWeb ? styles.webOverlay : styles.nativeOverlay}
407
- >
408
- {content}
409
- </Pressable>
410
-
411
- <Dialog
412
- control={signOutAllDialog}
413
- title={t('accountMenu.signOutAll') || 'Sign out of all accounts'}
414
- description={t('common.confirms.signOutAll') || 'Are you sure you want to sign out of all accounts?'}
415
- actions={[
416
- {
417
- label: t('accountMenu.signOutAll') || 'Sign out of all accounts',
418
- color: 'destructive',
419
- onPress: performSignOutAll,
420
- },
421
- { label: t('common.cancel') || 'Cancel', color: 'cancel' },
422
- ]}
423
- />
424
- </Modal>
425
- );
426
- };
427
-
428
- /** Bottom-section action row (Add account / Manage / View profile). */
429
- const ActionRow: React.FC<{
430
- icon: React.ComponentProps<typeof MaterialCommunityIcons>['name'];
431
- iconColor: string;
432
- textColor: string;
433
- label: string;
434
- disabled: boolean;
435
- onPress: () => void;
436
- }> = ({ icon, iconColor, textColor, label, disabled, onPress }) => (
437
- <Pressable
438
- accessibilityRole="menuitem"
439
- accessibilityLabel={label}
440
- onPress={onPress}
441
- disabled={disabled}
442
- style={[styles.actionRow, disabled && styles.rowDimmed]}
443
- >
444
- <MaterialCommunityIcons name={icon} size={20} color={iconColor} />
445
- <Text style={[styles.actionText, { color: textColor }]}>{label}</Text>
446
- </Pressable>
447
- );
448
-
449
- const styles = StyleSheet.create({
450
- // Overlay: web is a positioning context for the anchored panel; native dims
451
- // the backdrop and docks the sheet to the bottom.
452
- webOverlay: {
453
- flex: 1,
454
- position: 'relative',
455
- },
456
- nativeOverlay: {
457
- flex: 1,
458
- justifyContent: 'flex-end',
459
- backgroundColor: 'rgba(0,0,0,0.32)',
460
- },
461
- // Panel shell — web popover / native bottom sheet.
462
- panelWeb: {
463
- overflow: 'hidden',
464
- borderRadius: 16,
465
- borderWidth: 1,
466
- },
467
- panelNative: {
468
- overflow: 'hidden',
469
- borderTopLeftRadius: 24,
470
- borderTopRightRadius: 24,
471
- paddingBottom: 12,
472
- },
473
- // Shared height cap applied to both panel variants.
474
- panelBounds: {
475
- maxHeight: '85%',
476
- },
477
- // The panel's drop shadow (dynamic elevation).
478
- shadow: {
479
- shadowColor: '#000',
480
- shadowOpacity: 0.18,
481
- shadowRadius: 24,
482
- shadowOffset: { width: 0, height: 8 },
483
- elevation: 12,
484
- },
485
- scroll: {
486
- flexGrow: 0,
487
- },
488
- scrollContent: {
489
- paddingVertical: 4,
490
- },
491
- accountRow: {
492
- flexDirection: 'row',
493
- alignItems: 'center',
494
- gap: 12,
495
- paddingHorizontal: 16,
496
- paddingVertical: 10,
497
- },
498
- childRow: {
499
- paddingLeft: 40,
500
- },
501
- rowDimmed: {
502
- opacity: 0.4,
503
- },
504
- accountInfo: {
505
- flex: 1,
506
- minWidth: 0,
507
- },
508
- nameRow: {
509
- flexDirection: 'row',
510
- alignItems: 'center',
511
- gap: 6,
512
- },
513
- accountName: {
514
- fontWeight: '500',
515
- flexShrink: 1,
516
- },
517
- accountNameActive: {
518
- fontWeight: '600',
519
- },
520
- accountEmail: {
521
- fontSize: 12,
522
- },
523
- roleBadge: {
524
- paddingHorizontal: 6,
525
- paddingVertical: 1,
526
- borderRadius: 8,
527
- },
528
- roleBadgeText: {
529
- fontSize: 10,
530
- fontWeight: '600',
531
- textTransform: 'capitalize',
532
- },
533
- rowSignOutButton: {
534
- width: 28,
535
- height: 28,
536
- alignItems: 'center',
537
- justifyContent: 'center',
538
- borderRadius: 9999,
539
- opacity: 0.6,
540
- },
541
- switchingRow: {
542
- flexDirection: 'row',
543
- alignItems: 'center',
544
- justifyContent: 'center',
545
- gap: 8,
546
- paddingVertical: 8,
547
- },
548
- switchingText: {
549
- fontSize: 12,
550
- fontWeight: '500',
551
- },
552
- actionRow: {
553
- flexDirection: 'row',
554
- alignItems: 'center',
555
- gap: 12,
556
- paddingHorizontal: 16,
557
- paddingVertical: 12,
558
- },
559
- actionText: {
560
- fontWeight: '500',
561
- },
562
- });
563
-
564
- export default ProfileMenu;