@oxyhq/services 12.2.0 → 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 +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 +64 -27
- 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 +64 -27
- 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 +113 -35
- 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,50 @@ 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,
|
|
2088
|
+
}, [isAuthenticated, initialized, tokenReady, refreshAccounts]);
|
|
2035
2089
|
|
|
2036
|
-
const setActingAs = useCallback((
|
|
2037
|
-
oxyServices.setActingAs(
|
|
2038
|
-
setActingAsState(
|
|
2090
|
+
const setActingAs = useCallback((accountId: string | null) => {
|
|
2091
|
+
oxyServices.setActingAs(accountId);
|
|
2092
|
+
setActingAsState(accountId);
|
|
2039
2093
|
// Persist to storage
|
|
2040
2094
|
if (storage) {
|
|
2041
|
-
if (
|
|
2042
|
-
storage.setItem(`${storageKeyPrefix}_acting_as`,
|
|
2095
|
+
if (accountId) {
|
|
2096
|
+
storage.setItem(`${storageKeyPrefix}_acting_as`, accountId).catch((persistError) => {
|
|
2043
2097
|
loggerUtil.debug('Failed to persist acting-as account', { component: 'OxyContext' }, persistError as unknown);
|
|
2044
2098
|
});
|
|
2045
2099
|
} else {
|
|
@@ -2049,12 +2103,30 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
2049
2103
|
}
|
|
2050
2104
|
}
|
|
2051
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
|
+
);
|
|
2052
2124
|
|
|
2053
|
-
const
|
|
2054
|
-
const account = await oxyServices.
|
|
2055
|
-
await
|
|
2125
|
+
const createAccountFn = useCallback(async (data: CreateAccountInput): Promise<AccountNode> => {
|
|
2126
|
+
const account = await oxyServices.createAccount(data);
|
|
2127
|
+
await refreshAccounts();
|
|
2056
2128
|
return account;
|
|
2057
|
-
}, [oxyServices,
|
|
2129
|
+
}, [oxyServices, refreshAccounts]);
|
|
2058
2130
|
|
|
2059
2131
|
const canUsePrivateApi = authResolved && isAuthenticated && tokenReady && hasAccessToken;
|
|
2060
2132
|
const isPrivateApiPending = !authResolved || (isAuthenticated && (!tokenReady || !hasAccessToken));
|
|
@@ -2099,10 +2171,12 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
2099
2171
|
showBottomSheet: showBottomSheetForContext,
|
|
2100
2172
|
openAvatarPicker,
|
|
2101
2173
|
actingAs,
|
|
2102
|
-
|
|
2174
|
+
accounts,
|
|
2175
|
+
actingAsAccount,
|
|
2176
|
+
activeAccount,
|
|
2103
2177
|
setActingAs,
|
|
2104
|
-
|
|
2105
|
-
|
|
2178
|
+
refreshAccounts,
|
|
2179
|
+
createAccount: createAccountFn,
|
|
2106
2180
|
}), [
|
|
2107
2181
|
activeSessionId,
|
|
2108
2182
|
signIn,
|
|
@@ -2144,10 +2218,12 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
2144
2218
|
showBottomSheetForContext,
|
|
2145
2219
|
openAvatarPicker,
|
|
2146
2220
|
actingAs,
|
|
2147
|
-
|
|
2221
|
+
accounts,
|
|
2222
|
+
actingAsAccount,
|
|
2223
|
+
activeAccount,
|
|
2148
2224
|
setActingAs,
|
|
2149
|
-
|
|
2150
|
-
|
|
2225
|
+
refreshAccounts,
|
|
2226
|
+
createAccountFn,
|
|
2151
2227
|
]);
|
|
2152
2228
|
|
|
2153
2229
|
return (
|
|
@@ -2215,10 +2291,12 @@ const LOADING_STATE: OxyContextState = {
|
|
|
2215
2291
|
oxyServices: LOADING_STATE_OXY_SERVICES,
|
|
2216
2292
|
openAvatarPicker: () => {},
|
|
2217
2293
|
actingAs: null,
|
|
2218
|
-
|
|
2294
|
+
accounts: [],
|
|
2295
|
+
actingAsAccount: null,
|
|
2296
|
+
activeAccount: null,
|
|
2219
2297
|
setActingAs: () => {},
|
|
2220
|
-
|
|
2221
|
-
|
|
2298
|
+
refreshAccounts: () => rejectMissingProvider<void>(),
|
|
2299
|
+
createAccount: () => rejectMissingProvider<AccountNode>(),
|
|
2222
2300
|
};
|
|
2223
2301
|
|
|
2224
2302
|
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,
|