@oxyhq/services 13.3.0 → 14.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 +11 -17
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/components/AccountMenu.js +103 -65
- package/lib/commonjs/ui/components/AccountMenu.js.map +1 -1
- package/lib/commonjs/ui/components/AccountSwitcher.js +84 -126
- package/lib/commonjs/ui/components/AccountSwitcher.js.map +1 -1
- package/lib/commonjs/ui/components/ProfileMenu.js +140 -153
- package/lib/commonjs/ui/components/ProfileMenu.js.map +1 -1
- package/lib/commonjs/ui/components/SignInModal.js +4 -4
- package/lib/commonjs/ui/components/SignInModal.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +506 -466
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js +59 -57
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/commonjs/ui/context/inSessionTokenRefresh.js +23 -42
- package/lib/commonjs/ui/context/inSessionTokenRefresh.js.map +1 -1
- package/lib/commonjs/ui/context/silentSessionRestore.js +12 -21
- package/lib/commonjs/ui/context/silentSessionRestore.js.map +1 -1
- package/lib/commonjs/ui/hooks/useAuth.js +4 -3
- package/lib/commonjs/ui/hooks/useAuth.js.map +1 -1
- package/lib/commonjs/ui/hooks/useOxyAuthSession.js +12 -7
- package/lib/commonjs/ui/hooks/useOxyAuthSession.js.map +1 -1
- package/lib/commonjs/ui/hooks/useSessionManagement.js +17 -65
- package/lib/commonjs/ui/hooks/useSessionManagement.js.map +1 -1
- package/lib/commonjs/ui/hooks/useSwitchableAccounts.js +288 -0
- package/lib/commonjs/ui/hooks/useSwitchableAccounts.js.map +1 -0
- package/lib/commonjs/ui/hooks/useWebSSO.js +12 -191
- package/lib/commonjs/ui/hooks/useWebSSO.js.map +1 -1
- package/lib/commonjs/ui/index.js.map +1 -1
- package/lib/commonjs/ui/screens/OxyAuthScreen.js +2 -2
- package/lib/commonjs/ui/screens/OxyAuthScreen.js.map +1 -1
- package/lib/commonjs/ui/session/createSessionClient.js +28 -0
- package/lib/commonjs/ui/session/createSessionClient.js.map +1 -0
- package/lib/commonjs/ui/session/index.js +52 -0
- package/lib/commonjs/ui/session/index.js.map +1 -0
- package/lib/commonjs/ui/session/projectSessionState.js +86 -0
- package/lib/commonjs/ui/session/projectSessionState.js.map +1 -0
- package/lib/commonjs/ui/session/sessionClientHost.js +30 -0
- package/lib/commonjs/ui/session/sessionClientHost.js.map +1 -0
- package/lib/commonjs/ui/session/tokenTransport.js +77 -0
- package/lib/commonjs/ui/session/tokenTransport.js.map +1 -0
- package/lib/commonjs/ui/utils/activeAuthuser.js +35 -79
- package/lib/commonjs/ui/utils/activeAuthuser.js.map +1 -1
- package/lib/commonjs/ui/utils/sessionHelpers.js +1 -51
- package/lib/commonjs/ui/utils/sessionHelpers.js.map +1 -1
- package/lib/commonjs/utils/deviceFlowSignIn.js +70 -15
- package/lib/commonjs/utils/deviceFlowSignIn.js.map +1 -1
- package/lib/module/index.js +5 -5
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/components/AccountMenu.js +103 -65
- package/lib/module/ui/components/AccountMenu.js.map +1 -1
- package/lib/module/ui/components/AccountSwitcher.js +85 -127
- package/lib/module/ui/components/AccountSwitcher.js.map +1 -1
- package/lib/module/ui/components/ProfileMenu.js +143 -156
- package/lib/module/ui/components/ProfileMenu.js.map +1 -1
- package/lib/module/ui/components/SignInModal.js +4 -4
- package/lib/module/ui/components/SignInModal.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +510 -470
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/context/hooks/useAuthOperations.js +61 -60
- package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/module/ui/context/inSessionTokenRefresh.js +24 -43
- package/lib/module/ui/context/inSessionTokenRefresh.js.map +1 -1
- package/lib/module/ui/context/silentSessionRestore.js +12 -20
- package/lib/module/ui/context/silentSessionRestore.js.map +1 -1
- package/lib/module/ui/hooks/useAuth.js +4 -3
- package/lib/module/ui/hooks/useAuth.js.map +1 -1
- package/lib/module/ui/hooks/useOxyAuthSession.js +12 -7
- package/lib/module/ui/hooks/useOxyAuthSession.js.map +1 -1
- package/lib/module/ui/hooks/useSessionManagement.js +18 -66
- package/lib/module/ui/hooks/useSessionManagement.js.map +1 -1
- package/lib/module/ui/hooks/useSwitchableAccounts.js +283 -0
- package/lib/module/ui/hooks/useSwitchableAccounts.js.map +1 -0
- package/lib/module/ui/hooks/useWebSSO.js +13 -192
- package/lib/module/ui/hooks/useWebSSO.js.map +1 -1
- package/lib/module/ui/index.js +0 -1
- package/lib/module/ui/index.js.map +1 -1
- package/lib/module/ui/screens/OxyAuthScreen.js +2 -2
- package/lib/module/ui/screens/OxyAuthScreen.js.map +1 -1
- package/lib/module/ui/session/createSessionClient.js +25 -0
- package/lib/module/ui/session/createSessionClient.js.map +1 -0
- package/lib/module/ui/session/index.js +17 -0
- package/lib/module/ui/session/index.js.map +1 -0
- package/lib/module/ui/session/projectSessionState.js +79 -0
- package/lib/module/ui/session/projectSessionState.js.map +1 -0
- package/lib/module/ui/session/sessionClientHost.js +26 -0
- package/lib/module/ui/session/sessionClientHost.js.map +1 -0
- package/lib/module/ui/session/tokenTransport.js +74 -0
- package/lib/module/ui/session/tokenTransport.js.map +1 -0
- package/lib/module/ui/utils/activeAuthuser.js +34 -76
- package/lib/module/ui/utils/activeAuthuser.js.map +1 -1
- package/lib/module/ui/utils/sessionHelpers.js +0 -49
- package/lib/module/ui/utils/sessionHelpers.js.map +1 -1
- package/lib/module/utils/deviceFlowSignIn.js +71 -16
- package/lib/module/utils/deviceFlowSignIn.js.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +2 -3
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/AccountMenu.d.ts +4 -2
- package/lib/typescript/commonjs/ui/components/AccountMenu.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/AccountSwitcher.d.ts +7 -8
- package/lib/typescript/commonjs/ui/components/AccountSwitcher.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/ProfileMenu.d.ts +12 -9
- package/lib/typescript/commonjs/ui/components/ProfileMenu.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +16 -13
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts +27 -3
- package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/inSessionTokenRefresh.d.ts +22 -17
- package/lib/typescript/commonjs/ui/context/inSessionTokenRefresh.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/silentSessionRestore.d.ts +12 -18
- package/lib/typescript/commonjs/ui/context/silentSessionRestore.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +1 -0
- package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts +1 -1
- package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useOxyAuthSession.d.ts +2 -2
- package/lib/typescript/commonjs/ui/hooks/useOxyAuthSession.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useSessionManagement.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useSwitchableAccounts.d.ts +162 -0
- package/lib/typescript/commonjs/ui/hooks/useSwitchableAccounts.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/hooks/useWebSSO.d.ts +13 -52
- package/lib/typescript/commonjs/ui/hooks/useWebSSO.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts +15 -0
- package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/session/index.d.ts +15 -0
- package/lib/typescript/commonjs/ui/session/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/session/projectSessionState.d.ts +45 -0
- package/lib/typescript/commonjs/ui/session/projectSessionState.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/session/sessionClientHost.d.ts +14 -0
- package/lib/typescript/commonjs/ui/session/sessionClientHost.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/session/tokenTransport.d.ts +24 -0
- package/lib/typescript/commonjs/ui/session/tokenTransport.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/utils/activeAuthuser.d.ts +21 -29
- package/lib/typescript/commonjs/ui/utils/activeAuthuser.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/sessionHelpers.d.ts +0 -19
- package/lib/typescript/commonjs/ui/utils/sessionHelpers.d.ts.map +1 -1
- package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts +51 -19
- package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +2 -3
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/AccountMenu.d.ts +4 -2
- package/lib/typescript/module/ui/components/AccountMenu.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/AccountSwitcher.d.ts +7 -8
- package/lib/typescript/module/ui/components/AccountSwitcher.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ProfileMenu.d.ts +12 -9
- package/lib/typescript/module/ui/components/ProfileMenu.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/OxyContext.d.ts +16 -13
- package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts +27 -3
- package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/inSessionTokenRefresh.d.ts +22 -17
- package/lib/typescript/module/ui/context/inSessionTokenRefresh.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/silentSessionRestore.d.ts +12 -18
- package/lib/typescript/module/ui/context/silentSessionRestore.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +1 -0
- package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useAuth.d.ts +1 -1
- package/lib/typescript/module/ui/hooks/useAuth.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useOxyAuthSession.d.ts +2 -2
- package/lib/typescript/module/ui/hooks/useOxyAuthSession.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useSessionManagement.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useSwitchableAccounts.d.ts +162 -0
- package/lib/typescript/module/ui/hooks/useSwitchableAccounts.d.ts.map +1 -0
- package/lib/typescript/module/ui/hooks/useWebSSO.d.ts +13 -52
- package/lib/typescript/module/ui/hooks/useWebSSO.d.ts.map +1 -1
- package/lib/typescript/module/ui/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/session/createSessionClient.d.ts +15 -0
- package/lib/typescript/module/ui/session/createSessionClient.d.ts.map +1 -0
- package/lib/typescript/module/ui/session/index.d.ts +15 -0
- package/lib/typescript/module/ui/session/index.d.ts.map +1 -0
- package/lib/typescript/module/ui/session/projectSessionState.d.ts +45 -0
- package/lib/typescript/module/ui/session/projectSessionState.d.ts.map +1 -0
- package/lib/typescript/module/ui/session/sessionClientHost.d.ts +14 -0
- package/lib/typescript/module/ui/session/sessionClientHost.d.ts.map +1 -0
- package/lib/typescript/module/ui/session/tokenTransport.d.ts +24 -0
- package/lib/typescript/module/ui/session/tokenTransport.d.ts.map +1 -0
- package/lib/typescript/module/ui/utils/activeAuthuser.d.ts +21 -29
- package/lib/typescript/module/ui/utils/activeAuthuser.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/sessionHelpers.d.ts +0 -19
- package/lib/typescript/module/ui/utils/sessionHelpers.d.ts.map +1 -1
- package/lib/typescript/module/utils/deviceFlowSignIn.d.ts +51 -19
- package/lib/typescript/module/utils/deviceFlowSignIn.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +9 -9
- package/src/ui/components/AccountMenu.tsx +117 -70
- package/src/ui/components/AccountSwitcher.tsx +88 -134
- package/src/ui/components/ProfileMenu.tsx +158 -162
- package/src/ui/components/SignInModal.tsx +5 -5
- package/src/ui/context/OxyContext.tsx +560 -475
- package/src/ui/context/hooks/useAuthOperations.ts +72 -63
- package/src/ui/context/inSessionTokenRefresh.ts +23 -42
- package/src/ui/context/silentSessionRestore.ts +12 -26
- package/src/ui/hooks/useAuth.ts +4 -3
- package/src/ui/hooks/useOxyAuthSession.ts +13 -8
- package/src/ui/hooks/useSessionManagement.ts +16 -87
- package/src/ui/hooks/useSwitchableAccounts.ts +397 -0
- package/src/ui/hooks/useWebSSO.ts +13 -227
- package/src/ui/index.ts +0 -1
- package/src/ui/screens/OxyAuthScreen.tsx +2 -2
- package/src/ui/session/__tests__/createSessionClient.test.ts +41 -0
- package/src/ui/session/__tests__/projectSessionState.test.ts +114 -0
- package/src/ui/session/__tests__/sessionClientHost.test.ts +43 -0
- package/src/ui/session/__tests__/tokenTransport.test.ts +142 -0
- package/src/ui/session/createSessionClient.ts +21 -0
- package/src/ui/session/index.ts +19 -0
- package/src/ui/session/projectSessionState.ts +85 -0
- package/src/ui/session/sessionClientHost.ts +27 -0
- package/src/ui/session/tokenTransport.ts +76 -0
- package/src/ui/utils/activeAuthuser.ts +34 -76
- package/src/ui/utils/sessionHelpers.ts +0 -66
- package/src/utils/__tests__/deviceFlowSignIn.test.ts +243 -34
- package/src/utils/deviceFlowSignIn.ts +98 -21
- package/lib/commonjs/ui/components/accountMenuRows.js +0 -33
- package/lib/commonjs/ui/components/accountMenuRows.js.map +0 -1
- package/lib/commonjs/ui/hooks/useDeviceAccounts.js +0 -280
- package/lib/commonjs/ui/hooks/useDeviceAccounts.js.map +0 -1
- package/lib/commonjs/ui/hooks/useSessionSocket.js +0 -203
- package/lib/commonjs/ui/hooks/useSessionSocket.js.map +0 -1
- package/lib/commonjs/utils/silentGuardKey.js +0 -54
- package/lib/commonjs/utils/silentGuardKey.js.map +0 -1
- package/lib/module/ui/components/accountMenuRows.js +0 -29
- package/lib/module/ui/components/accountMenuRows.js.map +0 -1
- package/lib/module/ui/hooks/useDeviceAccounts.js +0 -276
- package/lib/module/ui/hooks/useDeviceAccounts.js.map +0 -1
- package/lib/module/ui/hooks/useSessionSocket.js +0 -199
- package/lib/module/ui/hooks/useSessionSocket.js.map +0 -1
- package/lib/module/utils/silentGuardKey.js +0 -49
- package/lib/module/utils/silentGuardKey.js.map +0 -1
- package/lib/typescript/commonjs/ui/components/accountMenuRows.d.ts +0 -34
- package/lib/typescript/commonjs/ui/components/accountMenuRows.d.ts.map +0 -1
- package/lib/typescript/commonjs/ui/hooks/useDeviceAccounts.d.ts +0 -133
- package/lib/typescript/commonjs/ui/hooks/useDeviceAccounts.d.ts.map +0 -1
- package/lib/typescript/commonjs/ui/hooks/useSessionSocket.d.ts +0 -14
- package/lib/typescript/commonjs/ui/hooks/useSessionSocket.d.ts.map +0 -1
- package/lib/typescript/commonjs/utils/silentGuardKey.d.ts +0 -31
- package/lib/typescript/commonjs/utils/silentGuardKey.d.ts.map +0 -1
- package/lib/typescript/module/ui/components/accountMenuRows.d.ts +0 -34
- package/lib/typescript/module/ui/components/accountMenuRows.d.ts.map +0 -1
- package/lib/typescript/module/ui/hooks/useDeviceAccounts.d.ts +0 -133
- package/lib/typescript/module/ui/hooks/useDeviceAccounts.d.ts.map +0 -1
- package/lib/typescript/module/ui/hooks/useSessionSocket.d.ts +0 -14
- package/lib/typescript/module/ui/hooks/useSessionSocket.d.ts.map +0 -1
- package/lib/typescript/module/utils/silentGuardKey.d.ts +0 -31
- package/lib/typescript/module/utils/silentGuardKey.d.ts.map +0 -1
- package/src/ui/components/accountMenuRows.ts +0 -48
- package/src/ui/hooks/useDeviceAccounts.ts +0 -344
- package/src/ui/hooks/useSessionSocket.ts +0 -233
- package/src/ui/utils/__tests__/activeAuthuser.test.ts +0 -75
- package/src/utils/__tests__/silentGuardKey.test.ts +0 -82
- package/src/utils/silentGuardKey.ts +0 -46
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { useQuery } from '@tanstack/react-query';
|
|
3
|
+
import {
|
|
4
|
+
getAccountDisplayName,
|
|
5
|
+
getAccountFallbackHandle,
|
|
6
|
+
} from '@oxyhq/core';
|
|
7
|
+
import type {
|
|
8
|
+
User,
|
|
9
|
+
AccountNode,
|
|
10
|
+
AccountRelationship,
|
|
11
|
+
AccountKind,
|
|
12
|
+
AccountMember,
|
|
13
|
+
} from '@oxyhq/core';
|
|
14
|
+
import { useOxy } from '../context/OxyContext';
|
|
15
|
+
import { useI18n } from './useI18n';
|
|
16
|
+
import { queryKeys } from './queries/queryKeys';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The per-account user shape carried by a {@link SwitchableAccount}. The SDK's
|
|
20
|
+
* canonical {@link User} document — either the freshest `useOxy().user` (the
|
|
21
|
+
* active row), a profile resolved via `oxyServices.getUsersByIds()` (every other
|
|
22
|
+
* device row), or the `account` document embedded in an account-graph node.
|
|
23
|
+
*/
|
|
24
|
+
export type SwitchableAccountUser = User;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* One account the signed-in user can switch INTO, in the uniform switch model.
|
|
28
|
+
*
|
|
29
|
+
* A switchable account is either a device sign-in, an account-graph node (owned
|
|
30
|
+
* org / shared-with-you), or BOTH (an account that has been switched into
|
|
31
|
+
* becomes a device session while still being a graph node — the two are deduped
|
|
32
|
+
* into a single row). Every row carries a canonical `accountId` (the uniform
|
|
33
|
+
* switch key passed to `switchToAccount`); `sessionId` is present IFF the
|
|
34
|
+
* account is currently signed in on THIS device.
|
|
35
|
+
*/
|
|
36
|
+
export interface SwitchableAccount {
|
|
37
|
+
/**
|
|
38
|
+
* Canonical account id (the underlying `User._id`). The single key EVERY
|
|
39
|
+
* switch uses — `switchToAccount(accountId)`. Always present.
|
|
40
|
+
*/
|
|
41
|
+
accountId: string;
|
|
42
|
+
/**
|
|
43
|
+
* Device session id, present IFF this account is signed in on THIS device.
|
|
44
|
+
* Absent for a graph account not yet switched into. Used only for
|
|
45
|
+
* device-scoped actions (per-account sign-out via `removeSession`); switching
|
|
46
|
+
* ALWAYS goes through `switchToAccount(accountId)`.
|
|
47
|
+
*/
|
|
48
|
+
sessionId?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Device-local refresh-cookie slot index (0..N-1), when the underlying
|
|
51
|
+
* `ClientSession` carries one (web silent-switch). Absent on native and for
|
|
52
|
+
* graph-only rows.
|
|
53
|
+
*/
|
|
54
|
+
authuser?: number;
|
|
55
|
+
/** Whether this account is the currently-active one. */
|
|
56
|
+
isCurrent: boolean;
|
|
57
|
+
/** Whether this account is signed in on THIS device (has a `sessionId`). */
|
|
58
|
+
onDevice: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* The caller's relationship to this account when it appears in the account
|
|
61
|
+
* graph: `self` (the caller's own personal account), `owner` (an org/project/
|
|
62
|
+
* bot the caller owns), or `member` (shared with the caller). Absent for an
|
|
63
|
+
* independent device sign-in that is NOT in the active account's graph.
|
|
64
|
+
*/
|
|
65
|
+
relationship?: AccountRelationship;
|
|
66
|
+
/** Account classification (personal/organization/…). Cosmetic badge only. */
|
|
67
|
+
kind?: AccountKind;
|
|
68
|
+
/** Parent account id for 2-level tree grouping, or `null` for a root. */
|
|
69
|
+
parentAccountId?: string | null;
|
|
70
|
+
/**
|
|
71
|
+
* The caller's effective membership (role + permissions) in this account when
|
|
72
|
+
* it appears in the graph, or `null`/absent otherwise. Use `permissions` to
|
|
73
|
+
* gate per-account settings UI.
|
|
74
|
+
*/
|
|
75
|
+
callerMembership?: AccountMember | null;
|
|
76
|
+
/** Friendly display name (never blank — falls back to a handle/sentinel). */
|
|
77
|
+
displayName: string;
|
|
78
|
+
/**
|
|
79
|
+
* Real account email, or `null` when the account genuinely has none. NEVER a
|
|
80
|
+
* synthesized `username@oxy.so` — a missing email falls back to the `@handle`
|
|
81
|
+
* secondary line.
|
|
82
|
+
*/
|
|
83
|
+
email: string | null;
|
|
84
|
+
/** Resolved avatar thumbnail URL, or `undefined` when the account has no avatar. */
|
|
85
|
+
avatarUrl?: string;
|
|
86
|
+
/** Account's preferred Bloom color preset, or `null` when unset. */
|
|
87
|
+
color: string | null;
|
|
88
|
+
/** The underlying per-account user payload. */
|
|
89
|
+
user: SwitchableAccountUser;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface UseSwitchableAccountsResult {
|
|
93
|
+
/** Every switchable account (device sign-ins + linked graph accounts). */
|
|
94
|
+
accounts: SwitchableAccount[];
|
|
95
|
+
/** True until the per-device-account profile fetch settles. */
|
|
96
|
+
isLoading: boolean;
|
|
97
|
+
/** The currently-active session id (mirrors `useOxy().activeSessionId`). */
|
|
98
|
+
currentSessionId: string | null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The minimal device-session shape {@link buildSwitchableAccounts} reads. */
|
|
102
|
+
export interface SwitchableSessionInput {
|
|
103
|
+
sessionId: string;
|
|
104
|
+
userId?: string;
|
|
105
|
+
authuser?: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface BuildSwitchableAccountsInput {
|
|
109
|
+
/** Device sessions projected from the server-authoritative `SessionClient`. */
|
|
110
|
+
sessions: SwitchableSessionInput[];
|
|
111
|
+
/** The currently-active session id. */
|
|
112
|
+
activeSessionId: string | null;
|
|
113
|
+
/** The freshest copy of the active account's user (`useOxy().user`). */
|
|
114
|
+
liveUser: User | null;
|
|
115
|
+
isAuthenticated: boolean;
|
|
116
|
+
/** The account graph under the active account (`useOxy().accounts`). */
|
|
117
|
+
graph: AccountNode[];
|
|
118
|
+
/** Per-device-account profiles resolved via `getUsersByIds()`, keyed by id. */
|
|
119
|
+
profilesById: Map<string, User>;
|
|
120
|
+
locale: string;
|
|
121
|
+
/** Resolves a file id to a thumbnail URL (bound to `getFileDownloadUrl`). */
|
|
122
|
+
resolveAvatarUrl: (avatar: string | null | undefined) => string | undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Resolve which entries are the current account, robustly.
|
|
127
|
+
*
|
|
128
|
+
* Primary signal: `entry.sessionId === activeSessionId` — both are projected
|
|
129
|
+
* from the same `SessionClient` state, so this normally matches exactly.
|
|
130
|
+
* Fallbacks exist only to bridge the brief window between the two sequential
|
|
131
|
+
* `updateSessions` / `setActiveSessionId` calls in `OxyContext.syncFromClient`,
|
|
132
|
+
* applied only when the `sessionId` match found nothing AND the user is
|
|
133
|
+
* authenticated:
|
|
134
|
+
* 1. Match the live `user.id` against each entry's per-account user id.
|
|
135
|
+
* 2. If still nothing and there is exactly one account, mark that one current.
|
|
136
|
+
*
|
|
137
|
+
* At most ONE entry is ever marked current. Runs over DEVICE rows only (graph
|
|
138
|
+
* rows are never active — the active account is always a device session), so the
|
|
139
|
+
* single-account fallback counts device rows.
|
|
140
|
+
*
|
|
141
|
+
* Pure & side-effect free so it is unit-testable without rendering React.
|
|
142
|
+
*/
|
|
143
|
+
export function markCurrentAccount(
|
|
144
|
+
accounts: SwitchableAccount[],
|
|
145
|
+
activeSessionId: string | null | undefined,
|
|
146
|
+
liveUserId: string | null | undefined,
|
|
147
|
+
isAuthenticated: boolean,
|
|
148
|
+
): SwitchableAccount[] {
|
|
149
|
+
const bySession = accounts.map((account): SwitchableAccount => ({
|
|
150
|
+
...account,
|
|
151
|
+
isCurrent: Boolean(activeSessionId) && account.sessionId === activeSessionId,
|
|
152
|
+
}));
|
|
153
|
+
|
|
154
|
+
if (bySession.some((account) => account.isCurrent) || !isAuthenticated) {
|
|
155
|
+
return bySession;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// No row matched by session id: fall back to the live user's id, marking at
|
|
159
|
+
// most the FIRST matching entry current (never more than one).
|
|
160
|
+
if (liveUserId) {
|
|
161
|
+
let matched = false;
|
|
162
|
+
const byUser = bySession.map((account): SwitchableAccount => {
|
|
163
|
+
if (!matched && account.user.id === liveUserId) {
|
|
164
|
+
matched = true;
|
|
165
|
+
return { ...account, isCurrent: true };
|
|
166
|
+
}
|
|
167
|
+
return account;
|
|
168
|
+
});
|
|
169
|
+
if (matched) {
|
|
170
|
+
return byUser;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Authenticated, nothing matched, but there is exactly one account → it must
|
|
175
|
+
// be the current one.
|
|
176
|
+
if (bySession.length === 1) {
|
|
177
|
+
return [{ ...bySession[0], isCurrent: true }];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return bySession;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Pure union of device sign-ins and account-graph nodes into the flat
|
|
185
|
+
* {@link SwitchableAccount}[] the switchers render. Extracted so the merge /
|
|
186
|
+
* dedup semantics are unit-testable without rendering React.
|
|
187
|
+
*
|
|
188
|
+
* Order: device rows first (in session order, current flagged), then graph-only
|
|
189
|
+
* rows (in graph order). An account present as BOTH a device session and a graph
|
|
190
|
+
* node is deduped into ONE device row enriched with the graph metadata
|
|
191
|
+
* (relationship / kind / parent / membership).
|
|
192
|
+
*/
|
|
193
|
+
export function buildSwitchableAccounts(input: BuildSwitchableAccountsInput): SwitchableAccount[] {
|
|
194
|
+
const {
|
|
195
|
+
sessions,
|
|
196
|
+
activeSessionId,
|
|
197
|
+
liveUser,
|
|
198
|
+
isAuthenticated,
|
|
199
|
+
graph,
|
|
200
|
+
profilesById,
|
|
201
|
+
locale,
|
|
202
|
+
resolveAvatarUrl,
|
|
203
|
+
} = input;
|
|
204
|
+
|
|
205
|
+
// Nothing is switchable when signed out — never surface a lingering device
|
|
206
|
+
// session or graph node once the user is unauthenticated.
|
|
207
|
+
if (!isAuthenticated) {
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const toRow = (
|
|
212
|
+
accountUser: User,
|
|
213
|
+
opts: {
|
|
214
|
+
sessionId?: string;
|
|
215
|
+
authuser?: number;
|
|
216
|
+
relationship?: AccountRelationship;
|
|
217
|
+
kind?: AccountKind;
|
|
218
|
+
parentAccountId?: string | null;
|
|
219
|
+
callerMembership?: AccountMember | null;
|
|
220
|
+
},
|
|
221
|
+
): SwitchableAccount => {
|
|
222
|
+
const displayName = getAccountDisplayName(accountUser, locale);
|
|
223
|
+
const handle = getAccountFallbackHandle(accountUser);
|
|
224
|
+
const secondaryHandle = handle ? `@${handle}` : null;
|
|
225
|
+
return {
|
|
226
|
+
accountId: accountUser.id?.toString() ?? '',
|
|
227
|
+
sessionId: opts.sessionId,
|
|
228
|
+
authuser: opts.authuser,
|
|
229
|
+
isCurrent: Boolean(opts.sessionId) && opts.sessionId === activeSessionId,
|
|
230
|
+
onDevice: Boolean(opts.sessionId),
|
|
231
|
+
relationship: opts.relationship,
|
|
232
|
+
kind: opts.kind,
|
|
233
|
+
parentAccountId: opts.parentAccountId,
|
|
234
|
+
callerMembership: opts.callerMembership,
|
|
235
|
+
displayName,
|
|
236
|
+
// Real email, or the `@handle` fallback (NEVER synthesized).
|
|
237
|
+
email: accountUser.email ?? secondaryHandle,
|
|
238
|
+
avatarUrl: resolveAvatarUrl(accountUser.avatar),
|
|
239
|
+
color: accountUser.color ?? null,
|
|
240
|
+
user: accountUser,
|
|
241
|
+
};
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// --- Device rows ---
|
|
245
|
+
const built = sessions.flatMap((session): SwitchableAccount[] => {
|
|
246
|
+
const isCurrent = session.sessionId === activeSessionId;
|
|
247
|
+
// The active row always uses the live `user` — freshest available —
|
|
248
|
+
// regardless of whether the batch profile fetch has resolved yet.
|
|
249
|
+
const accountUser: User | undefined = isCurrent && liveUser
|
|
250
|
+
? liveUser
|
|
251
|
+
: (session.userId ? profilesById.get(session.userId) : undefined);
|
|
252
|
+
if (!accountUser) {
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
255
|
+
return [toRow(accountUser, { sessionId: session.sessionId, authuser: session.authuser })];
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const flagged = markCurrentAccount(built, activeSessionId, liveUser?.id ?? null, isAuthenticated);
|
|
259
|
+
|
|
260
|
+
// The signed-in user must ALWAYS be represented, even before the device
|
|
261
|
+
// session set has synced (e.g. immediately after cold boot). Synthesize a
|
|
262
|
+
// single current row from the live `useOxy().user` in that case.
|
|
263
|
+
const deviceRows = flagged.length === 0 && isAuthenticated && liveUser && activeSessionId
|
|
264
|
+
? [toRow(liveUser, { sessionId: activeSessionId })]
|
|
265
|
+
: flagged;
|
|
266
|
+
|
|
267
|
+
// --- Merge graph nodes, deduping by account id ---
|
|
268
|
+
const byAccountId = new Map<string, SwitchableAccount>();
|
|
269
|
+
const order: string[] = [];
|
|
270
|
+
const remember = (row: SwitchableAccount): void => {
|
|
271
|
+
if (!row.accountId || byAccountId.has(row.accountId)) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
byAccountId.set(row.accountId, row);
|
|
275
|
+
order.push(row.accountId);
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
for (const row of deviceRows) {
|
|
279
|
+
remember(row);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
for (const node of graph) {
|
|
283
|
+
const existing = byAccountId.get(node.accountId);
|
|
284
|
+
if (existing) {
|
|
285
|
+
// On-device account that is ALSO in the graph: enrich the device row
|
|
286
|
+
// with graph metadata; keep its (freshest) profile + sessionId.
|
|
287
|
+
byAccountId.set(node.accountId, {
|
|
288
|
+
...existing,
|
|
289
|
+
relationship: node.relationship,
|
|
290
|
+
kind: node.kind,
|
|
291
|
+
parentAccountId: node.parentAccountId,
|
|
292
|
+
callerMembership: node.callerMembership,
|
|
293
|
+
});
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
// Graph-only account (owned org / shared, not yet a device session).
|
|
297
|
+
remember(toRow(node.account, {
|
|
298
|
+
relationship: node.relationship,
|
|
299
|
+
kind: node.kind,
|
|
300
|
+
parentAccountId: node.parentAccountId,
|
|
301
|
+
callerMembership: node.callerMembership,
|
|
302
|
+
}));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return order.flatMap((id) => {
|
|
306
|
+
const row = byAccountId.get(id);
|
|
307
|
+
return row ? [row] : [];
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Resolve every account the signed-in user can switch into — device sign-ins
|
|
313
|
+
* AND linked graph accounts (owned orgs + shared-with-you) — as one flat list
|
|
314
|
+
* with real per-account name / email / avatar / color, deduped by account id.
|
|
315
|
+
*
|
|
316
|
+
* ## Data sources
|
|
317
|
+
*
|
|
318
|
+
* - Device sign-ins: `useOxy().sessions` — a `ClientSession[]` projected by
|
|
319
|
+
* `OxyContext.syncFromClient` from the server-authoritative `SessionClient`.
|
|
320
|
+
* Each session carries only `sessionId` + `userId`, so this hook additionally
|
|
321
|
+
* fetches every device account's profile via `oxyServices.getUsersByIds()`.
|
|
322
|
+
* The ACTIVE row always uses `useOxy().user` directly (the freshest copy).
|
|
323
|
+
* - Linked accounts: `useOxy().accounts` — the `AccountNode[]` graph the context
|
|
324
|
+
* loads from `GET /accounts`. Each node already embeds its `account` `User`
|
|
325
|
+
* document, so graph-only rows need no extra fetch.
|
|
326
|
+
*
|
|
327
|
+
* An account present as BOTH a device session and a graph node is deduped into
|
|
328
|
+
* ONE row (see {@link buildSwitchableAccounts}). EVERY row switches the same
|
|
329
|
+
* way — through `useOxy().switchToAccount(row.accountId)`.
|
|
330
|
+
*
|
|
331
|
+
* ## Error handling
|
|
332
|
+
*
|
|
333
|
+
* `getUsersByIds()` resolves to `[]` on a failed chunk (logged internally)
|
|
334
|
+
* rather than throwing. Device accounts whose profile could not be resolved are
|
|
335
|
+
* omitted until a subsequent fetch succeeds (except the active one, which never
|
|
336
|
+
* depends on this fetch); graph accounts are unaffected.
|
|
337
|
+
*/
|
|
338
|
+
export function useSwitchableAccounts(): UseSwitchableAccountsResult {
|
|
339
|
+
const {
|
|
340
|
+
oxyServices,
|
|
341
|
+
sessions,
|
|
342
|
+
activeSessionId,
|
|
343
|
+
user,
|
|
344
|
+
isAuthenticated,
|
|
345
|
+
accounts: graph,
|
|
346
|
+
} = useOxy();
|
|
347
|
+
const { locale } = useI18n();
|
|
348
|
+
|
|
349
|
+
// Every distinct account id carried by the device's session set, sorted for
|
|
350
|
+
// a stable query key regardless of session ordering.
|
|
351
|
+
const accountIds = useMemo<string[]>(() => {
|
|
352
|
+
const ids = new Set<string>();
|
|
353
|
+
for (const session of sessions ?? []) {
|
|
354
|
+
if (session.userId) {
|
|
355
|
+
ids.add(session.userId);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return Array.from(ids).sort();
|
|
359
|
+
}, [sessions]);
|
|
360
|
+
|
|
361
|
+
const profilesQuery = useQuery({
|
|
362
|
+
queryKey: queryKeys.users.list(accountIds),
|
|
363
|
+
queryFn: () => oxyServices.getUsersByIds(accountIds),
|
|
364
|
+
enabled: isAuthenticated && accountIds.length > 0,
|
|
365
|
+
staleTime: 5 * 60 * 1000, // 5 minutes — matches useUserProfile's convention
|
|
366
|
+
gcTime: 30 * 60 * 1000, // 30 minutes
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
const profilesById = useMemo<Map<string, User>>(() => {
|
|
370
|
+
const map = new Map<string, User>();
|
|
371
|
+
for (const profile of profilesQuery.data ?? []) {
|
|
372
|
+
map.set(profile.id, profile);
|
|
373
|
+
}
|
|
374
|
+
return map;
|
|
375
|
+
}, [profilesQuery.data]);
|
|
376
|
+
|
|
377
|
+
const accounts = useMemo<SwitchableAccount[]>(
|
|
378
|
+
() => buildSwitchableAccounts({
|
|
379
|
+
sessions: sessions ?? [],
|
|
380
|
+
activeSessionId: activeSessionId ?? null,
|
|
381
|
+
liveUser: user ?? null,
|
|
382
|
+
isAuthenticated,
|
|
383
|
+
graph: graph ?? [],
|
|
384
|
+
profilesById,
|
|
385
|
+
locale,
|
|
386
|
+
resolveAvatarUrl: (avatar) =>
|
|
387
|
+
(avatar ? oxyServices.getFileDownloadUrl(avatar, 'thumb') : undefined),
|
|
388
|
+
}),
|
|
389
|
+
[sessions, activeSessionId, user, isAuthenticated, graph, profilesById, locale, oxyServices],
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
accounts,
|
|
394
|
+
isLoading: isAuthenticated && accountIds.length > 0 && profilesQuery.isLoading,
|
|
395
|
+
currentSessionId: activeSessionId ?? null,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
@@ -1,237 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* @see https://developer.mozilla.org/en-US/docs/Web/API/FedCM_API
|
|
2
|
+
* Platform detection helper for the web auth/session-sync surfaces.
|
|
3
|
+
*
|
|
4
|
+
* This module previously hosted a FedCM-based `useWebSSO` hook (Federated
|
|
5
|
+
* Credential Management — Chrome-only). It was removed: FedCM is no longer
|
|
6
|
+
* used anywhere in the client sign-in/cold-boot path (see `CrossDomainAuth`'s
|
|
7
|
+
* doc comment in `@oxyhq/core` for the production sign-in loop that motivated
|
|
8
|
+
* the removal). Cross-domain silent SSO is owned entirely by the
|
|
9
|
+
* `silent-iframe` cold-boot step (per-apex `/auth/silent`) plus the terminal
|
|
10
|
+
* `/sso` bounce in `OxyContext`.
|
|
11
|
+
*
|
|
12
|
+
* `isWebBrowser` is kept here (rather than moved) so every existing consumer
|
|
13
|
+
* import path stays valid.
|
|
16
14
|
*/
|
|
17
15
|
|
|
18
|
-
import { useEffect, useRef, useCallback } from 'react';
|
|
19
|
-
import type { OxyServices } from '@oxyhq/core';
|
|
20
|
-
import type { SessionLoginResponse } from '@oxyhq/core';
|
|
21
|
-
import { buildSilentGuardKey } from '../../utils/silentGuardKey';
|
|
22
|
-
|
|
23
|
-
interface UseWebSSOOptions {
|
|
24
|
-
oxyServices: OxyServices;
|
|
25
|
-
onSessionFound: (session: SessionLoginResponse) => Promise<void>;
|
|
26
|
-
onSSOUnavailable?: () => void;
|
|
27
|
-
onError?: (error: Error) => void;
|
|
28
|
-
enabled?: boolean;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
interface UseWebSSOResult {
|
|
32
|
-
/** Manually trigger SSO check */
|
|
33
|
-
checkSSO: () => Promise<SessionLoginResponse | null>;
|
|
34
|
-
/** Trigger interactive FedCM sign-in (shows browser UI) */
|
|
35
|
-
signInWithFedCM: () => Promise<SessionLoginResponse | null>;
|
|
36
|
-
/** Whether SSO check is in progress */
|
|
37
|
-
isChecking: boolean;
|
|
38
|
-
/** Whether FedCM is supported in this browser */
|
|
39
|
-
isFedCMSupported: boolean;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Module-level guard tracking which (origin + API) signatures have already
|
|
44
|
-
* had a silent SSO attempt this page load.
|
|
45
|
-
*
|
|
46
|
-
* A per-component `useRef` guard resets whenever the provider remounts (route
|
|
47
|
-
* churn, StrictMode double-invoke, error-boundary recovery), which previously
|
|
48
|
-
* allowed silent SSO to re-fire and — combined with a routing redirect loop —
|
|
49
|
-
* produced an accelerating `navigator.credentials.get` retry storm. Keying the
|
|
50
|
-
* guard on a stable signature instead of the component instance makes silent
|
|
51
|
-
* SSO fire EXACTLY ONCE per page load regardless of how many times the
|
|
52
|
-
* provider mounts. The set is intentionally never cleared: a fresh page load
|
|
53
|
-
* (the only thing that can change the answer) starts a fresh module scope.
|
|
54
|
-
*/
|
|
55
|
-
const silentSSOAttempted = new Set<string>();
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Build a stable signature for the silent-SSO run-once guard. Two providers
|
|
59
|
-
* pointed at the same API from the same origin share one attempt.
|
|
60
|
-
*/
|
|
61
|
-
function ssoSignature(oxyServices: OxyServices): string {
|
|
62
|
-
// Shared with `OxyContext.silentColdBootKey`. `buildSilentGuardKey` reads
|
|
63
|
-
// `window.location.origin` behind a guard that also verifies
|
|
64
|
-
// `window.location` exists — React Native aliases a global `window` with NO
|
|
65
|
-
// `window.location`, so a `typeof window`-only check would throw
|
|
66
|
-
// `Cannot read property 'origin' of undefined` off-browser.
|
|
67
|
-
return buildSilentGuardKey(() => oxyServices.getBaseURL?.());
|
|
68
|
-
}
|
|
69
|
-
|
|
70
16
|
/**
|
|
71
17
|
* Check if we're running in a web browser environment (not React Native)
|
|
72
18
|
*/
|
|
73
|
-
function isWebBrowser(): boolean {
|
|
19
|
+
export function isWebBrowser(): boolean {
|
|
74
20
|
return typeof window !== 'undefined' &&
|
|
75
21
|
typeof document !== 'undefined' &&
|
|
76
22
|
typeof document.documentElement !== 'undefined';
|
|
77
23
|
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Check if we're on the identity provider domain (where FedCM would authenticate against itself)
|
|
81
|
-
* Compares against config.authWebUrl if set, otherwise defaults to auth.oxy.so
|
|
82
|
-
*/
|
|
83
|
-
function isIdentityProvider(authWebUrl?: string): boolean {
|
|
84
|
-
if (!isWebBrowser()) return false;
|
|
85
|
-
const hostname = window.location.hostname;
|
|
86
|
-
let idpHostname = 'auth.oxy.so';
|
|
87
|
-
if (authWebUrl) {
|
|
88
|
-
try { idpHostname = new URL(authWebUrl).hostname; } catch { /* malformed URL, use default */ }
|
|
89
|
-
}
|
|
90
|
-
return hostname === idpHostname;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Hook for automatic cross-domain web SSO
|
|
95
|
-
*
|
|
96
|
-
* Uses FedCM (Federated Credential Management) - the modern browser-native
|
|
97
|
-
* identity federation API. This is the same technology that powers
|
|
98
|
-
* Google's cross-domain SSO (YouTube, Gmail, Maps, etc.).
|
|
99
|
-
*
|
|
100
|
-
* Key benefits:
|
|
101
|
-
* - Works across different TLDs (alia.onl ↔ mention.earth ↔ homiio.com)
|
|
102
|
-
* - No third-party cookies required
|
|
103
|
-
* - Privacy-preserving (browser mediates identity, IdP can't track)
|
|
104
|
-
* - Automatic silent sign-in after initial authentication
|
|
105
|
-
*
|
|
106
|
-
* For browsers without FedCM (Firefox, older browsers), automatic SSO
|
|
107
|
-
* is not possible. Users will see a sign-in button instead.
|
|
108
|
-
*/
|
|
109
|
-
export function useWebSSO({
|
|
110
|
-
oxyServices,
|
|
111
|
-
onSessionFound,
|
|
112
|
-
onSSOUnavailable,
|
|
113
|
-
onError,
|
|
114
|
-
enabled = true,
|
|
115
|
-
}: UseWebSSOOptions): UseWebSSOResult {
|
|
116
|
-
const isCheckingRef = useRef(false);
|
|
117
|
-
const hasCheckedRef = useRef(false);
|
|
118
|
-
|
|
119
|
-
// Check FedCM support once
|
|
120
|
-
const fedCMSupported = isWebBrowser() && oxyServices.isFedCMSupported?.();
|
|
121
|
-
const authWebUrl = oxyServices.config?.authWebUrl;
|
|
122
|
-
|
|
123
|
-
const checkSSO = useCallback(async (): Promise<SessionLoginResponse | null> => {
|
|
124
|
-
if (!isWebBrowser() || isCheckingRef.current) {
|
|
125
|
-
return null;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Don't use FedCM on the auth domain itself - it would authenticate against itself
|
|
129
|
-
if (isIdentityProvider(authWebUrl)) {
|
|
130
|
-
onSSOUnavailable?.();
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// FedCM is the only reliable cross-domain SSO mechanism
|
|
135
|
-
if (!fedCMSupported) {
|
|
136
|
-
onSSOUnavailable?.();
|
|
137
|
-
return null;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
isCheckingRef.current = true;
|
|
141
|
-
|
|
142
|
-
try {
|
|
143
|
-
const session = await oxyServices.silentSignInWithFedCM?.();
|
|
144
|
-
|
|
145
|
-
if (session) {
|
|
146
|
-
await onSessionFound(session);
|
|
147
|
-
return session;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
onSSOUnavailable?.();
|
|
151
|
-
return null;
|
|
152
|
-
} catch (error) {
|
|
153
|
-
onSSOUnavailable?.();
|
|
154
|
-
onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
155
|
-
return null;
|
|
156
|
-
} finally {
|
|
157
|
-
isCheckingRef.current = false;
|
|
158
|
-
}
|
|
159
|
-
}, [oxyServices, onSessionFound, onSSOUnavailable, onError, fedCMSupported, authWebUrl]);
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Trigger interactive FedCM sign-in
|
|
163
|
-
* This shows the browser's native "Sign in with Oxy" prompt.
|
|
164
|
-
* Use this when silent mediation fails (user hasn't previously consented).
|
|
165
|
-
*/
|
|
166
|
-
const signInWithFedCM = useCallback(async (): Promise<SessionLoginResponse | null> => {
|
|
167
|
-
if (!isWebBrowser() || isCheckingRef.current) {
|
|
168
|
-
return null;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (!fedCMSupported) {
|
|
172
|
-
onError?.(new Error('FedCM is not supported in this browser'));
|
|
173
|
-
return null;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
isCheckingRef.current = true;
|
|
177
|
-
|
|
178
|
-
try {
|
|
179
|
-
const session = await oxyServices.signInWithFedCM?.();
|
|
180
|
-
|
|
181
|
-
if (session) {
|
|
182
|
-
await onSessionFound(session);
|
|
183
|
-
return session;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return null;
|
|
187
|
-
} catch (error) {
|
|
188
|
-
onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
189
|
-
return null;
|
|
190
|
-
} finally {
|
|
191
|
-
isCheckingRef.current = false;
|
|
192
|
-
}
|
|
193
|
-
}, [oxyServices, onSessionFound, onError, fedCMSupported]);
|
|
194
|
-
|
|
195
|
-
// Auto-check SSO on mount (web only, FedCM only, not on auth domain).
|
|
196
|
-
//
|
|
197
|
-
// Run-once is enforced by TWO guards:
|
|
198
|
-
// 1. `hasCheckedRef` — cheap per-instance fast-path so effect re-runs
|
|
199
|
-
// (from changing deps) within one mount never re-fire.
|
|
200
|
-
// 2. `silentSSOAttempted` — module-level, survives remounts/StrictMode so
|
|
201
|
-
// silent SSO fires exactly once per page load even if the provider
|
|
202
|
-
// unmounts and remounts.
|
|
203
|
-
useEffect(() => {
|
|
204
|
-
if (!enabled || !isWebBrowser() || hasCheckedRef.current || isIdentityProvider(authWebUrl)) {
|
|
205
|
-
if (isIdentityProvider(authWebUrl)) {
|
|
206
|
-
onSSOUnavailable?.();
|
|
207
|
-
}
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const signature = ssoSignature(oxyServices);
|
|
212
|
-
if (silentSSOAttempted.has(signature)) {
|
|
213
|
-
// Already attempted this page load (e.g. before a remount) — do not
|
|
214
|
-
// re-fire. Mark the local fast-path too so subsequent re-renders skip.
|
|
215
|
-
hasCheckedRef.current = true;
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
hasCheckedRef.current = true;
|
|
220
|
-
silentSSOAttempted.add(signature);
|
|
221
|
-
|
|
222
|
-
if (fedCMSupported) {
|
|
223
|
-
checkSSO();
|
|
224
|
-
} else {
|
|
225
|
-
onSSOUnavailable?.();
|
|
226
|
-
}
|
|
227
|
-
}, [enabled, checkSSO, fedCMSupported, onSSOUnavailable, oxyServices, authWebUrl]);
|
|
228
|
-
|
|
229
|
-
return {
|
|
230
|
-
checkSSO,
|
|
231
|
-
signInWithFedCM,
|
|
232
|
-
isChecking: isCheckingRef.current,
|
|
233
|
-
isFedCMSupported: fedCMSupported,
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
export { isWebBrowser };
|
package/src/ui/index.ts
CHANGED
|
@@ -33,12 +33,12 @@ import { isCrossApexWeb } from '../../utils/crossApex';
|
|
|
33
33
|
|
|
34
34
|
const OxyAuthScreen: React.FC<BaseScreenProps> = ({ goBack, onAuthenticated }) => {
|
|
35
35
|
const bloomTheme = useTheme();
|
|
36
|
-
const { oxyServices,
|
|
36
|
+
const { oxyServices, handleWebSession, clientId } = useOxy();
|
|
37
37
|
|
|
38
38
|
const { qrData, qrPayload, isLoading, error, isWaiting, openAuthApproval, openSameDeviceApproval, retry } = useOxyAuthSession(
|
|
39
39
|
oxyServices,
|
|
40
40
|
clientId,
|
|
41
|
-
|
|
41
|
+
handleWebSession,
|
|
42
42
|
{ onSignedIn: onAuthenticated },
|
|
43
43
|
);
|
|
44
44
|
|