@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.
- package/lib/commonjs/index.js +73 -6
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/components/AccountMenuButton.js +26 -9
- package/lib/commonjs/ui/components/AccountMenuButton.js.map +1 -1
- package/lib/commonjs/ui/components/AccountSwitcher.js +792 -0
- package/lib/commonjs/ui/components/AccountSwitcher.js.map +1 -0
- package/lib/commonjs/ui/components/ActiveAccountBanner.js +144 -0
- package/lib/commonjs/ui/components/ActiveAccountBanner.js.map +1 -0
- package/lib/commonjs/ui/components/SignInModal.js +9 -2
- package/lib/commonjs/ui/components/SignInModal.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +72 -27
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js +8 -0
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/commonjs/ui/index.js +42 -0
- package/lib/commonjs/ui/index.js.map +1 -1
- package/lib/commonjs/ui/navigation/routes.js +4 -1
- package/lib/commonjs/ui/navigation/routes.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountMembersScreen.js +469 -0
- package/lib/commonjs/ui/screens/AccountMembersScreen.js.map +1 -0
- package/lib/commonjs/ui/screens/AccountSettingsScreen.js +264 -0
- package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -0
- package/lib/commonjs/ui/screens/AccountSwitcherScreen.js +51 -0
- package/lib/commonjs/ui/screens/AccountSwitcherScreen.js.map +1 -0
- package/lib/commonjs/ui/screens/{CreateManagedAccountScreen.js → CreateAccountScreen.js} +116 -25
- package/lib/commonjs/ui/screens/CreateAccountScreen.js.map +1 -0
- package/lib/commonjs/ui/screens/ManageAccountScreen.js +11 -11
- package/lib/commonjs/ui/screens/ManageAccountScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/OxyAuthScreen.js +10 -2
- package/lib/commonjs/ui/screens/OxyAuthScreen.js.map +1 -1
- package/lib/commonjs/utils/crossApex.js +74 -0
- package/lib/commonjs/utils/crossApex.js.map +1 -0
- package/lib/module/index.js +21 -4
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/components/AccountMenuButton.js +26 -9
- package/lib/module/ui/components/AccountMenuButton.js.map +1 -1
- package/lib/module/ui/components/AccountSwitcher.js +786 -0
- package/lib/module/ui/components/AccountSwitcher.js.map +1 -0
- package/lib/module/ui/components/ActiveAccountBanner.js +141 -0
- package/lib/module/ui/components/ActiveAccountBanner.js.map +1 -0
- package/lib/module/ui/components/SignInModal.js +9 -2
- package/lib/module/ui/components/SignInModal.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +72 -27
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/context/hooks/useAuthOperations.js +8 -0
- package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/module/ui/index.js +5 -0
- package/lib/module/ui/index.js.map +1 -1
- package/lib/module/ui/navigation/routes.js +4 -1
- package/lib/module/ui/navigation/routes.js.map +1 -1
- package/lib/module/ui/screens/AccountMembersScreen.js +464 -0
- package/lib/module/ui/screens/AccountMembersScreen.js.map +1 -0
- package/lib/module/ui/screens/AccountSettingsScreen.js +259 -0
- package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -0
- package/lib/module/ui/screens/AccountSwitcherScreen.js +48 -0
- package/lib/module/ui/screens/AccountSwitcherScreen.js.map +1 -0
- package/lib/module/ui/screens/{CreateManagedAccountScreen.js → CreateAccountScreen.js} +117 -26
- package/lib/module/ui/screens/CreateAccountScreen.js.map +1 -0
- package/lib/module/ui/screens/ManageAccountScreen.js +11 -11
- package/lib/module/ui/screens/ManageAccountScreen.js.map +1 -1
- package/lib/module/ui/screens/OxyAuthScreen.js +10 -2
- package/lib/module/ui/screens/OxyAuthScreen.js.map +1 -1
- package/lib/module/utils/crossApex.js +69 -0
- package/lib/module/utils/crossApex.js.map +1 -0
- package/lib/typescript/commonjs/index.d.ts +8 -1
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/AccountMenuButton.d.ts +13 -4
- package/lib/typescript/commonjs/ui/components/AccountMenuButton.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/AccountSwitcher.d.ts +47 -0
- package/lib/typescript/commonjs/ui/components/AccountSwitcher.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/{ActingAsBanner.d.ts → ActiveAccountBanner.d.ts} +1 -1
- package/lib/typescript/commonjs/ui/components/ActiveAccountBanner.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/SignInModal.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +33 -5
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/index.d.ts +5 -0
- package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/navigation/routes.d.ts +1 -1
- package/lib/typescript/commonjs/ui/navigation/routes.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/AccountMembersScreen.d.ts +10 -0
- package/lib/typescript/commonjs/ui/screens/AccountMembersScreen.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/screens/AccountSettingsScreen.d.ts +12 -0
- package/lib/typescript/commonjs/ui/screens/AccountSettingsScreen.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/screens/AccountSwitcherScreen.d.ts +11 -0
- package/lib/typescript/commonjs/ui/screens/AccountSwitcherScreen.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/screens/CreateAccountScreen.d.ts +11 -0
- package/lib/typescript/commonjs/ui/screens/CreateAccountScreen.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/screens/OxyAuthScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/utils/crossApex.d.ts +55 -0
- package/lib/typescript/commonjs/utils/crossApex.d.ts.map +1 -0
- package/lib/typescript/module/index.d.ts +8 -1
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/AccountMenuButton.d.ts +13 -4
- package/lib/typescript/module/ui/components/AccountMenuButton.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/AccountSwitcher.d.ts +47 -0
- package/lib/typescript/module/ui/components/AccountSwitcher.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/{ActingAsBanner.d.ts → ActiveAccountBanner.d.ts} +1 -1
- package/lib/typescript/module/ui/components/ActiveAccountBanner.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/SignInModal.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/OxyContext.d.ts +33 -5
- package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/module/ui/index.d.ts +5 -0
- package/lib/typescript/module/ui/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/navigation/routes.d.ts +1 -1
- package/lib/typescript/module/ui/navigation/routes.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/AccountMembersScreen.d.ts +10 -0
- package/lib/typescript/module/ui/screens/AccountMembersScreen.d.ts.map +1 -0
- package/lib/typescript/module/ui/screens/AccountSettingsScreen.d.ts +12 -0
- package/lib/typescript/module/ui/screens/AccountSettingsScreen.d.ts.map +1 -0
- package/lib/typescript/module/ui/screens/AccountSwitcherScreen.d.ts +11 -0
- package/lib/typescript/module/ui/screens/AccountSwitcherScreen.d.ts.map +1 -0
- package/lib/typescript/module/ui/screens/CreateAccountScreen.d.ts +11 -0
- package/lib/typescript/module/ui/screens/CreateAccountScreen.d.ts.map +1 -0
- package/lib/typescript/module/ui/screens/OxyAuthScreen.d.ts.map +1 -1
- package/lib/typescript/module/utils/crossApex.d.ts +55 -0
- package/lib/typescript/module/utils/crossApex.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/index.ts +22 -4
- package/src/ui/components/AccountMenuButton.tsx +25 -10
- package/src/ui/components/AccountSwitcher.tsx +763 -0
- package/src/ui/components/ActiveAccountBanner.tsx +130 -0
- package/src/ui/components/SignInModal.tsx +18 -6
- package/src/ui/context/OxyContext.tsx +121 -35
- package/src/ui/context/hooks/useAuthOperations.ts +8 -0
- package/src/ui/index.ts +5 -0
- package/src/ui/navigation/routes.ts +8 -2
- package/src/ui/screens/AccountMembersScreen.tsx +481 -0
- package/src/ui/screens/AccountSettingsScreen.tsx +261 -0
- package/src/ui/screens/AccountSwitcherScreen.tsx +43 -0
- package/src/ui/screens/{CreateManagedAccountScreen.tsx → CreateAccountScreen.tsx} +140 -22
- package/src/ui/screens/ManageAccountScreen.tsx +19 -19
- package/src/ui/screens/OxyAuthScreen.tsx +20 -6
- package/src/utils/crossApex.ts +75 -0
- package/lib/commonjs/ui/components/ActingAsBanner.js +0 -140
- package/lib/commonjs/ui/components/ActingAsBanner.js.map +0 -1
- package/lib/commonjs/ui/screens/CreateManagedAccountScreen.js.map +0 -1
- package/lib/module/ui/components/ActingAsBanner.js +0 -137
- package/lib/module/ui/components/ActingAsBanner.js.map +0 -1
- package/lib/module/ui/screens/CreateManagedAccountScreen.js.map +0 -1
- package/lib/typescript/commonjs/ui/components/ActingAsBanner.d.ts.map +0 -1
- package/lib/typescript/commonjs/ui/screens/CreateManagedAccountScreen.d.ts +0 -5
- package/lib/typescript/commonjs/ui/screens/CreateManagedAccountScreen.d.ts.map +0 -1
- package/lib/typescript/module/ui/components/ActingAsBanner.d.ts.map +0 -1
- package/lib/typescript/module/ui/screens/CreateManagedAccountScreen.d.ts +0 -5
- package/lib/typescript/module/ui/screens/CreateManagedAccountScreen.d.ts.map +0 -1
- 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
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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 {
|
|
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
|
-
//
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
// ---
|
|
2020
|
+
// --- Account graph state ---
|
|
1977
2021
|
const [actingAs, setActingAsState] = useState<string | null>(null);
|
|
1978
|
-
const [
|
|
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
|
|
2001
|
-
const
|
|
2053
|
+
// Load the unified account graph when authenticated
|
|
2054
|
+
const refreshAccounts = useCallback(async (): Promise<void> => {
|
|
2002
2055
|
if (!isAuthenticated || !tokenReady || !oxyServices.getAccessToken()) {
|
|
2003
|
-
|
|
2056
|
+
setAccounts([]);
|
|
2004
2057
|
return;
|
|
2005
2058
|
}
|
|
2006
2059
|
|
|
2007
2060
|
try {
|
|
2008
|
-
const
|
|
2009
|
-
|
|
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
|
-
|
|
2074
|
+
setAccounts([]);
|
|
2013
2075
|
await clearSessionStateRef.current();
|
|
2014
2076
|
return;
|
|
2015
2077
|
}
|
|
2016
2078
|
if (__DEV__) {
|
|
2017
|
-
loggerUtil.debug('Failed to load
|
|
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
|
-
|
|
2086
|
+
refreshAccounts();
|
|
2025
2087
|
}
|
|
2026
|
-
}, [isAuthenticated, initialized, tokenReady,
|
|
2088
|
+
}, [isAuthenticated, initialized, tokenReady, refreshAccounts]);
|
|
2027
2089
|
|
|
2028
|
-
const setActingAs = useCallback((
|
|
2029
|
-
oxyServices.setActingAs(
|
|
2030
|
-
setActingAsState(
|
|
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 (
|
|
2034
|
-
storage.setItem(`${storageKeyPrefix}_acting_as`,
|
|
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
|
|
2046
|
-
const account = await oxyServices.
|
|
2047
|
-
await
|
|
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,
|
|
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
|
-
|
|
2174
|
+
accounts,
|
|
2175
|
+
actingAsAccount,
|
|
2176
|
+
activeAccount,
|
|
2095
2177
|
setActingAs,
|
|
2096
|
-
|
|
2097
|
-
|
|
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
|
-
|
|
2221
|
+
accounts,
|
|
2222
|
+
actingAsAccount,
|
|
2223
|
+
activeAccount,
|
|
2140
2224
|
setActingAs,
|
|
2141
|
-
|
|
2142
|
-
|
|
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
|
-
|
|
2294
|
+
accounts: [],
|
|
2295
|
+
actingAsAccount: null,
|
|
2296
|
+
activeAccount: null,
|
|
2211
2297
|
setActingAs: () => {},
|
|
2212
|
-
|
|
2213
|
-
|
|
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
|
-
| '
|
|
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
|
-
|
|
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,
|