@oxyhq/services 13.2.0 → 14.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 (260) hide show
  1. package/lib/commonjs/index.js +11 -17
  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/ProfileButton.js +81 -22
  8. package/lib/commonjs/ui/components/ProfileButton.js.map +1 -1
  9. package/lib/commonjs/ui/components/ProfileMenu.js +244 -142
  10. package/lib/commonjs/ui/components/ProfileMenu.js.map +1 -1
  11. package/lib/commonjs/ui/components/SignInModal.js +4 -4
  12. package/lib/commonjs/ui/components/SignInModal.js.map +1 -1
  13. package/lib/commonjs/ui/context/OxyContext.js +506 -466
  14. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  15. package/lib/commonjs/ui/context/hooks/useAuthOperations.js +59 -57
  16. package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
  17. package/lib/commonjs/ui/context/inSessionTokenRefresh.js +23 -42
  18. package/lib/commonjs/ui/context/inSessionTokenRefresh.js.map +1 -1
  19. package/lib/commonjs/ui/context/silentSessionRestore.js +12 -21
  20. package/lib/commonjs/ui/context/silentSessionRestore.js.map +1 -1
  21. package/lib/commonjs/ui/hooks/useAuth.js +4 -3
  22. package/lib/commonjs/ui/hooks/useAuth.js.map +1 -1
  23. package/lib/commonjs/ui/hooks/useOxyAuthSession.js +12 -7
  24. package/lib/commonjs/ui/hooks/useOxyAuthSession.js.map +1 -1
  25. package/lib/commonjs/ui/hooks/useSessionManagement.js +17 -65
  26. package/lib/commonjs/ui/hooks/useSessionManagement.js.map +1 -1
  27. package/lib/commonjs/ui/hooks/useSwitchableAccounts.js +288 -0
  28. package/lib/commonjs/ui/hooks/useSwitchableAccounts.js.map +1 -0
  29. package/lib/commonjs/ui/hooks/useWebSSO.js +12 -191
  30. package/lib/commonjs/ui/hooks/useWebSSO.js.map +1 -1
  31. package/lib/commonjs/ui/index.js.map +1 -1
  32. package/lib/commonjs/ui/screens/OxyAuthScreen.js +2 -2
  33. package/lib/commonjs/ui/screens/OxyAuthScreen.js.map +1 -1
  34. package/lib/commonjs/ui/session/createSessionClient.js +28 -0
  35. package/lib/commonjs/ui/session/createSessionClient.js.map +1 -0
  36. package/lib/commonjs/ui/session/index.js +52 -0
  37. package/lib/commonjs/ui/session/index.js.map +1 -0
  38. package/lib/commonjs/ui/session/projectSessionState.js +86 -0
  39. package/lib/commonjs/ui/session/projectSessionState.js.map +1 -0
  40. package/lib/commonjs/ui/session/sessionClientHost.js +30 -0
  41. package/lib/commonjs/ui/session/sessionClientHost.js.map +1 -0
  42. package/lib/commonjs/ui/session/tokenTransport.js +77 -0
  43. package/lib/commonjs/ui/session/tokenTransport.js.map +1 -0
  44. package/lib/commonjs/ui/utils/activeAuthuser.js +35 -79
  45. package/lib/commonjs/ui/utils/activeAuthuser.js.map +1 -1
  46. package/lib/commonjs/ui/utils/sessionHelpers.js +1 -51
  47. package/lib/commonjs/ui/utils/sessionHelpers.js.map +1 -1
  48. package/lib/commonjs/utils/deviceFlowSignIn.js +70 -15
  49. package/lib/commonjs/utils/deviceFlowSignIn.js.map +1 -1
  50. package/lib/module/index.js +5 -5
  51. package/lib/module/index.js.map +1 -1
  52. package/lib/module/ui/components/AccountMenu.js +103 -65
  53. package/lib/module/ui/components/AccountMenu.js.map +1 -1
  54. package/lib/module/ui/components/AccountSwitcher.js +85 -127
  55. package/lib/module/ui/components/AccountSwitcher.js.map +1 -1
  56. package/lib/module/ui/components/ProfileButton.js +82 -23
  57. package/lib/module/ui/components/ProfileButton.js.map +1 -1
  58. package/lib/module/ui/components/ProfileMenu.js +247 -145
  59. package/lib/module/ui/components/ProfileMenu.js.map +1 -1
  60. package/lib/module/ui/components/SignInModal.js +4 -4
  61. package/lib/module/ui/components/SignInModal.js.map +1 -1
  62. package/lib/module/ui/context/OxyContext.js +510 -470
  63. package/lib/module/ui/context/OxyContext.js.map +1 -1
  64. package/lib/module/ui/context/hooks/useAuthOperations.js +61 -60
  65. package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
  66. package/lib/module/ui/context/inSessionTokenRefresh.js +24 -43
  67. package/lib/module/ui/context/inSessionTokenRefresh.js.map +1 -1
  68. package/lib/module/ui/context/silentSessionRestore.js +12 -20
  69. package/lib/module/ui/context/silentSessionRestore.js.map +1 -1
  70. package/lib/module/ui/hooks/useAuth.js +4 -3
  71. package/lib/module/ui/hooks/useAuth.js.map +1 -1
  72. package/lib/module/ui/hooks/useOxyAuthSession.js +12 -7
  73. package/lib/module/ui/hooks/useOxyAuthSession.js.map +1 -1
  74. package/lib/module/ui/hooks/useSessionManagement.js +18 -66
  75. package/lib/module/ui/hooks/useSessionManagement.js.map +1 -1
  76. package/lib/module/ui/hooks/useSwitchableAccounts.js +283 -0
  77. package/lib/module/ui/hooks/useSwitchableAccounts.js.map +1 -0
  78. package/lib/module/ui/hooks/useWebSSO.js +13 -192
  79. package/lib/module/ui/hooks/useWebSSO.js.map +1 -1
  80. package/lib/module/ui/index.js +0 -1
  81. package/lib/module/ui/index.js.map +1 -1
  82. package/lib/module/ui/screens/OxyAuthScreen.js +2 -2
  83. package/lib/module/ui/screens/OxyAuthScreen.js.map +1 -1
  84. package/lib/module/ui/session/createSessionClient.js +25 -0
  85. package/lib/module/ui/session/createSessionClient.js.map +1 -0
  86. package/lib/module/ui/session/index.js +17 -0
  87. package/lib/module/ui/session/index.js.map +1 -0
  88. package/lib/module/ui/session/projectSessionState.js +79 -0
  89. package/lib/module/ui/session/projectSessionState.js.map +1 -0
  90. package/lib/module/ui/session/sessionClientHost.js +26 -0
  91. package/lib/module/ui/session/sessionClientHost.js.map +1 -0
  92. package/lib/module/ui/session/tokenTransport.js +74 -0
  93. package/lib/module/ui/session/tokenTransport.js.map +1 -0
  94. package/lib/module/ui/utils/activeAuthuser.js +34 -76
  95. package/lib/module/ui/utils/activeAuthuser.js.map +1 -1
  96. package/lib/module/ui/utils/sessionHelpers.js +0 -49
  97. package/lib/module/ui/utils/sessionHelpers.js.map +1 -1
  98. package/lib/module/utils/deviceFlowSignIn.js +71 -16
  99. package/lib/module/utils/deviceFlowSignIn.js.map +1 -1
  100. package/lib/typescript/commonjs/index.d.ts +2 -3
  101. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  102. package/lib/typescript/commonjs/ui/components/AccountMenu.d.ts +4 -2
  103. package/lib/typescript/commonjs/ui/components/AccountMenu.d.ts.map +1 -1
  104. package/lib/typescript/commonjs/ui/components/AccountSwitcher.d.ts +7 -8
  105. package/lib/typescript/commonjs/ui/components/AccountSwitcher.d.ts.map +1 -1
  106. package/lib/typescript/commonjs/ui/components/ProfileButton.d.ts +11 -1
  107. package/lib/typescript/commonjs/ui/components/ProfileButton.d.ts.map +1 -1
  108. package/lib/typescript/commonjs/ui/components/ProfileMenu.d.ts +12 -9
  109. package/lib/typescript/commonjs/ui/components/ProfileMenu.d.ts.map +1 -1
  110. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +16 -13
  111. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
  112. package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts +27 -3
  113. package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  114. package/lib/typescript/commonjs/ui/context/inSessionTokenRefresh.d.ts +22 -17
  115. package/lib/typescript/commonjs/ui/context/inSessionTokenRefresh.d.ts.map +1 -1
  116. package/lib/typescript/commonjs/ui/context/silentSessionRestore.d.ts +12 -18
  117. package/lib/typescript/commonjs/ui/context/silentSessionRestore.d.ts.map +1 -1
  118. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +1 -0
  119. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  120. package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts +1 -1
  121. package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts.map +1 -1
  122. package/lib/typescript/commonjs/ui/hooks/useOxyAuthSession.d.ts +2 -2
  123. package/lib/typescript/commonjs/ui/hooks/useOxyAuthSession.d.ts.map +1 -1
  124. package/lib/typescript/commonjs/ui/hooks/useSessionManagement.d.ts.map +1 -1
  125. package/lib/typescript/commonjs/ui/hooks/useSwitchableAccounts.d.ts +162 -0
  126. package/lib/typescript/commonjs/ui/hooks/useSwitchableAccounts.d.ts.map +1 -0
  127. package/lib/typescript/commonjs/ui/hooks/useWebSSO.d.ts +13 -52
  128. package/lib/typescript/commonjs/ui/hooks/useWebSSO.d.ts.map +1 -1
  129. package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
  130. package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts +15 -0
  131. package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts.map +1 -0
  132. package/lib/typescript/commonjs/ui/session/index.d.ts +15 -0
  133. package/lib/typescript/commonjs/ui/session/index.d.ts.map +1 -0
  134. package/lib/typescript/commonjs/ui/session/projectSessionState.d.ts +45 -0
  135. package/lib/typescript/commonjs/ui/session/projectSessionState.d.ts.map +1 -0
  136. package/lib/typescript/commonjs/ui/session/sessionClientHost.d.ts +14 -0
  137. package/lib/typescript/commonjs/ui/session/sessionClientHost.d.ts.map +1 -0
  138. package/lib/typescript/commonjs/ui/session/tokenTransport.d.ts +24 -0
  139. package/lib/typescript/commonjs/ui/session/tokenTransport.d.ts.map +1 -0
  140. package/lib/typescript/commonjs/ui/utils/activeAuthuser.d.ts +21 -29
  141. package/lib/typescript/commonjs/ui/utils/activeAuthuser.d.ts.map +1 -1
  142. package/lib/typescript/commonjs/ui/utils/sessionHelpers.d.ts +0 -19
  143. package/lib/typescript/commonjs/ui/utils/sessionHelpers.d.ts.map +1 -1
  144. package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts +51 -19
  145. package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts.map +1 -1
  146. package/lib/typescript/module/index.d.ts +2 -3
  147. package/lib/typescript/module/index.d.ts.map +1 -1
  148. package/lib/typescript/module/ui/components/AccountMenu.d.ts +4 -2
  149. package/lib/typescript/module/ui/components/AccountMenu.d.ts.map +1 -1
  150. package/lib/typescript/module/ui/components/AccountSwitcher.d.ts +7 -8
  151. package/lib/typescript/module/ui/components/AccountSwitcher.d.ts.map +1 -1
  152. package/lib/typescript/module/ui/components/ProfileButton.d.ts +11 -1
  153. package/lib/typescript/module/ui/components/ProfileButton.d.ts.map +1 -1
  154. package/lib/typescript/module/ui/components/ProfileMenu.d.ts +12 -9
  155. package/lib/typescript/module/ui/components/ProfileMenu.d.ts.map +1 -1
  156. package/lib/typescript/module/ui/context/OxyContext.d.ts +16 -13
  157. package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
  158. package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts +27 -3
  159. package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  160. package/lib/typescript/module/ui/context/inSessionTokenRefresh.d.ts +22 -17
  161. package/lib/typescript/module/ui/context/inSessionTokenRefresh.d.ts.map +1 -1
  162. package/lib/typescript/module/ui/context/silentSessionRestore.d.ts +12 -18
  163. package/lib/typescript/module/ui/context/silentSessionRestore.d.ts.map +1 -1
  164. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +1 -0
  165. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  166. package/lib/typescript/module/ui/hooks/useAuth.d.ts +1 -1
  167. package/lib/typescript/module/ui/hooks/useAuth.d.ts.map +1 -1
  168. package/lib/typescript/module/ui/hooks/useOxyAuthSession.d.ts +2 -2
  169. package/lib/typescript/module/ui/hooks/useOxyAuthSession.d.ts.map +1 -1
  170. package/lib/typescript/module/ui/hooks/useSessionManagement.d.ts.map +1 -1
  171. package/lib/typescript/module/ui/hooks/useSwitchableAccounts.d.ts +162 -0
  172. package/lib/typescript/module/ui/hooks/useSwitchableAccounts.d.ts.map +1 -0
  173. package/lib/typescript/module/ui/hooks/useWebSSO.d.ts +13 -52
  174. package/lib/typescript/module/ui/hooks/useWebSSO.d.ts.map +1 -1
  175. package/lib/typescript/module/ui/index.d.ts.map +1 -1
  176. package/lib/typescript/module/ui/session/createSessionClient.d.ts +15 -0
  177. package/lib/typescript/module/ui/session/createSessionClient.d.ts.map +1 -0
  178. package/lib/typescript/module/ui/session/index.d.ts +15 -0
  179. package/lib/typescript/module/ui/session/index.d.ts.map +1 -0
  180. package/lib/typescript/module/ui/session/projectSessionState.d.ts +45 -0
  181. package/lib/typescript/module/ui/session/projectSessionState.d.ts.map +1 -0
  182. package/lib/typescript/module/ui/session/sessionClientHost.d.ts +14 -0
  183. package/lib/typescript/module/ui/session/sessionClientHost.d.ts.map +1 -0
  184. package/lib/typescript/module/ui/session/tokenTransport.d.ts +24 -0
  185. package/lib/typescript/module/ui/session/tokenTransport.d.ts.map +1 -0
  186. package/lib/typescript/module/ui/utils/activeAuthuser.d.ts +21 -29
  187. package/lib/typescript/module/ui/utils/activeAuthuser.d.ts.map +1 -1
  188. package/lib/typescript/module/ui/utils/sessionHelpers.d.ts +0 -19
  189. package/lib/typescript/module/ui/utils/sessionHelpers.d.ts.map +1 -1
  190. package/lib/typescript/module/utils/deviceFlowSignIn.d.ts +51 -19
  191. package/lib/typescript/module/utils/deviceFlowSignIn.d.ts.map +1 -1
  192. package/package.json +4 -4
  193. package/src/index.ts +9 -9
  194. package/src/ui/components/AccountMenu.tsx +117 -70
  195. package/src/ui/components/AccountSwitcher.tsx +88 -134
  196. package/src/ui/components/ProfileButton.tsx +95 -17
  197. package/src/ui/components/ProfileMenu.tsx +263 -155
  198. package/src/ui/components/SignInModal.tsx +5 -5
  199. package/src/ui/context/OxyContext.tsx +560 -475
  200. package/src/ui/context/hooks/useAuthOperations.ts +72 -63
  201. package/src/ui/context/inSessionTokenRefresh.ts +23 -42
  202. package/src/ui/context/silentSessionRestore.ts +12 -26
  203. package/src/ui/hooks/useAuth.ts +4 -3
  204. package/src/ui/hooks/useOxyAuthSession.ts +13 -8
  205. package/src/ui/hooks/useSessionManagement.ts +16 -87
  206. package/src/ui/hooks/useSwitchableAccounts.ts +397 -0
  207. package/src/ui/hooks/useWebSSO.ts +13 -227
  208. package/src/ui/index.ts +0 -1
  209. package/src/ui/screens/OxyAuthScreen.tsx +2 -2
  210. package/src/ui/session/__tests__/createSessionClient.test.ts +41 -0
  211. package/src/ui/session/__tests__/projectSessionState.test.ts +114 -0
  212. package/src/ui/session/__tests__/sessionClientHost.test.ts +43 -0
  213. package/src/ui/session/__tests__/tokenTransport.test.ts +142 -0
  214. package/src/ui/session/createSessionClient.ts +21 -0
  215. package/src/ui/session/index.ts +19 -0
  216. package/src/ui/session/projectSessionState.ts +85 -0
  217. package/src/ui/session/sessionClientHost.ts +27 -0
  218. package/src/ui/session/tokenTransport.ts +76 -0
  219. package/src/ui/utils/activeAuthuser.ts +34 -76
  220. package/src/ui/utils/sessionHelpers.ts +0 -66
  221. package/src/utils/__tests__/deviceFlowSignIn.test.ts +243 -34
  222. package/src/utils/deviceFlowSignIn.ts +98 -21
  223. package/lib/commonjs/ui/components/accountMenuRows.js +0 -33
  224. package/lib/commonjs/ui/components/accountMenuRows.js.map +0 -1
  225. package/lib/commonjs/ui/hooks/useDeviceAccounts.js +0 -280
  226. package/lib/commonjs/ui/hooks/useDeviceAccounts.js.map +0 -1
  227. package/lib/commonjs/ui/hooks/useSessionSocket.js +0 -203
  228. package/lib/commonjs/ui/hooks/useSessionSocket.js.map +0 -1
  229. package/lib/commonjs/utils/silentGuardKey.js +0 -54
  230. package/lib/commonjs/utils/silentGuardKey.js.map +0 -1
  231. package/lib/module/ui/components/accountMenuRows.js +0 -29
  232. package/lib/module/ui/components/accountMenuRows.js.map +0 -1
  233. package/lib/module/ui/hooks/useDeviceAccounts.js +0 -276
  234. package/lib/module/ui/hooks/useDeviceAccounts.js.map +0 -1
  235. package/lib/module/ui/hooks/useSessionSocket.js +0 -199
  236. package/lib/module/ui/hooks/useSessionSocket.js.map +0 -1
  237. package/lib/module/utils/silentGuardKey.js +0 -49
  238. package/lib/module/utils/silentGuardKey.js.map +0 -1
  239. package/lib/typescript/commonjs/ui/components/accountMenuRows.d.ts +0 -34
  240. package/lib/typescript/commonjs/ui/components/accountMenuRows.d.ts.map +0 -1
  241. package/lib/typescript/commonjs/ui/hooks/useDeviceAccounts.d.ts +0 -133
  242. package/lib/typescript/commonjs/ui/hooks/useDeviceAccounts.d.ts.map +0 -1
  243. package/lib/typescript/commonjs/ui/hooks/useSessionSocket.d.ts +0 -14
  244. package/lib/typescript/commonjs/ui/hooks/useSessionSocket.d.ts.map +0 -1
  245. package/lib/typescript/commonjs/utils/silentGuardKey.d.ts +0 -31
  246. package/lib/typescript/commonjs/utils/silentGuardKey.d.ts.map +0 -1
  247. package/lib/typescript/module/ui/components/accountMenuRows.d.ts +0 -34
  248. package/lib/typescript/module/ui/components/accountMenuRows.d.ts.map +0 -1
  249. package/lib/typescript/module/ui/hooks/useDeviceAccounts.d.ts +0 -133
  250. package/lib/typescript/module/ui/hooks/useDeviceAccounts.d.ts.map +0 -1
  251. package/lib/typescript/module/ui/hooks/useSessionSocket.d.ts +0 -14
  252. package/lib/typescript/module/ui/hooks/useSessionSocket.d.ts.map +0 -1
  253. package/lib/typescript/module/utils/silentGuardKey.d.ts +0 -31
  254. package/lib/typescript/module/utils/silentGuardKey.d.ts.map +0 -1
  255. package/src/ui/components/accountMenuRows.ts +0 -48
  256. package/src/ui/hooks/useDeviceAccounts.ts +0 -344
  257. package/src/ui/hooks/useSessionSocket.ts +0 -233
  258. package/src/ui/utils/__tests__/activeAuthuser.test.ts +0 -75
  259. package/src/utils/__tests__/silentGuardKey.test.ts +0 -82
  260. 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();
@@ -3,6 +3,7 @@ import { useCallback, useRef, useState } from 'react';
3
3
  import {
4
4
  View,
5
5
  Pressable,
6
+ StyleSheet,
6
7
  Platform,
7
8
  type StyleProp,
8
9
  type ViewStyle,
@@ -85,7 +86,11 @@ export interface ProfileButtonProps {
85
86
  * Native (bottom-sheet) is unaffected — this only influences the web popover.
86
87
  */
87
88
  placement?: 'up' | 'down' | 'auto';
88
- /** Extra className applied to the outer trigger. */
89
+ /**
90
+ * Extra className applied to the outer trigger. Kept for NativeWind consumers
91
+ * that layer utility classes on top; the component's own layout is driven by
92
+ * `StyleSheet` so it renders correctly with or without NativeWind.
93
+ */
89
94
  className?: string;
90
95
  /** Extra style applied to the outer trigger. */
91
96
  style?: StyleProp<ViewStyle>;
@@ -104,6 +109,12 @@ export interface ProfileButtonProps {
104
109
  * against the measured trigger, or as a bottom sheet (native).
105
110
  * - **Signed out**: a "Sign in" row that calls `useAuth().signIn()`.
106
111
  *
112
+ * Styling uses react-native `StyleSheet` + the Bloom theme (via `useTheme`) so
113
+ * the layout renders identically in EVERY consumer — including apps that do not
114
+ * use NativeWind (e.g. the accounts app). Only the web hover animation keeps
115
+ * dynamic inline `style` (the CSS transition/transform values), which is what
116
+ * the `react-native-web-style.d.ts` augmentation exists for.
117
+ *
107
118
  * All display strings resolve through `@oxyhq/core`'s
108
119
  * `getAccountDisplayName` / `getAccountFallbackHandle` — no hand-rolled name
109
120
  * fallbacks. Avatars pass the bare file id as `source` + `variant="thumb"`, so
@@ -208,8 +219,14 @@ const ProfileButton: React.FC<ProfileButtonProps> = ({
208
219
  importantForAccessibility="no-hide-descendants"
209
220
  >
210
221
  <View
211
- className="rounded-full bg-secondary"
212
- style={{ width: resolvedAvatarSize, height: resolvedAvatarSize }}
222
+ style={[
223
+ styles.skeletonCircle,
224
+ {
225
+ width: resolvedAvatarSize,
226
+ height: resolvedAvatarSize,
227
+ backgroundColor: colors.backgroundSecondary,
228
+ },
229
+ ]}
213
230
  />
214
231
  </View>
215
232
  );
@@ -220,15 +237,21 @@ const ProfileButton: React.FC<ProfileButtonProps> = ({
220
237
  const signInLabel = t('common.actions.signIn') || 'Sign in';
221
238
  return (
222
239
  <Pressable
223
- className={`${expanded ? 'w-full ' : ''}flex-row items-center gap-3 rounded-full px-2 py-2 ${className ?? ''}`}
224
- style={style}
240
+ className={className}
241
+ style={[styles.row, expanded && styles.rowExpanded, style]}
225
242
  onPress={() => { void signIn(); }}
226
243
  accessibilityRole="button"
227
244
  accessibilityLabel={signInLabel}
228
245
  >
229
246
  <View
230
- className="items-center justify-center rounded-full bg-secondary"
231
- style={{ width: resolvedAvatarSize, height: resolvedAvatarSize }}
247
+ style={[
248
+ styles.avatarBadge,
249
+ {
250
+ width: resolvedAvatarSize,
251
+ height: resolvedAvatarSize,
252
+ backgroundColor: colors.backgroundSecondary,
253
+ },
254
+ ]}
232
255
  >
233
256
  <MaterialCommunityIcons
234
257
  name="login"
@@ -238,7 +261,7 @@ const ProfileButton: React.FC<ProfileButtonProps> = ({
238
261
  </View>
239
262
  {expanded ? (
240
263
  <Text
241
- className="flex-1 font-semibold text-foreground"
264
+ style={[styles.signInLabel, { color: colors.text }]}
242
265
  numberOfLines={1}
243
266
  >
244
267
  {signInLabel}
@@ -296,8 +319,7 @@ const ProfileButton: React.FC<ProfileButtonProps> = ({
296
319
  <View className={className} style={style}>
297
320
  <Pressable
298
321
  ref={triggerRef}
299
- className="rounded-full"
300
- style={collapsedBgStyle}
322
+ style={[styles.collapsedTrigger, collapsedBgStyle]}
301
323
  onPress={openMenu}
302
324
  accessibilityRole="button"
303
325
  accessibilityLabel={accountLabel}
@@ -329,7 +351,7 @@ const ProfileButton: React.FC<ProfileButtonProps> = ({
329
351
  // Slide the shrunk avatar left so its visual left edge stays put as it
330
352
  // contracts. The avatar loses `size * (1 - scale)` of width; half of that
331
353
  // (the left half, since scale is centered) is the offset, plus the row's
332
- // left padding (px-2 = 8px) so it hugs the row edge like Bluesky's.
354
+ // left padding (8px) so it hugs the row edge like Bluesky's.
333
355
  const activeAvatarTranslateX =
334
356
  -(resolvedAvatarSize * (1 - ACTIVE_AVATAR_SCALE)) / 2 - 8;
335
357
 
@@ -378,23 +400,28 @@ const ProfileButton: React.FC<ProfileButtonProps> = ({
378
400
  : undefined;
379
401
 
380
402
  return (
381
- <View className={`w-full ${className ?? ''}`} style={style}>
403
+ <View className={className} style={[styles.fullWidth, style]}>
382
404
  <Pressable
383
405
  ref={triggerRef}
384
- className="w-full flex-row items-center gap-3 rounded-full px-2 py-2"
385
- style={rowStyle}
406
+ style={[styles.row, styles.rowExpanded, rowStyle]}
386
407
  onPress={openMenu}
387
408
  accessibilityRole="button"
388
409
  accessibilityLabel={accountLabel}
389
410
  {...webInteractionProps}
390
411
  >
391
412
  <View style={avatarWrapperStyle}>{avatarNode}</View>
392
- <View className="min-w-0 flex-1" style={identityStyle}>
393
- <Text className="font-bold text-foreground" numberOfLines={1}>
413
+ <View style={[styles.identity, identityStyle]}>
414
+ <Text
415
+ style={[styles.displayName, { color: colors.text }]}
416
+ numberOfLines={1}
417
+ >
394
418
  {displayName}
395
419
  </Text>
396
420
  {handleLine ? (
397
- <Text className="text-xs text-muted-foreground" numberOfLines={1}>
421
+ <Text
422
+ style={[styles.handle, { color: colors.textSecondary }]}
423
+ numberOfLines={1}
424
+ >
398
425
  {handleLine}
399
426
  </Text>
400
427
  ) : null}
@@ -420,4 +447,55 @@ const ProfileButton: React.FC<ProfileButtonProps> = ({
420
447
  );
421
448
  };
422
449
 
450
+ const styles = StyleSheet.create({
451
+ fullWidth: {
452
+ width: '100%',
453
+ },
454
+ // Neutral skeleton / signed-out avatar circle (`rounded-full`).
455
+ skeletonCircle: {
456
+ borderRadius: 9999,
457
+ },
458
+ // Shared trigger row (`flex-row items-center gap-3 rounded-full px-2 py-2`).
459
+ row: {
460
+ flexDirection: 'row',
461
+ alignItems: 'center',
462
+ gap: 12,
463
+ borderRadius: 9999,
464
+ paddingHorizontal: 8,
465
+ paddingVertical: 8,
466
+ },
467
+ // `w-full` on the expanded row + its signed-out variant.
468
+ rowExpanded: {
469
+ width: '100%',
470
+ },
471
+ // Signed-out login badge (`items-center justify-center rounded-full`).
472
+ avatarBadge: {
473
+ alignItems: 'center',
474
+ justifyContent: 'center',
475
+ borderRadius: 9999,
476
+ },
477
+ // Signed-out label (`flex-1 font-semibold`).
478
+ signInLabel: {
479
+ flex: 1,
480
+ fontWeight: '600',
481
+ },
482
+ // Collapsed avatar-only trigger (`rounded-full`).
483
+ collapsedTrigger: {
484
+ borderRadius: 9999,
485
+ },
486
+ // Identity block (`min-w-0 flex-1`).
487
+ identity: {
488
+ flex: 1,
489
+ minWidth: 0,
490
+ },
491
+ // Display name (`font-bold`).
492
+ displayName: {
493
+ fontWeight: '700',
494
+ },
495
+ // Handle line (`text-xs`).
496
+ handle: {
497
+ fontSize: 12,
498
+ },
499
+ });
500
+
423
501
  export default ProfileButton;