@oxyhq/services 12.2.0 → 13.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/lib/commonjs/index.js +56 -6
  2. package/lib/commonjs/index.js.map +1 -1
  3. package/lib/commonjs/ui/components/AccountMenuButton.js +26 -9
  4. package/lib/commonjs/ui/components/AccountMenuButton.js.map +1 -1
  5. package/lib/commonjs/ui/components/AccountSwitcher.js +792 -0
  6. package/lib/commonjs/ui/components/AccountSwitcher.js.map +1 -0
  7. package/lib/commonjs/ui/components/ActiveAccountBanner.js +144 -0
  8. package/lib/commonjs/ui/components/ActiveAccountBanner.js.map +1 -0
  9. package/lib/commonjs/ui/context/OxyContext.js +72 -28
  10. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  11. package/lib/commonjs/ui/index.js +42 -0
  12. package/lib/commonjs/ui/index.js.map +1 -1
  13. package/lib/commonjs/ui/navigation/routes.js +4 -1
  14. package/lib/commonjs/ui/navigation/routes.js.map +1 -1
  15. package/lib/commonjs/ui/screens/AccountMembersScreen.js +469 -0
  16. package/lib/commonjs/ui/screens/AccountMembersScreen.js.map +1 -0
  17. package/lib/commonjs/ui/screens/AccountSettingsScreen.js +264 -0
  18. package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -0
  19. package/lib/commonjs/ui/screens/AccountSwitcherScreen.js +51 -0
  20. package/lib/commonjs/ui/screens/AccountSwitcherScreen.js.map +1 -0
  21. package/lib/commonjs/ui/screens/{CreateManagedAccountScreen.js → CreateAccountScreen.js} +116 -25
  22. package/lib/commonjs/ui/screens/CreateAccountScreen.js.map +1 -0
  23. package/lib/commonjs/ui/screens/ManageAccountScreen.js +11 -11
  24. package/lib/commonjs/ui/screens/ManageAccountScreen.js.map +1 -1
  25. package/lib/module/index.js +16 -4
  26. package/lib/module/index.js.map +1 -1
  27. package/lib/module/ui/components/AccountMenuButton.js +26 -9
  28. package/lib/module/ui/components/AccountMenuButton.js.map +1 -1
  29. package/lib/module/ui/components/AccountSwitcher.js +786 -0
  30. package/lib/module/ui/components/AccountSwitcher.js.map +1 -0
  31. package/lib/module/ui/components/ActiveAccountBanner.js +141 -0
  32. package/lib/module/ui/components/ActiveAccountBanner.js.map +1 -0
  33. package/lib/module/ui/context/OxyContext.js +72 -28
  34. package/lib/module/ui/context/OxyContext.js.map +1 -1
  35. package/lib/module/ui/index.js +5 -0
  36. package/lib/module/ui/index.js.map +1 -1
  37. package/lib/module/ui/navigation/routes.js +4 -1
  38. package/lib/module/ui/navigation/routes.js.map +1 -1
  39. package/lib/module/ui/screens/AccountMembersScreen.js +464 -0
  40. package/lib/module/ui/screens/AccountMembersScreen.js.map +1 -0
  41. package/lib/module/ui/screens/AccountSettingsScreen.js +259 -0
  42. package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -0
  43. package/lib/module/ui/screens/AccountSwitcherScreen.js +48 -0
  44. package/lib/module/ui/screens/AccountSwitcherScreen.js.map +1 -0
  45. package/lib/module/ui/screens/{CreateManagedAccountScreen.js → CreateAccountScreen.js} +117 -26
  46. package/lib/module/ui/screens/CreateAccountScreen.js.map +1 -0
  47. package/lib/module/ui/screens/ManageAccountScreen.js +11 -11
  48. package/lib/module/ui/screens/ManageAccountScreen.js.map +1 -1
  49. package/lib/typescript/commonjs/index.d.ts +7 -1
  50. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  51. package/lib/typescript/commonjs/ui/components/AccountMenuButton.d.ts +13 -4
  52. package/lib/typescript/commonjs/ui/components/AccountMenuButton.d.ts.map +1 -1
  53. package/lib/typescript/commonjs/ui/components/AccountSwitcher.d.ts +47 -0
  54. package/lib/typescript/commonjs/ui/components/AccountSwitcher.d.ts.map +1 -0
  55. package/lib/typescript/commonjs/ui/components/{ActingAsBanner.d.ts → ActiveAccountBanner.d.ts} +1 -1
  56. package/lib/typescript/commonjs/ui/components/ActiveAccountBanner.d.ts.map +1 -0
  57. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +33 -5
  58. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
  59. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  60. package/lib/typescript/commonjs/ui/index.d.ts +5 -0
  61. package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
  62. package/lib/typescript/commonjs/ui/navigation/routes.d.ts +1 -1
  63. package/lib/typescript/commonjs/ui/navigation/routes.d.ts.map +1 -1
  64. package/lib/typescript/commonjs/ui/screens/AccountMembersScreen.d.ts +10 -0
  65. package/lib/typescript/commonjs/ui/screens/AccountMembersScreen.d.ts.map +1 -0
  66. package/lib/typescript/commonjs/ui/screens/AccountSettingsScreen.d.ts +12 -0
  67. package/lib/typescript/commonjs/ui/screens/AccountSettingsScreen.d.ts.map +1 -0
  68. package/lib/typescript/commonjs/ui/screens/AccountSwitcherScreen.d.ts +11 -0
  69. package/lib/typescript/commonjs/ui/screens/AccountSwitcherScreen.d.ts.map +1 -0
  70. package/lib/typescript/commonjs/ui/screens/CreateAccountScreen.d.ts +11 -0
  71. package/lib/typescript/commonjs/ui/screens/CreateAccountScreen.d.ts.map +1 -0
  72. package/lib/typescript/module/index.d.ts +7 -1
  73. package/lib/typescript/module/index.d.ts.map +1 -1
  74. package/lib/typescript/module/ui/components/AccountMenuButton.d.ts +13 -4
  75. package/lib/typescript/module/ui/components/AccountMenuButton.d.ts.map +1 -1
  76. package/lib/typescript/module/ui/components/AccountSwitcher.d.ts +47 -0
  77. package/lib/typescript/module/ui/components/AccountSwitcher.d.ts.map +1 -0
  78. package/lib/typescript/module/ui/components/{ActingAsBanner.d.ts → ActiveAccountBanner.d.ts} +1 -1
  79. package/lib/typescript/module/ui/components/ActiveAccountBanner.d.ts.map +1 -0
  80. package/lib/typescript/module/ui/context/OxyContext.d.ts +33 -5
  81. package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
  82. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  83. package/lib/typescript/module/ui/index.d.ts +5 -0
  84. package/lib/typescript/module/ui/index.d.ts.map +1 -1
  85. package/lib/typescript/module/ui/navigation/routes.d.ts +1 -1
  86. package/lib/typescript/module/ui/navigation/routes.d.ts.map +1 -1
  87. package/lib/typescript/module/ui/screens/AccountMembersScreen.d.ts +10 -0
  88. package/lib/typescript/module/ui/screens/AccountMembersScreen.d.ts.map +1 -0
  89. package/lib/typescript/module/ui/screens/AccountSettingsScreen.d.ts +12 -0
  90. package/lib/typescript/module/ui/screens/AccountSettingsScreen.d.ts.map +1 -0
  91. package/lib/typescript/module/ui/screens/AccountSwitcherScreen.d.ts +11 -0
  92. package/lib/typescript/module/ui/screens/AccountSwitcherScreen.d.ts.map +1 -0
  93. package/lib/typescript/module/ui/screens/CreateAccountScreen.d.ts +11 -0
  94. package/lib/typescript/module/ui/screens/CreateAccountScreen.d.ts.map +1 -0
  95. package/package.json +3 -3
  96. package/src/index.ts +18 -4
  97. package/src/ui/components/AccountMenuButton.tsx +25 -10
  98. package/src/ui/components/AccountSwitcher.tsx +763 -0
  99. package/src/ui/components/ActiveAccountBanner.tsx +130 -0
  100. package/src/ui/context/OxyContext.tsx +122 -37
  101. package/src/ui/index.ts +5 -0
  102. package/src/ui/navigation/routes.ts +8 -2
  103. package/src/ui/screens/AccountMembersScreen.tsx +481 -0
  104. package/src/ui/screens/AccountSettingsScreen.tsx +261 -0
  105. package/src/ui/screens/AccountSwitcherScreen.tsx +43 -0
  106. package/src/ui/screens/{CreateManagedAccountScreen.tsx → CreateAccountScreen.tsx} +140 -22
  107. package/src/ui/screens/ManageAccountScreen.tsx +19 -19
  108. package/lib/commonjs/ui/components/ActingAsBanner.js +0 -140
  109. package/lib/commonjs/ui/components/ActingAsBanner.js.map +0 -1
  110. package/lib/commonjs/ui/screens/CreateManagedAccountScreen.js.map +0 -1
  111. package/lib/module/ui/components/ActingAsBanner.js +0 -137
  112. package/lib/module/ui/components/ActingAsBanner.js.map +0 -1
  113. package/lib/module/ui/screens/CreateManagedAccountScreen.js.map +0 -1
  114. package/lib/typescript/commonjs/ui/components/ActingAsBanner.d.ts.map +0 -1
  115. package/lib/typescript/commonjs/ui/screens/CreateManagedAccountScreen.d.ts +0 -5
  116. package/lib/typescript/commonjs/ui/screens/CreateManagedAccountScreen.d.ts.map +0 -1
  117. package/lib/typescript/module/ui/components/ActingAsBanner.d.ts.map +0 -1
  118. package/lib/typescript/module/ui/screens/CreateManagedAccountScreen.d.ts +0 -5
  119. package/lib/typescript/module/ui/screens/CreateManagedAccountScreen.d.ts.map +0 -1
  120. package/src/ui/components/ActingAsBanner.tsx +0 -129
@@ -0,0 +1,130 @@
1
+ import React from 'react';
2
+ import {
3
+ View,
4
+ Text,
5
+ TouchableOpacity,
6
+ StyleSheet,
7
+ Platform,
8
+ Image,
9
+ } from 'react-native';
10
+ import { Ionicons } from '@expo/vector-icons';
11
+ import { useTheme } from '@oxyhq/bloom/theme';
12
+ import { useOxy } from '../context/OxyContext';
13
+ import { getAccountDisplayName } from '@oxyhq/core';
14
+ import { useI18n } from '../hooks/useI18n';
15
+
16
+ /**
17
+ * ActiveAccountBanner — a subtle context cue shown when the active account is an
18
+ * account the user switched INTO (an org / project / bot / shared account)
19
+ * rather than their own personal account.
20
+ *
21
+ * Framing: this reads as the CURRENT account, NOT as delegation. There is no
22
+ * "acting as" / "on behalf of" copy and no "switch back" affordance — switching
23
+ * into an account makes the whole app become that account, and the banner simply
24
+ * confirms which account is active. To change accounts (including returning to
25
+ * the personal account) the user opens the unified account switcher and picks
26
+ * one; tapping the banner opens it.
27
+ *
28
+ * Renders nothing on the personal account. Place it in your app's layout where a
29
+ * persistent "you're in <Account>" cue is useful (typically below the header).
30
+ * The "account" here is the relational Account, NOT the cryptographic
31
+ * Commons/DID identity.
32
+ */
33
+ const ActiveAccountBanner: React.FC = () => {
34
+ const bloomTheme = useTheme();
35
+ const { actingAsAccount, showBottomSheet, oxyServices } = useOxy();
36
+ const { t, locale } = useI18n();
37
+
38
+ const account = actingAsAccount?.account ?? null;
39
+
40
+ // Only a switched-into account warrants the cue; the personal account is the
41
+ // default and needs no banner.
42
+ if (!account) {
43
+ return null;
44
+ }
45
+
46
+ const displayName = getAccountDisplayName(account, locale);
47
+
48
+ const handlePress = () => {
49
+ showBottomSheet?.('AccountSwitcher');
50
+ };
51
+
52
+ return (
53
+ <TouchableOpacity
54
+ style={[styles.container, { backgroundColor: `${bloomTheme.colors.primary}14` }]}
55
+ onPress={handlePress}
56
+ activeOpacity={0.7}
57
+ accessibilityRole="button"
58
+ accessibilityLabel={
59
+ t('accounts.activeAccount.a11y', { name: displayName })
60
+ || `Active account: ${displayName}. Tap to switch accounts.`
61
+ }
62
+ >
63
+ <View style={styles.content}>
64
+ {account.avatar ? (
65
+ <Image
66
+ source={{ uri: oxyServices.getFileDownloadUrl(account.avatar, 'thumb') }}
67
+ style={styles.avatar}
68
+ />
69
+ ) : (
70
+ <View style={[styles.avatarFallback, { backgroundColor: `${bloomTheme.colors.primary}30` }]}>
71
+ <Text style={[styles.avatarText, { color: bloomTheme.colors.primary }]}>
72
+ {displayName.charAt(0).toUpperCase()}
73
+ </Text>
74
+ </View>
75
+ )}
76
+ <View style={styles.textContainer}>
77
+ <Text style={[styles.name, { color: bloomTheme.colors.primary }]} numberOfLines={1}>
78
+ {displayName}
79
+ </Text>
80
+ <Text style={[styles.caption, { color: bloomTheme.colors.primary }]} numberOfLines={1}>
81
+ {t('accounts.activeAccount.label') || 'Active account'}
82
+ </Text>
83
+ </View>
84
+ <Ionicons name="chevron-down" size={18} color={bloomTheme.colors.primary} />
85
+ </View>
86
+ </TouchableOpacity>
87
+ );
88
+ };
89
+
90
+ const styles = StyleSheet.create({
91
+ container: {
92
+ paddingHorizontal: 16,
93
+ paddingVertical: 10,
94
+ },
95
+ content: {
96
+ flexDirection: 'row',
97
+ alignItems: 'center',
98
+ },
99
+ avatar: {
100
+ width: 24,
101
+ height: 24,
102
+ borderRadius: 12,
103
+ },
104
+ avatarFallback: {
105
+ width: 24,
106
+ height: 24,
107
+ borderRadius: 12,
108
+ alignItems: 'center',
109
+ justifyContent: 'center',
110
+ },
111
+ avatarText: {
112
+ fontSize: 12,
113
+ fontWeight: Platform.OS === 'web' ? '600' : undefined,
114
+ },
115
+ textContainer: {
116
+ flex: 1,
117
+ marginLeft: 10,
118
+ },
119
+ name: {
120
+ fontSize: 14,
121
+ fontWeight: Platform.OS === 'web' ? '600' : undefined,
122
+ },
123
+ caption: {
124
+ fontSize: 11,
125
+ opacity: 0.8,
126
+ marginTop: 1,
127
+ },
128
+ });
129
+
130
+ export default React.memo(ActiveAccountBanner);
@@ -12,7 +12,7 @@ import {
12
12
  } from 'react';
13
13
  import { OxyServices, oxyClient } from '@oxyhq/core';
14
14
  import type { User, ApiError, SessionLoginResponse } from '@oxyhq/core';
15
- import type { ManagedAccount, CreateManagedAccountInput } from '@oxyhq/core';
15
+ import type { AccountNode, CreateAccountInput } from '@oxyhq/core';
16
16
  import { KeyManager } from '@oxyhq/core';
17
17
  import type { ClientSession } from '@oxyhq/core';
18
18
  import {
@@ -157,12 +157,48 @@ export interface OxyContextState {
157
157
  showBottomSheet?: (screenOrConfig: RouteName | { screen: RouteName; props?: Record<string, unknown> }) => void;
158
158
  openAvatarPicker: () => void;
159
159
 
160
- // Managed accounts (sub-accounts / managed identities)
160
+ // Unified account graph (self, owned orgs/projects/bots, accounts shared with
161
+ // the caller). The cryptographic Commons/DID "identity" is a SEPARATE concept.
162
+ //
163
+ // UX concept: the user picks an account and the WHOLE app becomes that account
164
+ // — a genuine switch, NOT an "acting on behalf of" delegation. `actingAs` +
165
+ // `X-Acting-As` remain the underlying transport (the only one that works for
166
+ // passwordless org/project/bot accounts), but the framing everywhere is simply
167
+ // "this is the active account". Read {@link activeAccount} for "who am I"
168
+ // surfaces; `actingAs` is the low-level mechanism state.
169
+ /**
170
+ * The id of the account switched INTO (`X-Acting-As`), or `null` when the
171
+ * active account is the sign-in's own personal account. This is the underlying
172
+ * mechanism state — UI should present the result through {@link activeAccount}
173
+ * rather than framing it as delegation.
174
+ */
161
175
  actingAs: string | null;
162
- managedAccounts: ManagedAccount[];
163
- setActingAs: (userId: string | null) => void;
164
- refreshManagedAccounts: () => Promise<void>;
165
- createManagedAccount: (data: CreateManagedAccountInput) => Promise<ManagedAccount>;
176
+ /** Every account the caller can access — own personal root, owned, and shared — from `listAccounts()`. */
177
+ accounts: AccountNode[];
178
+ /**
179
+ * The {@link AccountNode} switched into, resolved from `accounts` by
180
+ * `actingAs`. `null` when the active account is the sign-in's own personal
181
+ * account, or while the switched-into id has not yet appeared in the loaded
182
+ * `accounts` list.
183
+ */
184
+ actingAsAccount: AccountNode | null;
185
+ /**
186
+ * The effective ACTIVE account presented across the whole app — the account
187
+ * the user switched into when one is set and resolved, otherwise the signed-in
188
+ * user's own personal account. This is the single "who am I" source every
189
+ * identity surface (header avatar/name, profile chrome, context cues) should
190
+ * read, so a switch is reflected everywhere as a real account change.
191
+ *
192
+ * It is a {@link User} (the personal user, or the switched-into account's
193
+ * embedded user) so every identity surface renders it identically. Falls back
194
+ * to the personal `user` during the brief window after a switch before
195
+ * `accounts` has loaded the switched-into node, so the header never flashes
196
+ * empty. `null` only when signed out.
197
+ */
198
+ activeAccount: User | null;
199
+ setActingAs: (accountId: string | null) => void;
200
+ refreshAccounts: () => Promise<void>;
201
+ createAccount: (data: CreateAccountInput) => Promise<AccountNode>;
166
202
  }
167
203
 
168
204
  const OxyContext = createContext<OxyContextState | null>(null);
@@ -1981,9 +2017,18 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
1981
2017
  showBottomSheet: showBottomSheetForContext,
1982
2018
  });
1983
2019
 
1984
- // --- Managed accounts state ---
2020
+ // --- Account graph state ---
1985
2021
  const [actingAs, setActingAsState] = useState<string | null>(null);
1986
- const [managedAccounts, setManagedAccounts] = useState<ManagedAccount[]>([]);
2022
+ const [accounts, setAccounts] = useState<AccountNode[]>([]);
2023
+
2024
+ // Latest `actingAs`, mirrored into a ref so `refreshAccounts` can reconcile a
2025
+ // stale switch without taking `actingAs` as a dependency (which would re-run
2026
+ // the account load on every switch). See the reconciliation block below.
2027
+ const actingAsRef = useRef(actingAs);
2028
+ actingAsRef.current = actingAs;
2029
+ // `setActingAs` is declared after `refreshAccounts`; route the reconciliation
2030
+ // clear through a ref so the load callback can call the latest implementation.
2031
+ const setActingAsRef = useRef<((accountId: string | null) => void) | null>(null);
1987
2032
 
1988
2033
  // Restore actingAs from storage on startup
1989
2034
  useEffect(() => {
@@ -2005,41 +2050,57 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
2005
2050
  return () => { mounted = false; };
2006
2051
  }, [storage, initialized, storageKeyPrefix, oxyServices]);
2007
2052
 
2008
- // Load managed accounts when authenticated
2009
- const refreshManagedAccounts = useCallback(async (): Promise<void> => {
2053
+ // Load the unified account graph when authenticated
2054
+ const refreshAccounts = useCallback(async (): Promise<void> => {
2010
2055
  if (!isAuthenticated || !tokenReady || !oxyServices.getAccessToken()) {
2011
- setManagedAccounts([]);
2056
+ setAccounts([]);
2012
2057
  return;
2013
2058
  }
2014
2059
 
2015
2060
  try {
2016
- const accounts = await oxyServices.getManagedAccounts();
2017
- setManagedAccounts(accounts);
2061
+ const list = await oxyServices.listAccounts();
2062
+ setAccounts(list);
2063
+ // Reconcile a stale switch: if the active account was switched INTO an id
2064
+ // that is no longer accessible (account removed/archived, or it belonged
2065
+ // to a now-inactive sign-in), drop back to the personal account so the
2066
+ // app never keeps sending a dead `X-Acting-As` header. `list` is the
2067
+ // authoritative accessible set on a successful fetch.
2068
+ const current = actingAsRef.current;
2069
+ if (current && !list.some((node) => node.accountId === current)) {
2070
+ setActingAsRef.current?.(null);
2071
+ }
2018
2072
  } catch (err) {
2019
2073
  if (isUnauthorizedStatus(err)) {
2020
- setManagedAccounts([]);
2074
+ setAccounts([]);
2021
2075
  await clearSessionStateRef.current();
2022
2076
  return;
2023
2077
  }
2024
2078
  if (__DEV__) {
2025
- loggerUtil.debug('Failed to load managed accounts', { component: 'OxyContext' }, err as unknown);
2079
+ loggerUtil.debug('Failed to load accounts', { component: 'OxyContext' }, err as unknown);
2026
2080
  }
2027
2081
  }
2028
2082
  }, [isAuthenticated, oxyServices, tokenReady]);
2029
2083
 
2030
2084
  useEffect(() => {
2031
2085
  if (isAuthenticated && initialized && tokenReady) {
2032
- refreshManagedAccounts();
2086
+ refreshAccounts();
2033
2087
  }
2034
- }, [isAuthenticated, initialized, tokenReady, refreshManagedAccounts]);
2035
-
2036
- const setActingAs = useCallback((userId: string | null) => {
2037
- oxyServices.setActingAs(userId);
2038
- setActingAsState(userId);
2088
+ }, [isAuthenticated, initialized, tokenReady, refreshAccounts]);
2089
+
2090
+ const setActingAs = useCallback((accountId: string | null) => {
2091
+ oxyServices.setActingAs(accountId);
2092
+ setActingAsState(accountId);
2093
+ // Switching the acting-as identity changes every server response, but React
2094
+ // Query keys are not namespaced by the acting-as target, so the cache would
2095
+ // otherwise keep serving the previous account's data. Invalidate everything
2096
+ // so each query refetches under the new identity (the next request carries
2097
+ // the updated `X-Acting-As` header). `invalidateQueries` (not `clear`) keeps
2098
+ // the account list/switcher populated mid-switch instead of blanking out.
2099
+ queryClient.invalidateQueries();
2039
2100
  // Persist to storage
2040
2101
  if (storage) {
2041
- if (userId) {
2042
- storage.setItem(`${storageKeyPrefix}_acting_as`, userId).catch((persistError) => {
2102
+ if (accountId) {
2103
+ storage.setItem(`${storageKeyPrefix}_acting_as`, accountId).catch((persistError) => {
2043
2104
  loggerUtil.debug('Failed to persist acting-as account', { component: 'OxyContext' }, persistError as unknown);
2044
2105
  });
2045
2106
  } else {
@@ -2048,13 +2109,31 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
2048
2109
  });
2049
2110
  }
2050
2111
  }
2051
- }, [oxyServices, storage, storageKeyPrefix]);
2112
+ }, [oxyServices, storage, storageKeyPrefix, queryClient]);
2113
+ setActingAsRef.current = setActingAs;
2114
+
2115
+ // The account switched into, resolved from the loaded graph.
2116
+ const actingAsAccount = useMemo<AccountNode | null>(() => {
2117
+ if (!actingAs) return null;
2118
+ return accounts.find((node) => node.accountId === actingAs) ?? null;
2119
+ }, [actingAs, accounts]);
2120
+
2121
+ // The effective ACTIVE account presented across the app: the switched-into
2122
+ // account's user when a switch is set and resolved, otherwise the signed-in
2123
+ // user's own personal account. Derived (no effect) so identity surfaces always
2124
+ // render the current account without separate syncing. Falls back to `user`
2125
+ // while a just-set switch has not yet resolved in `accounts`, so the header
2126
+ // never flashes empty mid-switch.
2127
+ const activeAccount = useMemo<User | null>(
2128
+ () => (actingAs && actingAsAccount ? actingAsAccount.account : user),
2129
+ [actingAs, actingAsAccount, user],
2130
+ );
2052
2131
 
2053
- const createManagedAccountFn = useCallback(async (data: CreateManagedAccountInput): Promise<ManagedAccount> => {
2054
- const account = await oxyServices.createManagedAccount(data);
2055
- await refreshManagedAccounts();
2132
+ const createAccountFn = useCallback(async (data: CreateAccountInput): Promise<AccountNode> => {
2133
+ const account = await oxyServices.createAccount(data);
2134
+ await refreshAccounts();
2056
2135
  return account;
2057
- }, [oxyServices, refreshManagedAccounts]);
2136
+ }, [oxyServices, refreshAccounts]);
2058
2137
 
2059
2138
  const canUsePrivateApi = authResolved && isAuthenticated && tokenReady && hasAccessToken;
2060
2139
  const isPrivateApiPending = !authResolved || (isAuthenticated && (!tokenReady || !hasAccessToken));
@@ -2099,10 +2178,12 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
2099
2178
  showBottomSheet: showBottomSheetForContext,
2100
2179
  openAvatarPicker,
2101
2180
  actingAs,
2102
- managedAccounts,
2181
+ accounts,
2182
+ actingAsAccount,
2183
+ activeAccount,
2103
2184
  setActingAs,
2104
- refreshManagedAccounts,
2105
- createManagedAccount: createManagedAccountFn,
2185
+ refreshAccounts,
2186
+ createAccount: createAccountFn,
2106
2187
  }), [
2107
2188
  activeSessionId,
2108
2189
  signIn,
@@ -2144,10 +2225,12 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
2144
2225
  showBottomSheetForContext,
2145
2226
  openAvatarPicker,
2146
2227
  actingAs,
2147
- managedAccounts,
2228
+ accounts,
2229
+ actingAsAccount,
2230
+ activeAccount,
2148
2231
  setActingAs,
2149
- refreshManagedAccounts,
2150
- createManagedAccountFn,
2232
+ refreshAccounts,
2233
+ createAccountFn,
2151
2234
  ]);
2152
2235
 
2153
2236
  return (
@@ -2215,10 +2298,12 @@ const LOADING_STATE: OxyContextState = {
2215
2298
  oxyServices: LOADING_STATE_OXY_SERVICES,
2216
2299
  openAvatarPicker: () => {},
2217
2300
  actingAs: null,
2218
- managedAccounts: [],
2301
+ accounts: [],
2302
+ actingAsAccount: null,
2303
+ activeAccount: null,
2219
2304
  setActingAs: () => {},
2220
- refreshManagedAccounts: () => rejectMissingProvider<void>(),
2221
- createManagedAccount: () => rejectMissingProvider<ManagedAccount>(),
2305
+ refreshAccounts: () => rejectMissingProvider<void>(),
2306
+ createAccount: () => rejectMissingProvider<AccountNode>(),
2222
2307
  };
2223
2308
 
2224
2309
  export const useOxy = (): OxyContextState => {
package/src/ui/index.ts CHANGED
@@ -27,6 +27,7 @@ export { FontLoader, setupFonts } from './components/FontLoader';
27
27
  export { default as OxyIcon } from './components/icon/OxyIcon';
28
28
  export { default as AccountMenu } from './components/AccountMenu';
29
29
  export { default as AccountMenuButton } from './components/AccountMenuButton';
30
+ export { default as AccountSwitcher, AccountSwitcherView } from './components/AccountSwitcher';
30
31
 
31
32
  // Context + hooks
32
33
  export { useOxy } from './context/OxyContext';
@@ -38,6 +39,10 @@ export type { UseStorageOptions, UseStorageResult } from './hooks/useStorage';
38
39
  // Screens
39
40
  export { default as ProfileScreen } from './screens/ProfileScreen';
40
41
  export { default as ManageAccountScreen } from './screens/ManageAccountScreen';
42
+ export { default as AccountSwitcherScreen } from './screens/AccountSwitcherScreen';
43
+ export { default as CreateAccountScreen } from './screens/CreateAccountScreen';
44
+ export { default as AccountMembersScreen } from './screens/AccountMembersScreen';
45
+ export { default as AccountSettingsScreen } from './screens/AccountSettingsScreen';
41
46
 
42
47
  // Stores
43
48
  export { useAuthStore } from './stores/authStore';
@@ -36,7 +36,10 @@ export type RouteName =
36
36
  | 'TrustFAQ'
37
37
  | 'FollowersList' // List of user's followers
38
38
  | 'FollowingList' // List of users being followed
39
- | 'CreateManagedAccount' // Create a new managed sub-account
39
+ | 'AccountSwitcher' // Unified account switcher (device sign-ins + account graph)
40
+ | 'CreateAccount' // Create a new account (organization / project / bot)
41
+ | 'AccountMembers' // Manage an account's members (invite / roles / transfer)
42
+ | 'AccountSettings' // Per-account profile edit + members + danger zone
40
43
  | 'AvatarCrop' // Square-crop editor presented before avatar upload
41
44
  | 'Notifications' // Per-channel notification preferences
42
45
  | 'ConnectedApps' // FedCM-authorized RP apps the user can revoke
@@ -77,7 +80,10 @@ const screenLoaders: Record<RouteName, () => ComponentType<BaseScreenProps>> = {
77
80
  // User list screens (followers/following)
78
81
  FollowersList: () => require('../screens/FollowersListScreen').default,
79
82
  FollowingList: () => require('../screens/FollowingListScreen').default,
80
- CreateManagedAccount: () => require('../screens/CreateManagedAccountScreen').default,
83
+ AccountSwitcher: () => require('../screens/AccountSwitcherScreen').default,
84
+ CreateAccount: () => require('../screens/CreateAccountScreen').default,
85
+ AccountMembers: () => require('../screens/AccountMembersScreen').default,
86
+ AccountSettings: () => require('../screens/AccountSettingsScreen').default,
81
87
  AvatarCrop: () => require('../screens/AvatarCropScreen').default,
82
88
  Notifications: () => require('../screens/NotificationsScreen').default,
83
89
  ConnectedApps: () => require('../screens/ConnectedAppsScreen').default,