@oxyhq/services 12.1.1 → 13.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 (150) hide show
  1. package/lib/commonjs/index.js +73 -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/components/SignInModal.js +9 -2
  10. package/lib/commonjs/ui/components/SignInModal.js.map +1 -1
  11. package/lib/commonjs/ui/context/OxyContext.js +72 -27
  12. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  13. package/lib/commonjs/ui/context/hooks/useAuthOperations.js +8 -0
  14. package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
  15. package/lib/commonjs/ui/index.js +42 -0
  16. package/lib/commonjs/ui/index.js.map +1 -1
  17. package/lib/commonjs/ui/navigation/routes.js +4 -1
  18. package/lib/commonjs/ui/navigation/routes.js.map +1 -1
  19. package/lib/commonjs/ui/screens/AccountMembersScreen.js +469 -0
  20. package/lib/commonjs/ui/screens/AccountMembersScreen.js.map +1 -0
  21. package/lib/commonjs/ui/screens/AccountSettingsScreen.js +264 -0
  22. package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -0
  23. package/lib/commonjs/ui/screens/AccountSwitcherScreen.js +51 -0
  24. package/lib/commonjs/ui/screens/AccountSwitcherScreen.js.map +1 -0
  25. package/lib/commonjs/ui/screens/{CreateManagedAccountScreen.js → CreateAccountScreen.js} +116 -25
  26. package/lib/commonjs/ui/screens/CreateAccountScreen.js.map +1 -0
  27. package/lib/commonjs/ui/screens/ManageAccountScreen.js +11 -11
  28. package/lib/commonjs/ui/screens/ManageAccountScreen.js.map +1 -1
  29. package/lib/commonjs/ui/screens/OxyAuthScreen.js +10 -2
  30. package/lib/commonjs/ui/screens/OxyAuthScreen.js.map +1 -1
  31. package/lib/commonjs/utils/crossApex.js +74 -0
  32. package/lib/commonjs/utils/crossApex.js.map +1 -0
  33. package/lib/module/index.js +21 -4
  34. package/lib/module/index.js.map +1 -1
  35. package/lib/module/ui/components/AccountMenuButton.js +26 -9
  36. package/lib/module/ui/components/AccountMenuButton.js.map +1 -1
  37. package/lib/module/ui/components/AccountSwitcher.js +786 -0
  38. package/lib/module/ui/components/AccountSwitcher.js.map +1 -0
  39. package/lib/module/ui/components/ActiveAccountBanner.js +141 -0
  40. package/lib/module/ui/components/ActiveAccountBanner.js.map +1 -0
  41. package/lib/module/ui/components/SignInModal.js +9 -2
  42. package/lib/module/ui/components/SignInModal.js.map +1 -1
  43. package/lib/module/ui/context/OxyContext.js +72 -27
  44. package/lib/module/ui/context/OxyContext.js.map +1 -1
  45. package/lib/module/ui/context/hooks/useAuthOperations.js +8 -0
  46. package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
  47. package/lib/module/ui/index.js +5 -0
  48. package/lib/module/ui/index.js.map +1 -1
  49. package/lib/module/ui/navigation/routes.js +4 -1
  50. package/lib/module/ui/navigation/routes.js.map +1 -1
  51. package/lib/module/ui/screens/AccountMembersScreen.js +464 -0
  52. package/lib/module/ui/screens/AccountMembersScreen.js.map +1 -0
  53. package/lib/module/ui/screens/AccountSettingsScreen.js +259 -0
  54. package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -0
  55. package/lib/module/ui/screens/AccountSwitcherScreen.js +48 -0
  56. package/lib/module/ui/screens/AccountSwitcherScreen.js.map +1 -0
  57. package/lib/module/ui/screens/{CreateManagedAccountScreen.js → CreateAccountScreen.js} +117 -26
  58. package/lib/module/ui/screens/CreateAccountScreen.js.map +1 -0
  59. package/lib/module/ui/screens/ManageAccountScreen.js +11 -11
  60. package/lib/module/ui/screens/ManageAccountScreen.js.map +1 -1
  61. package/lib/module/ui/screens/OxyAuthScreen.js +10 -2
  62. package/lib/module/ui/screens/OxyAuthScreen.js.map +1 -1
  63. package/lib/module/utils/crossApex.js +69 -0
  64. package/lib/module/utils/crossApex.js.map +1 -0
  65. package/lib/typescript/commonjs/index.d.ts +8 -1
  66. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  67. package/lib/typescript/commonjs/ui/components/AccountMenuButton.d.ts +13 -4
  68. package/lib/typescript/commonjs/ui/components/AccountMenuButton.d.ts.map +1 -1
  69. package/lib/typescript/commonjs/ui/components/AccountSwitcher.d.ts +47 -0
  70. package/lib/typescript/commonjs/ui/components/AccountSwitcher.d.ts.map +1 -0
  71. package/lib/typescript/commonjs/ui/components/{ActingAsBanner.d.ts → ActiveAccountBanner.d.ts} +1 -1
  72. package/lib/typescript/commonjs/ui/components/ActiveAccountBanner.d.ts.map +1 -0
  73. package/lib/typescript/commonjs/ui/components/SignInModal.d.ts.map +1 -1
  74. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +33 -5
  75. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
  76. package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  77. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  78. package/lib/typescript/commonjs/ui/index.d.ts +5 -0
  79. package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
  80. package/lib/typescript/commonjs/ui/navigation/routes.d.ts +1 -1
  81. package/lib/typescript/commonjs/ui/navigation/routes.d.ts.map +1 -1
  82. package/lib/typescript/commonjs/ui/screens/AccountMembersScreen.d.ts +10 -0
  83. package/lib/typescript/commonjs/ui/screens/AccountMembersScreen.d.ts.map +1 -0
  84. package/lib/typescript/commonjs/ui/screens/AccountSettingsScreen.d.ts +12 -0
  85. package/lib/typescript/commonjs/ui/screens/AccountSettingsScreen.d.ts.map +1 -0
  86. package/lib/typescript/commonjs/ui/screens/AccountSwitcherScreen.d.ts +11 -0
  87. package/lib/typescript/commonjs/ui/screens/AccountSwitcherScreen.d.ts.map +1 -0
  88. package/lib/typescript/commonjs/ui/screens/CreateAccountScreen.d.ts +11 -0
  89. package/lib/typescript/commonjs/ui/screens/CreateAccountScreen.d.ts.map +1 -0
  90. package/lib/typescript/commonjs/ui/screens/OxyAuthScreen.d.ts.map +1 -1
  91. package/lib/typescript/commonjs/utils/crossApex.d.ts +55 -0
  92. package/lib/typescript/commonjs/utils/crossApex.d.ts.map +1 -0
  93. package/lib/typescript/module/index.d.ts +8 -1
  94. package/lib/typescript/module/index.d.ts.map +1 -1
  95. package/lib/typescript/module/ui/components/AccountMenuButton.d.ts +13 -4
  96. package/lib/typescript/module/ui/components/AccountMenuButton.d.ts.map +1 -1
  97. package/lib/typescript/module/ui/components/AccountSwitcher.d.ts +47 -0
  98. package/lib/typescript/module/ui/components/AccountSwitcher.d.ts.map +1 -0
  99. package/lib/typescript/module/ui/components/{ActingAsBanner.d.ts → ActiveAccountBanner.d.ts} +1 -1
  100. package/lib/typescript/module/ui/components/ActiveAccountBanner.d.ts.map +1 -0
  101. package/lib/typescript/module/ui/components/SignInModal.d.ts.map +1 -1
  102. package/lib/typescript/module/ui/context/OxyContext.d.ts +33 -5
  103. package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
  104. package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  105. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  106. package/lib/typescript/module/ui/index.d.ts +5 -0
  107. package/lib/typescript/module/ui/index.d.ts.map +1 -1
  108. package/lib/typescript/module/ui/navigation/routes.d.ts +1 -1
  109. package/lib/typescript/module/ui/navigation/routes.d.ts.map +1 -1
  110. package/lib/typescript/module/ui/screens/AccountMembersScreen.d.ts +10 -0
  111. package/lib/typescript/module/ui/screens/AccountMembersScreen.d.ts.map +1 -0
  112. package/lib/typescript/module/ui/screens/AccountSettingsScreen.d.ts +12 -0
  113. package/lib/typescript/module/ui/screens/AccountSettingsScreen.d.ts.map +1 -0
  114. package/lib/typescript/module/ui/screens/AccountSwitcherScreen.d.ts +11 -0
  115. package/lib/typescript/module/ui/screens/AccountSwitcherScreen.d.ts.map +1 -0
  116. package/lib/typescript/module/ui/screens/CreateAccountScreen.d.ts +11 -0
  117. package/lib/typescript/module/ui/screens/CreateAccountScreen.d.ts.map +1 -0
  118. package/lib/typescript/module/ui/screens/OxyAuthScreen.d.ts.map +1 -1
  119. package/lib/typescript/module/utils/crossApex.d.ts +55 -0
  120. package/lib/typescript/module/utils/crossApex.d.ts.map +1 -0
  121. package/package.json +3 -3
  122. package/src/index.ts +22 -4
  123. package/src/ui/components/AccountMenuButton.tsx +25 -10
  124. package/src/ui/components/AccountSwitcher.tsx +763 -0
  125. package/src/ui/components/ActiveAccountBanner.tsx +130 -0
  126. package/src/ui/components/SignInModal.tsx +18 -6
  127. package/src/ui/context/OxyContext.tsx +121 -35
  128. package/src/ui/context/hooks/useAuthOperations.ts +8 -0
  129. package/src/ui/index.ts +5 -0
  130. package/src/ui/navigation/routes.ts +8 -2
  131. package/src/ui/screens/AccountMembersScreen.tsx +481 -0
  132. package/src/ui/screens/AccountSettingsScreen.tsx +261 -0
  133. package/src/ui/screens/AccountSwitcherScreen.tsx +43 -0
  134. package/src/ui/screens/{CreateManagedAccountScreen.tsx → CreateAccountScreen.tsx} +140 -22
  135. package/src/ui/screens/ManageAccountScreen.tsx +19 -19
  136. package/src/ui/screens/OxyAuthScreen.tsx +20 -6
  137. package/src/utils/crossApex.ts +75 -0
  138. package/lib/commonjs/ui/components/ActingAsBanner.js +0 -140
  139. package/lib/commonjs/ui/components/ActingAsBanner.js.map +0 -1
  140. package/lib/commonjs/ui/screens/CreateManagedAccountScreen.js.map +0 -1
  141. package/lib/module/ui/components/ActingAsBanner.js +0 -137
  142. package/lib/module/ui/components/ActingAsBanner.js.map +0 -1
  143. package/lib/module/ui/screens/CreateManagedAccountScreen.js.map +0 -1
  144. package/lib/typescript/commonjs/ui/components/ActingAsBanner.d.ts.map +0 -1
  145. package/lib/typescript/commonjs/ui/screens/CreateManagedAccountScreen.d.ts +0 -5
  146. package/lib/typescript/commonjs/ui/screens/CreateManagedAccountScreen.d.ts.map +0 -1
  147. package/lib/typescript/module/ui/components/ActingAsBanner.d.ts.map +0 -1
  148. package/lib/typescript/module/ui/screens/CreateManagedAccountScreen.d.ts +0 -5
  149. package/lib/typescript/module/ui/screens/CreateManagedAccountScreen.d.ts.map +0 -1
  150. 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);
@@ -30,6 +30,7 @@ import { useOxy } from '../context/OxyContext';
30
30
  import OxyLogo from './OxyLogo';
31
31
  import AnotherDeviceQR from './AnotherDeviceQR';
32
32
  import { useOxyAuthSession, OXY_ACCOUNTS_WEB_URL } from '../hooks/useOxyAuthSession';
33
+ import { isCrossApexWeb } from '../../utils/crossApex';
33
34
 
34
35
  // Store for modal visibility with subscription support
35
36
  let modalVisible = false;
@@ -110,6 +111,12 @@ const SignInModalContent: React.FC<SignInModalContentProps> = ({
110
111
  { onSignedIn: hideSignInModal },
111
112
  );
112
113
 
114
+ // On a cross-apex web RP, only the "Continue with Oxy" IdP popup establishes
115
+ // a durable `fedcm_session`. The Commons-app handoffs (same-device deep-link
116
+ // + cross-device QR) approve OUTSIDE the browser, so they leave no IdP
117
+ // session and the user would be logged out on reload — hide them there.
118
+ const crossApexWeb = isCrossApexWeb();
119
+
113
120
  // Entrance animation.
114
121
  const opacity = useSharedValue(0);
115
122
  const scale = useSharedValue(0.96);
@@ -194,8 +201,9 @@ const SignInModalContent: React.FC<SignInModalContentProps> = ({
194
201
 
195
202
  {/* Same-device "Sign in with Oxy" handoff — deep-links into the
196
203
  native Oxy app to approve. Shown only when the handoff backend
197
- returned a payload. */}
198
- {qrPayload && (
204
+ returned a payload, and never on a cross-apex web RP (the
205
+ approval happens outside the browser → no durable session). */}
206
+ {!crossApexWeb && qrPayload && (
199
207
  <Button
200
208
  variant="secondary"
201
209
  onPress={openSameDeviceApproval}
@@ -216,10 +224,14 @@ const SignInModalContent: React.FC<SignInModalContentProps> = ({
216
224
  </View>
217
225
  )}
218
226
 
219
- {/* Collapsed "sign in on another device" QR disclosure */}
220
- <View style={styles.qrSection}>
221
- <AnotherDeviceQR qrData={qrData} qrPayload={qrPayload} />
222
- </View>
227
+ {/* Collapsed "sign in on another device" QR disclosure. Hidden on
228
+ a cross-apex web RP: a remote Commons-app approval completes via
229
+ the device-flow claim, which plants no `fedcm_session`. */}
230
+ {!crossApexWeb && (
231
+ <View style={styles.qrSection}>
232
+ <AnotherDeviceQR qrData={qrData} qrPayload={qrPayload} />
233
+ </View>
234
+ )}
223
235
  </>
224
236
  )}
225
237
 
@@ -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 {
@@ -53,6 +53,7 @@ import { useAccountStore } from '../stores/accountStore';
53
53
  import { logger as loggerUtil } from '@oxyhq/core';
54
54
  import { useWebSSO, isWebBrowser } from '../hooks/useWebSSO';
55
55
  import { buildSilentGuardKey } from '../../utils/silentGuardKey';
56
+ import { isCrossApexWeb, CrossApexDirectSignInError } from '../../utils/crossApex';
56
57
  import { createInSessionRefreshHandler, startTokenRefreshScheduler } from './inSessionTokenRefresh';
57
58
  import { mintSessionViaPerApexIframe, selectActiveRefreshAccount } from './silentSessionRestore';
58
59
 
@@ -156,12 +157,48 @@ export interface OxyContextState {
156
157
  showBottomSheet?: (screenOrConfig: RouteName | { screen: RouteName; props?: Record<string, unknown> }) => void;
157
158
  openAvatarPicker: () => void;
158
159
 
159
- // 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
+ */
160
175
  actingAs: string | null;
161
- managedAccounts: ManagedAccount[];
162
- setActingAs: (userId: string | null) => void;
163
- refreshManagedAccounts: () => Promise<void>;
164
- 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>;
165
202
  }
166
203
 
167
204
  const OxyContext = createContext<OxyContextState | null>(null);
@@ -1776,6 +1813,13 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
1776
1813
  password: string,
1777
1814
  opts?: { deviceName?: string; deviceFingerprint?: string },
1778
1815
  ): Promise<PasswordSignInResult> => {
1816
+ // On a cross-apex web RP a direct password sign-in mints a bearer against
1817
+ // the Oxy API but establishes no `fedcm_session`, so the session would be
1818
+ // lost on reload. Refuse it and direct the app to the durable IdP popup
1819
+ // ("Continue with Oxy"). Native and same-apex `*.oxy.so` are unaffected.
1820
+ if (isCrossApexWeb()) {
1821
+ throw new CrossApexDirectSignInError();
1822
+ }
1779
1823
  const response = await oxyServices.signIn(
1780
1824
  identifier,
1781
1825
  password,
@@ -1973,9 +2017,18 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
1973
2017
  showBottomSheet: showBottomSheetForContext,
1974
2018
  });
1975
2019
 
1976
- // --- Managed accounts state ---
2020
+ // --- Account graph state ---
1977
2021
  const [actingAs, setActingAsState] = useState<string | null>(null);
1978
- 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);
1979
2032
 
1980
2033
  // Restore actingAs from storage on startup
1981
2034
  useEffect(() => {
@@ -1997,41 +2050,50 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
1997
2050
  return () => { mounted = false; };
1998
2051
  }, [storage, initialized, storageKeyPrefix, oxyServices]);
1999
2052
 
2000
- // Load managed accounts when authenticated
2001
- const refreshManagedAccounts = useCallback(async (): Promise<void> => {
2053
+ // Load the unified account graph when authenticated
2054
+ const refreshAccounts = useCallback(async (): Promise<void> => {
2002
2055
  if (!isAuthenticated || !tokenReady || !oxyServices.getAccessToken()) {
2003
- setManagedAccounts([]);
2056
+ setAccounts([]);
2004
2057
  return;
2005
2058
  }
2006
2059
 
2007
2060
  try {
2008
- const accounts = await oxyServices.getManagedAccounts();
2009
- 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
+ }
2010
2072
  } catch (err) {
2011
2073
  if (isUnauthorizedStatus(err)) {
2012
- setManagedAccounts([]);
2074
+ setAccounts([]);
2013
2075
  await clearSessionStateRef.current();
2014
2076
  return;
2015
2077
  }
2016
2078
  if (__DEV__) {
2017
- loggerUtil.debug('Failed to load managed accounts', { component: 'OxyContext' }, err as unknown);
2079
+ loggerUtil.debug('Failed to load accounts', { component: 'OxyContext' }, err as unknown);
2018
2080
  }
2019
2081
  }
2020
2082
  }, [isAuthenticated, oxyServices, tokenReady]);
2021
2083
 
2022
2084
  useEffect(() => {
2023
2085
  if (isAuthenticated && initialized && tokenReady) {
2024
- refreshManagedAccounts();
2086
+ refreshAccounts();
2025
2087
  }
2026
- }, [isAuthenticated, initialized, tokenReady, refreshManagedAccounts]);
2088
+ }, [isAuthenticated, initialized, tokenReady, refreshAccounts]);
2027
2089
 
2028
- const setActingAs = useCallback((userId: string | null) => {
2029
- oxyServices.setActingAs(userId);
2030
- setActingAsState(userId);
2090
+ const setActingAs = useCallback((accountId: string | null) => {
2091
+ oxyServices.setActingAs(accountId);
2092
+ setActingAsState(accountId);
2031
2093
  // Persist to storage
2032
2094
  if (storage) {
2033
- if (userId) {
2034
- storage.setItem(`${storageKeyPrefix}_acting_as`, userId).catch((persistError) => {
2095
+ if (accountId) {
2096
+ storage.setItem(`${storageKeyPrefix}_acting_as`, accountId).catch((persistError) => {
2035
2097
  loggerUtil.debug('Failed to persist acting-as account', { component: 'OxyContext' }, persistError as unknown);
2036
2098
  });
2037
2099
  } else {
@@ -2041,12 +2103,30 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
2041
2103
  }
2042
2104
  }
2043
2105
  }, [oxyServices, storage, storageKeyPrefix]);
2106
+ setActingAsRef.current = setActingAs;
2107
+
2108
+ // The account switched into, resolved from the loaded graph.
2109
+ const actingAsAccount = useMemo<AccountNode | null>(() => {
2110
+ if (!actingAs) return null;
2111
+ return accounts.find((node) => node.accountId === actingAs) ?? null;
2112
+ }, [actingAs, accounts]);
2113
+
2114
+ // The effective ACTIVE account presented across the app: the switched-into
2115
+ // account's user when a switch is set and resolved, otherwise the signed-in
2116
+ // user's own personal account. Derived (no effect) so identity surfaces always
2117
+ // render the current account without separate syncing. Falls back to `user`
2118
+ // while a just-set switch has not yet resolved in `accounts`, so the header
2119
+ // never flashes empty mid-switch.
2120
+ const activeAccount = useMemo<User | null>(
2121
+ () => (actingAs && actingAsAccount ? actingAsAccount.account : user),
2122
+ [actingAs, actingAsAccount, user],
2123
+ );
2044
2124
 
2045
- const createManagedAccountFn = useCallback(async (data: CreateManagedAccountInput): Promise<ManagedAccount> => {
2046
- const account = await oxyServices.createManagedAccount(data);
2047
- await refreshManagedAccounts();
2125
+ const createAccountFn = useCallback(async (data: CreateAccountInput): Promise<AccountNode> => {
2126
+ const account = await oxyServices.createAccount(data);
2127
+ await refreshAccounts();
2048
2128
  return account;
2049
- }, [oxyServices, refreshManagedAccounts]);
2129
+ }, [oxyServices, refreshAccounts]);
2050
2130
 
2051
2131
  const canUsePrivateApi = authResolved && isAuthenticated && tokenReady && hasAccessToken;
2052
2132
  const isPrivateApiPending = !authResolved || (isAuthenticated && (!tokenReady || !hasAccessToken));
@@ -2091,10 +2171,12 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
2091
2171
  showBottomSheet: showBottomSheetForContext,
2092
2172
  openAvatarPicker,
2093
2173
  actingAs,
2094
- managedAccounts,
2174
+ accounts,
2175
+ actingAsAccount,
2176
+ activeAccount,
2095
2177
  setActingAs,
2096
- refreshManagedAccounts,
2097
- createManagedAccount: createManagedAccountFn,
2178
+ refreshAccounts,
2179
+ createAccount: createAccountFn,
2098
2180
  }), [
2099
2181
  activeSessionId,
2100
2182
  signIn,
@@ -2136,10 +2218,12 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
2136
2218
  showBottomSheetForContext,
2137
2219
  openAvatarPicker,
2138
2220
  actingAs,
2139
- managedAccounts,
2221
+ accounts,
2222
+ actingAsAccount,
2223
+ activeAccount,
2140
2224
  setActingAs,
2141
- refreshManagedAccounts,
2142
- createManagedAccountFn,
2225
+ refreshAccounts,
2226
+ createAccountFn,
2143
2227
  ]);
2144
2228
 
2145
2229
  return (
@@ -2207,10 +2291,12 @@ const LOADING_STATE: OxyContextState = {
2207
2291
  oxyServices: LOADING_STATE_OXY_SERVICES,
2208
2292
  openAvatarPicker: () => {},
2209
2293
  actingAs: null,
2210
- managedAccounts: [],
2294
+ accounts: [],
2295
+ actingAsAccount: null,
2296
+ activeAccount: null,
2211
2297
  setActingAs: () => {},
2212
- refreshManagedAccounts: () => rejectMissingProvider<void>(),
2213
- createManagedAccount: () => rejectMissingProvider<ManagedAccount>(),
2298
+ refreshAccounts: () => rejectMissingProvider<void>(),
2299
+ createAccount: () => rejectMissingProvider<AccountNode>(),
2214
2300
  };
2215
2301
 
2216
2302
  export const useOxy = (): OxyContextState => {
@@ -10,6 +10,7 @@ import type { OxyServices } from '@oxyhq/core';
10
10
  import { SignatureService } from '@oxyhq/core';
11
11
  import { isWebBrowser } from '../../hooks/useWebSSO';
12
12
  import { clearActiveAuthuser, clearSsoBounceState } from '../../utils/activeAuthuser';
13
+ import { isCrossApexWeb, CrossApexDirectSignInError } from '../../../utils/crossApex';
13
14
 
14
15
  export interface UseAuthOperationsOptions {
15
16
  oxyServices: OxyServices;
@@ -203,6 +204,13 @@ export const useAuthOperations = ({
203
204
  */
204
205
  const signIn = useCallback(
205
206
  async (publicKey: string, deviceName?: string): Promise<User> => {
207
+ // On a cross-apex web RP a direct public-key sign-in mints a bearer against
208
+ // the Oxy API but establishes no `fedcm_session`, so the session would be
209
+ // lost on reload. Refuse it and direct the app to the durable IdP popup
210
+ // ("Continue with Oxy"). Native and same-apex `*.oxy.so` are unaffected.
211
+ if (isCrossApexWeb()) {
212
+ throw new CrossApexDirectSignInError();
213
+ }
206
214
  setAuthState({ isLoading: true, error: null });
207
215
 
208
216
  try {
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,