@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.
- package/lib/commonjs/index.js +56 -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/context/OxyContext.js +72 -28
- package/lib/commonjs/ui/context/OxyContext.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/module/index.js +16 -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/context/OxyContext.js +72 -28
- package/lib/module/ui/context/OxyContext.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/typescript/commonjs/index.d.ts +7 -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/context/OxyContext.d.ts +33 -5
- package/lib/typescript/commonjs/ui/context/OxyContext.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/module/index.d.ts +7 -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/context/OxyContext.d.ts +33 -5
- package/lib/typescript/module/ui/context/OxyContext.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/package.json +3 -3
- package/src/index.ts +18 -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/context/OxyContext.tsx +122 -37
- 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/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);
|
|
@@ -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 {
|
|
@@ -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
|
-
//
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
// ---
|
|
2020
|
+
// --- Account graph state ---
|
|
1985
2021
|
const [actingAs, setActingAsState] = useState<string | null>(null);
|
|
1986
|
-
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);
|
|
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
|
|
2009
|
-
const
|
|
2053
|
+
// Load the unified account graph when authenticated
|
|
2054
|
+
const refreshAccounts = useCallback(async (): Promise<void> => {
|
|
2010
2055
|
if (!isAuthenticated || !tokenReady || !oxyServices.getAccessToken()) {
|
|
2011
|
-
|
|
2056
|
+
setAccounts([]);
|
|
2012
2057
|
return;
|
|
2013
2058
|
}
|
|
2014
2059
|
|
|
2015
2060
|
try {
|
|
2016
|
-
const
|
|
2017
|
-
|
|
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
|
-
|
|
2074
|
+
setAccounts([]);
|
|
2021
2075
|
await clearSessionStateRef.current();
|
|
2022
2076
|
return;
|
|
2023
2077
|
}
|
|
2024
2078
|
if (__DEV__) {
|
|
2025
|
-
loggerUtil.debug('Failed to load
|
|
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
|
-
|
|
2086
|
+
refreshAccounts();
|
|
2033
2087
|
}
|
|
2034
|
-
}, [isAuthenticated, initialized, tokenReady,
|
|
2035
|
-
|
|
2036
|
-
const setActingAs = useCallback((
|
|
2037
|
-
oxyServices.setActingAs(
|
|
2038
|
-
setActingAsState(
|
|
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 (
|
|
2042
|
-
storage.setItem(`${storageKeyPrefix}_acting_as`,
|
|
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
|
|
2054
|
-
const account = await oxyServices.
|
|
2055
|
-
await
|
|
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,
|
|
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
|
-
|
|
2181
|
+
accounts,
|
|
2182
|
+
actingAsAccount,
|
|
2183
|
+
activeAccount,
|
|
2103
2184
|
setActingAs,
|
|
2104
|
-
|
|
2105
|
-
|
|
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
|
-
|
|
2228
|
+
accounts,
|
|
2229
|
+
actingAsAccount,
|
|
2230
|
+
activeAccount,
|
|
2148
2231
|
setActingAs,
|
|
2149
|
-
|
|
2150
|
-
|
|
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
|
-
|
|
2301
|
+
accounts: [],
|
|
2302
|
+
actingAsAccount: null,
|
|
2303
|
+
activeAccount: null,
|
|
2219
2304
|
setActingAs: () => {},
|
|
2220
|
-
|
|
2221
|
-
|
|
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
|
-
| '
|
|
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,
|