@oxyhq/services 16.0.0 → 18.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/README.md +77 -98
- package/lib/commonjs/index.js +40 -51
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/client.js +0 -14
- package/lib/commonjs/ui/client.js.map +1 -1
- package/lib/commonjs/ui/components/OxyAccountDialog.js +742 -0
- package/lib/commonjs/ui/components/OxyAccountDialog.js.map +1 -0
- package/lib/commonjs/ui/components/OxyConsentScreen.js +461 -0
- package/lib/commonjs/ui/components/OxyConsentScreen.js.map +1 -0
- package/lib/commonjs/ui/components/OxyProvider.js +13 -6
- package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
- package/lib/commonjs/ui/components/OxySignInButton.js +180 -22
- package/lib/commonjs/ui/components/OxySignInButton.js.map +1 -1
- package/lib/commonjs/ui/components/ProfileButton.js +25 -112
- package/lib/commonjs/ui/components/ProfileButton.js.map +1 -1
- package/lib/commonjs/ui/components/RequireOxyAuth.js +299 -0
- package/lib/commonjs/ui/components/RequireOxyAuth.js.map +1 -0
- package/lib/commonjs/ui/components/oauthNavigation.js +73 -0
- package/lib/commonjs/ui/components/oauthNavigation.js.map +1 -0
- package/lib/commonjs/ui/context/OxyContext.js +122 -4
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js +1 -1
- package/lib/commonjs/ui/hooks/useAuth.js +19 -30
- package/lib/commonjs/ui/hooks/useAuth.js.map +1 -1
- package/lib/commonjs/ui/hooks/useSwitchableAccounts.js +37 -267
- package/lib/commonjs/ui/hooks/useSwitchableAccounts.js.map +1 -1
- package/lib/commonjs/ui/index.js +5 -40
- package/lib/commonjs/ui/index.js.map +1 -1
- package/lib/commonjs/ui/navigation/accountDialogManager.js +91 -0
- package/lib/commonjs/ui/navigation/accountDialogManager.js.map +1 -0
- package/lib/commonjs/ui/navigation/routes.js +0 -2
- package/lib/commonjs/ui/navigation/routes.js.map +1 -1
- package/lib/commonjs/ui/screens/ManageAccountScreen.js +4 -3
- package/lib/commonjs/ui/screens/ManageAccountScreen.js.map +1 -1
- package/lib/commonjs/ui/session/createSessionClient.js +10 -2
- package/lib/commonjs/ui/session/createSessionClient.js.map +1 -1
- package/lib/module/index.js +19 -16
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/client.js +1 -2
- package/lib/module/ui/client.js.map +1 -1
- package/lib/module/ui/components/OxyAccountDialog.js +739 -0
- package/lib/module/ui/components/OxyAccountDialog.js.map +1 -0
- package/lib/module/ui/components/OxyConsentScreen.js +454 -0
- package/lib/module/ui/components/OxyConsentScreen.js.map +1 -0
- package/lib/module/ui/components/OxyProvider.js +13 -6
- package/lib/module/ui/components/OxyProvider.js.map +1 -1
- package/lib/module/ui/components/OxySignInButton.js +181 -22
- package/lib/module/ui/components/OxySignInButton.js.map +1 -1
- package/lib/module/ui/components/ProfileButton.js +26 -112
- package/lib/module/ui/components/ProfileButton.js.map +1 -1
- package/lib/module/ui/components/RequireOxyAuth.js +294 -0
- package/lib/module/ui/components/RequireOxyAuth.js.map +1 -0
- package/lib/module/ui/components/oauthNavigation.js +70 -0
- package/lib/module/ui/components/oauthNavigation.js.map +1 -0
- package/lib/module/ui/context/OxyContext.js +123 -5
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/hooks/mutations/useAccountMutations.js +1 -1
- package/lib/module/ui/hooks/useAuth.js +19 -30
- package/lib/module/ui/hooks/useAuth.js.map +1 -1
- package/lib/module/ui/hooks/useSwitchableAccounts.js +38 -267
- package/lib/module/ui/hooks/useSwitchableAccounts.js.map +1 -1
- package/lib/module/ui/index.js +1 -5
- package/lib/module/ui/index.js.map +1 -1
- package/lib/module/ui/navigation/accountDialogManager.js +81 -0
- package/lib/module/ui/navigation/accountDialogManager.js.map +1 -0
- package/lib/module/ui/navigation/routes.js +0 -2
- package/lib/module/ui/navigation/routes.js.map +1 -1
- package/lib/module/ui/screens/ManageAccountScreen.js +4 -3
- package/lib/module/ui/screens/ManageAccountScreen.js.map +1 -1
- package/lib/module/ui/session/createSessionClient.js +10 -2
- package/lib/module/ui/session/createSessionClient.js.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +8 -12
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/client.d.ts +0 -4
- package/lib/typescript/commonjs/ui/client.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/OxyAccountDialog.d.ts +51 -0
- package/lib/typescript/commonjs/ui/components/OxyAccountDialog.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/OxyConsentScreen.d.ts +45 -0
- package/lib/typescript/commonjs/ui/components/OxyConsentScreen.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/OxyProvider.d.ts +2 -2
- package/lib/typescript/commonjs/ui/components/OxyProvider.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/OxySignInButton.d.ts +56 -0
- package/lib/typescript/commonjs/ui/components/OxySignInButton.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/ProfileButton.d.ts +11 -23
- package/lib/typescript/commonjs/ui/components/ProfileButton.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/RequireOxyAuth.d.ts +55 -0
- package/lib/typescript/commonjs/ui/components/RequireOxyAuth.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/oauthNavigation.d.ts +36 -0
- package/lib/typescript/commonjs/ui/components/oauthNavigation.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +27 -1
- 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/hooks/queries/useServicesQueries.d.ts +1 -1
- package/lib/typescript/commonjs/ui/hooks/queries/useServicesQueries.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useSwitchableAccounts.d.ts +13 -152
- package/lib/typescript/commonjs/ui/hooks/useSwitchableAccounts.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/index.d.ts +2 -5
- package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/navigation/accountDialogManager.d.ts +49 -0
- package/lib/typescript/commonjs/ui/navigation/accountDialogManager.d.ts.map +1 -0
- 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/ManageAccountScreen.d.ts +1 -1
- package/lib/typescript/commonjs/ui/screens/ManageAccountScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts +9 -2
- package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/types/navigation.d.ts +27 -0
- package/lib/typescript/commonjs/ui/types/navigation.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +8 -12
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/client.d.ts +0 -4
- package/lib/typescript/module/ui/client.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/OxyAccountDialog.d.ts +51 -0
- package/lib/typescript/module/ui/components/OxyAccountDialog.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/OxyConsentScreen.d.ts +45 -0
- package/lib/typescript/module/ui/components/OxyConsentScreen.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/OxyProvider.d.ts +2 -2
- package/lib/typescript/module/ui/components/OxyProvider.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/OxySignInButton.d.ts +56 -0
- package/lib/typescript/module/ui/components/OxySignInButton.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ProfileButton.d.ts +11 -23
- package/lib/typescript/module/ui/components/ProfileButton.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/RequireOxyAuth.d.ts +55 -0
- package/lib/typescript/module/ui/components/RequireOxyAuth.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/oauthNavigation.d.ts +36 -0
- package/lib/typescript/module/ui/components/oauthNavigation.d.ts.map +1 -0
- package/lib/typescript/module/ui/context/OxyContext.d.ts +27 -1
- 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/hooks/queries/useServicesQueries.d.ts +1 -1
- package/lib/typescript/module/ui/hooks/queries/useServicesQueries.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useAuth.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useSwitchableAccounts.d.ts +13 -152
- package/lib/typescript/module/ui/hooks/useSwitchableAccounts.d.ts.map +1 -1
- package/lib/typescript/module/ui/index.d.ts +2 -5
- package/lib/typescript/module/ui/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/navigation/accountDialogManager.d.ts +49 -0
- package/lib/typescript/module/ui/navigation/accountDialogManager.d.ts.map +1 -0
- 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/ManageAccountScreen.d.ts +1 -1
- package/lib/typescript/module/ui/screens/ManageAccountScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/session/createSessionClient.d.ts +9 -2
- package/lib/typescript/module/ui/session/createSessionClient.d.ts.map +1 -1
- package/lib/typescript/module/ui/types/navigation.d.ts +27 -0
- package/lib/typescript/module/ui/types/navigation.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +33 -27
- package/src/ui/client.ts +0 -4
- package/src/ui/components/OxyAccountDialog.tsx +735 -0
- package/src/ui/components/OxyConsentScreen.tsx +456 -0
- package/src/ui/components/OxyProvider.tsx +14 -6
- package/src/ui/components/OxySignInButton.tsx +227 -22
- package/src/ui/components/ProfileButton.tsx +23 -126
- package/src/ui/components/RequireOxyAuth.tsx +266 -0
- package/src/ui/components/__tests__/OxyConsentScreen.test.tsx +130 -0
- package/src/ui/components/oauthNavigation.ts +89 -0
- package/src/ui/context/OxyContext.tsx +176 -9
- package/src/ui/context/__tests__/OxyContext.coldBoot.test.tsx +165 -0
- package/src/ui/hooks/mutations/useAccountMutations.ts +1 -1
- package/src/ui/hooks/useAuth.ts +19 -31
- package/src/ui/hooks/useSwitchableAccounts.ts +42 -376
- package/src/ui/index.ts +2 -5
- package/src/ui/navigation/accountDialogManager.ts +84 -0
- package/src/ui/navigation/routes.ts +0 -4
- package/src/ui/screens/ManageAccountScreen.tsx +3 -2
- package/src/ui/session/createSessionClient.ts +9 -1
- package/src/ui/types/navigation.ts +27 -0
- package/lib/commonjs/ui/components/AccountMenu.js +0 -598
- package/lib/commonjs/ui/components/AccountMenu.js.map +0 -1
- package/lib/commonjs/ui/components/AccountMenuButton.js +0 -135
- package/lib/commonjs/ui/components/AccountMenuButton.js.map +0 -1
- package/lib/commonjs/ui/components/AccountSwitcher.js +0 -780
- package/lib/commonjs/ui/components/AccountSwitcher.js.map +0 -1
- package/lib/commonjs/ui/components/AnotherDeviceQR.js +0 -116
- package/lib/commonjs/ui/components/AnotherDeviceQR.js.map +0 -1
- package/lib/commonjs/ui/components/ProfileMenu.js +0 -518
- package/lib/commonjs/ui/components/ProfileMenu.js.map +0 -1
- package/lib/commonjs/ui/components/SignInAccountChooser.js +0 -189
- package/lib/commonjs/ui/components/SignInAccountChooser.js.map +0 -1
- package/lib/commonjs/ui/components/SignInModal.js +0 -600
- package/lib/commonjs/ui/components/SignInModal.js.map +0 -1
- package/lib/commonjs/ui/hooks/useOxyAuthSession.js +0 -501
- package/lib/commonjs/ui/hooks/useOxyAuthSession.js.map +0 -1
- package/lib/commonjs/ui/hooks/usePasswordSignIn.js +0 -170
- package/lib/commonjs/ui/hooks/usePasswordSignIn.js.map +0 -1
- package/lib/commonjs/ui/screens/AccountSwitcherScreen.js +0 -51
- package/lib/commonjs/ui/screens/AccountSwitcherScreen.js.map +0 -1
- package/lib/commonjs/ui/screens/OxyAuthScreen.js +0 -312
- package/lib/commonjs/ui/screens/OxyAuthScreen.js.map +0 -1
- package/lib/commonjs/utils/deviceFlowSignIn.js +0 -109
- package/lib/commonjs/utils/deviceFlowSignIn.js.map +0 -1
- package/lib/module/ui/components/AccountMenu.js +0 -593
- package/lib/module/ui/components/AccountMenu.js.map +0 -1
- package/lib/module/ui/components/AccountMenuButton.js +0 -130
- package/lib/module/ui/components/AccountMenuButton.js.map +0 -1
- package/lib/module/ui/components/AccountSwitcher.js +0 -774
- package/lib/module/ui/components/AccountSwitcher.js.map +0 -1
- package/lib/module/ui/components/AnotherDeviceQR.js +0 -110
- package/lib/module/ui/components/AnotherDeviceQR.js.map +0 -1
- package/lib/module/ui/components/ProfileMenu.js +0 -514
- package/lib/module/ui/components/ProfileMenu.js.map +0 -1
- package/lib/module/ui/components/SignInAccountChooser.js +0 -183
- package/lib/module/ui/components/SignInAccountChooser.js.map +0 -1
- package/lib/module/ui/components/SignInModal.js +0 -591
- package/lib/module/ui/components/SignInModal.js.map +0 -1
- package/lib/module/ui/hooks/useOxyAuthSession.js +0 -495
- package/lib/module/ui/hooks/useOxyAuthSession.js.map +0 -1
- package/lib/module/ui/hooks/usePasswordSignIn.js +0 -165
- package/lib/module/ui/hooks/usePasswordSignIn.js.map +0 -1
- package/lib/module/ui/screens/AccountSwitcherScreen.js +0 -48
- package/lib/module/ui/screens/AccountSwitcherScreen.js.map +0 -1
- package/lib/module/ui/screens/OxyAuthScreen.js +0 -306
- package/lib/module/ui/screens/OxyAuthScreen.js.map +0 -1
- package/lib/module/utils/deviceFlowSignIn.js +0 -106
- package/lib/module/utils/deviceFlowSignIn.js.map +0 -1
- package/lib/typescript/commonjs/ui/components/AccountMenu.d.ts +0 -47
- package/lib/typescript/commonjs/ui/components/AccountMenu.d.ts.map +0 -1
- package/lib/typescript/commonjs/ui/components/AccountMenuButton.d.ts +0 -26
- package/lib/typescript/commonjs/ui/components/AccountMenuButton.d.ts.map +0 -1
- package/lib/typescript/commonjs/ui/components/AccountSwitcher.d.ts +0 -46
- package/lib/typescript/commonjs/ui/components/AccountSwitcher.d.ts.map +0 -1
- package/lib/typescript/commonjs/ui/components/AnotherDeviceQR.d.ts +0 -34
- package/lib/typescript/commonjs/ui/components/AnotherDeviceQR.d.ts.map +0 -1
- package/lib/typescript/commonjs/ui/components/ProfileMenu.d.ts +0 -49
- package/lib/typescript/commonjs/ui/components/ProfileMenu.d.ts.map +0 -1
- package/lib/typescript/commonjs/ui/components/SignInAccountChooser.d.ts +0 -29
- package/lib/typescript/commonjs/ui/components/SignInAccountChooser.d.ts.map +0 -1
- package/lib/typescript/commonjs/ui/components/SignInModal.d.ts +0 -28
- package/lib/typescript/commonjs/ui/components/SignInModal.d.ts.map +0 -1
- package/lib/typescript/commonjs/ui/hooks/useOxyAuthSession.d.ts +0 -105
- package/lib/typescript/commonjs/ui/hooks/useOxyAuthSession.d.ts.map +0 -1
- package/lib/typescript/commonjs/ui/hooks/usePasswordSignIn.d.ts +0 -31
- package/lib/typescript/commonjs/ui/hooks/usePasswordSignIn.d.ts.map +0 -1
- package/lib/typescript/commonjs/ui/screens/AccountSwitcherScreen.d.ts +0 -11
- package/lib/typescript/commonjs/ui/screens/AccountSwitcherScreen.d.ts.map +0 -1
- package/lib/typescript/commonjs/ui/screens/OxyAuthScreen.d.ts +0 -22
- package/lib/typescript/commonjs/ui/screens/OxyAuthScreen.d.ts.map +0 -1
- package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts +0 -97
- package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts.map +0 -1
- package/lib/typescript/module/ui/components/AccountMenu.d.ts +0 -47
- package/lib/typescript/module/ui/components/AccountMenu.d.ts.map +0 -1
- package/lib/typescript/module/ui/components/AccountMenuButton.d.ts +0 -26
- package/lib/typescript/module/ui/components/AccountMenuButton.d.ts.map +0 -1
- package/lib/typescript/module/ui/components/AccountSwitcher.d.ts +0 -46
- package/lib/typescript/module/ui/components/AccountSwitcher.d.ts.map +0 -1
- package/lib/typescript/module/ui/components/AnotherDeviceQR.d.ts +0 -34
- package/lib/typescript/module/ui/components/AnotherDeviceQR.d.ts.map +0 -1
- package/lib/typescript/module/ui/components/ProfileMenu.d.ts +0 -49
- package/lib/typescript/module/ui/components/ProfileMenu.d.ts.map +0 -1
- package/lib/typescript/module/ui/components/SignInAccountChooser.d.ts +0 -29
- package/lib/typescript/module/ui/components/SignInAccountChooser.d.ts.map +0 -1
- package/lib/typescript/module/ui/components/SignInModal.d.ts +0 -28
- package/lib/typescript/module/ui/components/SignInModal.d.ts.map +0 -1
- package/lib/typescript/module/ui/hooks/useOxyAuthSession.d.ts +0 -105
- package/lib/typescript/module/ui/hooks/useOxyAuthSession.d.ts.map +0 -1
- package/lib/typescript/module/ui/hooks/usePasswordSignIn.d.ts +0 -31
- package/lib/typescript/module/ui/hooks/usePasswordSignIn.d.ts.map +0 -1
- package/lib/typescript/module/ui/screens/AccountSwitcherScreen.d.ts +0 -11
- package/lib/typescript/module/ui/screens/AccountSwitcherScreen.d.ts.map +0 -1
- package/lib/typescript/module/ui/screens/OxyAuthScreen.d.ts +0 -22
- package/lib/typescript/module/ui/screens/OxyAuthScreen.d.ts.map +0 -1
- package/lib/typescript/module/utils/deviceFlowSignIn.d.ts +0 -97
- package/lib/typescript/module/utils/deviceFlowSignIn.d.ts.map +0 -1
- package/src/ui/components/AccountMenu.tsx +0 -645
- package/src/ui/components/AccountMenuButton.tsx +0 -126
- package/src/ui/components/AccountSwitcher.tsx +0 -751
- package/src/ui/components/AnotherDeviceQR.tsx +0 -119
- package/src/ui/components/ProfileMenu.tsx +0 -564
- package/src/ui/components/SignInAccountChooser.tsx +0 -162
- package/src/ui/components/SignInModal.tsx +0 -530
- package/src/ui/hooks/useOxyAuthSession.ts +0 -635
- package/src/ui/hooks/usePasswordSignIn.ts +0 -207
- package/src/ui/screens/AccountSwitcherScreen.tsx +0 -43
- package/src/ui/screens/OxyAuthScreen.tsx +0 -324
- package/src/utils/__tests__/deviceFlowSignIn.test.ts +0 -231
- package/src/utils/deviceFlowSignIn.ts +0 -152
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
useState,
|
|
10
10
|
type ReactNode,
|
|
11
11
|
} from 'react';
|
|
12
|
+
import { Linking } from 'react-native';
|
|
12
13
|
import { OxyServices, oxyClient } from '@oxyhq/core';
|
|
13
14
|
import type {
|
|
14
15
|
User,
|
|
@@ -19,14 +20,21 @@ import type {
|
|
|
19
20
|
ClientSession,
|
|
20
21
|
AuthStateStore,
|
|
21
22
|
PersistedAuthState,
|
|
23
|
+
AccountDialogController,
|
|
24
|
+
AccountDialogView,
|
|
22
25
|
} from '@oxyhq/core';
|
|
23
26
|
import {
|
|
24
27
|
KeyManager,
|
|
25
28
|
runSessionColdBoot,
|
|
26
29
|
installAuthRefreshHandler,
|
|
27
30
|
startTokenRefreshScheduler,
|
|
31
|
+
createAccountDialogController,
|
|
28
32
|
logger as loggerUtil,
|
|
29
33
|
} from '@oxyhq/core';
|
|
34
|
+
import {
|
|
35
|
+
registerAccountDialogControls,
|
|
36
|
+
notifyAccountDialogVisibility,
|
|
37
|
+
} from '../navigation/accountDialogManager';
|
|
30
38
|
import { useAuthStore, type AuthState } from '../stores/authStore';
|
|
31
39
|
import { useShallow } from 'zustand/react/shallow';
|
|
32
40
|
import type { UseFollowHook } from '../hooks/useFollow.types';
|
|
@@ -157,6 +165,22 @@ export interface OxyContextState {
|
|
|
157
165
|
showBottomSheet?: (screenOrConfig: RouteName | { screen: RouteName; props?: Record<string, unknown> }) => void;
|
|
158
166
|
openAvatarPicker: () => void;
|
|
159
167
|
|
|
168
|
+
// Unified account dialog (the single switcher + sign-in surface). The headless
|
|
169
|
+
// state machine lives in `@oxyhq/core`; `OxyAccountDialog` (mounted by
|
|
170
|
+
// `OxyProvider`) binds to it. `null` in the no-provider loading state.
|
|
171
|
+
/** The headless controller driving {@link openAccountDialog}. `null` before mount. */
|
|
172
|
+
accountDialogController: AccountDialogController | null;
|
|
173
|
+
/** Whether the unified account dialog is currently presented. */
|
|
174
|
+
isAccountDialogOpen: boolean;
|
|
175
|
+
/**
|
|
176
|
+
* Open the unified account dialog. `accounts` (default) shows the switcher;
|
|
177
|
+
* `signin` / `add` open the sign-in entry. Replaces every prior device/account
|
|
178
|
+
* surface and the standalone sign-in modal.
|
|
179
|
+
*/
|
|
180
|
+
openAccountDialog: (view?: AccountDialogView) => void;
|
|
181
|
+
/** Dismiss the unified account dialog and cancel any in-flight sign-in flow. */
|
|
182
|
+
closeAccountDialog: () => void;
|
|
183
|
+
|
|
160
184
|
// Unified account graph (self, owned orgs/projects/bots, accounts shared with
|
|
161
185
|
// the caller). The cryptographic Commons/DID "identity" is a SEPARATE concept.
|
|
162
186
|
//
|
|
@@ -210,6 +234,20 @@ export interface OxyContextProviderProps {
|
|
|
210
234
|
* for the cross-app device sign-in flow. See {@link OxyContextState.clientId}.
|
|
211
235
|
*/
|
|
212
236
|
clientId?: string;
|
|
237
|
+
/**
|
|
238
|
+
* Whether this provider is the device-first **session authority**. `true`
|
|
239
|
+
* (default) runs `runSessionColdBoot` on mount and opens the signed-out
|
|
240
|
+
* device-state socket — the correct behavior for every Relying Party app.
|
|
241
|
+
*
|
|
242
|
+
* `false` is the IdP host (`auth.oxy.so`) opt-out: the IdP is NOT a session
|
|
243
|
+
* authority (handoff "IdP vs RP"), so it must NOT restore or reproject an
|
|
244
|
+
* ambient device session. With `coldBoot={false}` the cold boot never runs
|
|
245
|
+
* and the signed-out device socket never opens; auth resolves immediately as
|
|
246
|
+
* signed out. Interactive sign-in still commits a normal session on this
|
|
247
|
+
* origin — only the automatic restore/reproject is suppressed.
|
|
248
|
+
* @default true
|
|
249
|
+
*/
|
|
250
|
+
coldBoot?: boolean;
|
|
213
251
|
onAuthStateChange?: (user: User | null) => void;
|
|
214
252
|
onError?: (error: ApiError) => void;
|
|
215
253
|
}
|
|
@@ -312,6 +350,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
312
350
|
authRedirectUri,
|
|
313
351
|
storageKeyPrefix = 'oxy_session',
|
|
314
352
|
clientId: clientIdProp,
|
|
353
|
+
coldBoot = true,
|
|
315
354
|
onAuthStateChange,
|
|
316
355
|
onError,
|
|
317
356
|
}) => {
|
|
@@ -367,6 +406,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
367
406
|
userRef.current = user;
|
|
368
407
|
isAuthenticatedRef.current = isAuthenticated;
|
|
369
408
|
const [initialized, setInitialized] = useState(false);
|
|
409
|
+
const [accountDialogOpen, setAccountDialogOpen] = useState(false);
|
|
370
410
|
const setAuthState = useAuthStore.setState;
|
|
371
411
|
|
|
372
412
|
// Keep the shared `oxyClient` singleton's token store in lockstep with the
|
|
@@ -505,18 +545,47 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
505
545
|
const markAuthResolvedRef = useRef(markAuthResolved);
|
|
506
546
|
markAuthResolvedRef.current = markAuthResolved;
|
|
507
547
|
|
|
548
|
+
// Re-boot indirection: the SessionClient is built in the ref initializer below
|
|
549
|
+
// (before `runColdBoot` is defined), but its `onSessionAppeared` must re-run the
|
|
550
|
+
// cold boot to self-acquire when a sibling signs in on this device. A ref bridges
|
|
551
|
+
// the ordering; it is assigned right after `runColdBoot` is declared.
|
|
552
|
+
const runColdBootRef = useRef<(() => Promise<void>) | null>(null);
|
|
553
|
+
|
|
508
554
|
// Server-authoritative device session client. Built ONCE per `oxyServices`
|
|
509
555
|
// instance. `onUnauthenticated` (a device signout-all pushed over the socket,
|
|
510
556
|
// or bootstrapped as zero-account) clears the persisted store + local state so
|
|
511
557
|
// a reload does not try to restore a session the device no longer has.
|
|
558
|
+
// `signedOutSocketAuth` + `onSessionAppeared` power the signed-out sync channel:
|
|
559
|
+
// an idle tab joins its `device:<id>` room (web via the `oxy_device` cookie,
|
|
560
|
+
// native via the shared device token) and self-acquires on a sibling sign-in.
|
|
512
561
|
const sessionClientPairRef = useRef<ReturnType<typeof createSessionClient> | null>(null);
|
|
513
562
|
if (!sessionClientPairRef.current) {
|
|
514
|
-
sessionClientPairRef.current = createSessionClient(
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
563
|
+
sessionClientPairRef.current = createSessionClient(
|
|
564
|
+
oxyServices,
|
|
565
|
+
authStore,
|
|
566
|
+
() => {
|
|
567
|
+
clearPersistedAuthSafe(authStore, logger);
|
|
568
|
+
void clearSessionStateRef.current().catch((clearError) => {
|
|
569
|
+
logger('Failed to clear local state on remote sign-out', clearError);
|
|
570
|
+
});
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
signedOutSocketAuth: async () => {
|
|
574
|
+
// Web (`*.oxy.so`): the first-party `oxy_device` cookie rides the
|
|
575
|
+
// same-site handshake automatically — just connect.
|
|
576
|
+
if (isWebBrowser()) return true;
|
|
577
|
+
// Native (no cookie jar): present the shared device token if we have one.
|
|
578
|
+
try {
|
|
579
|
+
return (await authStore.loadDeviceToken()) ?? false;
|
|
580
|
+
} catch {
|
|
581
|
+
return false;
|
|
582
|
+
}
|
|
583
|
+
},
|
|
584
|
+
onSessionAppeared: () => {
|
|
585
|
+
void runColdBootRef.current?.();
|
|
586
|
+
},
|
|
587
|
+
},
|
|
588
|
+
);
|
|
520
589
|
}
|
|
521
590
|
const { client: sessionClient, host: sessionClientHost } = sessionClientPairRef.current;
|
|
522
591
|
|
|
@@ -804,6 +873,61 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
804
873
|
[commitSession],
|
|
805
874
|
);
|
|
806
875
|
|
|
876
|
+
// ── Unified account dialog ─────────────────────────────────────────────────
|
|
877
|
+
// The single account-chooser + sign-in surface. Built ONCE per provider mount
|
|
878
|
+
// and bound to the live `oxyServices` + `sessionClient` + this provider's
|
|
879
|
+
// `handleWebSession` commit funnel. `handleWebSession` is threaded through a ref
|
|
880
|
+
// so the controller keeps a STABLE `commitSession` (rebuilding the controller
|
|
881
|
+
// on every commit-identity change would drop its subscription + state).
|
|
882
|
+
const handleWebSessionRef = useRef(handleWebSession);
|
|
883
|
+
handleWebSessionRef.current = handleWebSession;
|
|
884
|
+
|
|
885
|
+
const accountDialogControllerRef = useRef<AccountDialogController | null>(null);
|
|
886
|
+
if (!accountDialogControllerRef.current) {
|
|
887
|
+
accountDialogControllerRef.current = createAccountDialogController({
|
|
888
|
+
oxyServices,
|
|
889
|
+
sessionClient,
|
|
890
|
+
clientId,
|
|
891
|
+
locale: currentLanguage,
|
|
892
|
+
commitSession: (session) => handleWebSessionRef.current(session),
|
|
893
|
+
onSignedIn: () => setAccountDialogOpen(false),
|
|
894
|
+
openUrl: (url) => {
|
|
895
|
+
void Linking.openURL(url);
|
|
896
|
+
},
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
const accountDialogController = accountDialogControllerRef.current;
|
|
900
|
+
|
|
901
|
+
const openAccountDialog = useCallback((view?: AccountDialogView): void => {
|
|
902
|
+
accountDialogControllerRef.current?.setView(view ?? 'accounts');
|
|
903
|
+
setAccountDialogOpen(true);
|
|
904
|
+
}, []);
|
|
905
|
+
|
|
906
|
+
const closeAccountDialog = useCallback((): void => {
|
|
907
|
+
accountDialogControllerRef.current?.cancelSignIn();
|
|
908
|
+
setAccountDialogOpen(false);
|
|
909
|
+
}, []);
|
|
910
|
+
|
|
911
|
+
// Start driving the dialog on mount; tear it down on unmount.
|
|
912
|
+
useEffect(() => {
|
|
913
|
+
const controller = accountDialogControllerRef.current;
|
|
914
|
+
controller?.start();
|
|
915
|
+
return () => controller?.destroy();
|
|
916
|
+
}, []);
|
|
917
|
+
|
|
918
|
+
// Expose the live open/close controls to the imperative manager so
|
|
919
|
+
// `showSignInModal()` (and any app-level imperative "sign in" handler) works.
|
|
920
|
+
useEffect(
|
|
921
|
+
() => registerAccountDialogControls({ open: openAccountDialog, close: closeAccountDialog }),
|
|
922
|
+
[openAccountDialog, closeAccountDialog],
|
|
923
|
+
);
|
|
924
|
+
|
|
925
|
+
// Broadcast visibility so `OxySignInButton`'s "Signing in…" affordance stays
|
|
926
|
+
// accurate regardless of what opened or dismissed the dialog.
|
|
927
|
+
useEffect(() => {
|
|
928
|
+
notifyAccountDialogVisibility(accountDialogOpen);
|
|
929
|
+
}, [accountDialogOpen]);
|
|
930
|
+
|
|
807
931
|
// Keyless password sign-in: username/email + password.
|
|
808
932
|
const signInWithPassword = useCallback(
|
|
809
933
|
async (
|
|
@@ -908,6 +1032,18 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
908
1032
|
},
|
|
909
1033
|
onSignedOut: () => {
|
|
910
1034
|
markAuthResolvedRef.current();
|
|
1035
|
+
// Open the signed-out realtime channel so this idle tab joins its
|
|
1036
|
+
// `device:<id>` room (web `oxy_device` cookie / native device token)
|
|
1037
|
+
// and self-acquires when a sibling signs in. Idempotent, best-effort.
|
|
1038
|
+
void sessionClient.start().catch((startError) => {
|
|
1039
|
+
if (__DEV__) {
|
|
1040
|
+
loggerUtil.debug(
|
|
1041
|
+
'signed-out socket start failed (non-fatal)',
|
|
1042
|
+
{ component: 'OxyContext', method: 'runColdBoot' },
|
|
1043
|
+
startError as unknown,
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
});
|
|
911
1047
|
},
|
|
912
1048
|
onStepError: (id, error) => {
|
|
913
1049
|
if (__DEV__) {
|
|
@@ -931,10 +1067,26 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
931
1067
|
// Backstop: resolve on every exit path so the gate can never hang.
|
|
932
1068
|
markAuthResolvedRef.current();
|
|
933
1069
|
}
|
|
934
|
-
}, [oxyServices, authStore]);
|
|
1070
|
+
}, [oxyServices, authStore, sessionClient]);
|
|
1071
|
+
// Bridge for `onSessionAppeared` (SessionClient) → re-run the cold boot to
|
|
1072
|
+
// self-acquire when a sibling signs in on this device while this tab is idle.
|
|
1073
|
+
runColdBootRef.current = runColdBoot;
|
|
935
1074
|
|
|
936
1075
|
useEffect(() => {
|
|
937
|
-
if (
|
|
1076
|
+
if (initialized) {
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
// IdP mode (`coldBoot={false}`): this provider is NOT the ecosystem session
|
|
1080
|
+
// authority, so it never runs the device-first restore and never opens the
|
|
1081
|
+
// signed-out device-state socket (`runColdBoot` → `onSignedOut` is the sole
|
|
1082
|
+
// place that socket starts). Resolve auth immediately as signed out so there
|
|
1083
|
+
// is no boot spinner; a deliberate sign-in still commits a normal session.
|
|
1084
|
+
if (!coldBoot) {
|
|
1085
|
+
setInitialized(true);
|
|
1086
|
+
markAuthResolved();
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
if (!storage) {
|
|
938
1090
|
return;
|
|
939
1091
|
}
|
|
940
1092
|
setInitialized(true);
|
|
@@ -943,7 +1095,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
943
1095
|
logger('Cold boot failed', error);
|
|
944
1096
|
}
|
|
945
1097
|
});
|
|
946
|
-
}, [runColdBoot, storage, initialized, logger]);
|
|
1098
|
+
}, [coldBoot, runColdBoot, storage, initialized, logger, markAuthResolved]);
|
|
947
1099
|
|
|
948
1100
|
// Exposed `refreshSessions`: re-bootstrap the server-authoritative device
|
|
949
1101
|
// state and reproject — the manual counterpart to the realtime socket.
|
|
@@ -1018,6 +1170,9 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
1018
1170
|
useEffect(() => {
|
|
1019
1171
|
if (isAuthenticated && initialized && tokenReady) {
|
|
1020
1172
|
refreshAccounts();
|
|
1173
|
+
// Reload the dialog's own account graph too, so a session restored OUTSIDE
|
|
1174
|
+
// the dialog (cold boot / password sign-in) surfaces its graph accounts.
|
|
1175
|
+
void accountDialogControllerRef.current?.refresh();
|
|
1021
1176
|
}
|
|
1022
1177
|
}, [isAuthenticated, initialized, tokenReady, refreshAccounts]);
|
|
1023
1178
|
|
|
@@ -1115,6 +1270,10 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
1115
1270
|
useFollow: useFollowHook,
|
|
1116
1271
|
showBottomSheet: showBottomSheetForContext,
|
|
1117
1272
|
openAvatarPicker,
|
|
1273
|
+
accountDialogController,
|
|
1274
|
+
isAccountDialogOpen: accountDialogOpen,
|
|
1275
|
+
openAccountDialog,
|
|
1276
|
+
closeAccountDialog,
|
|
1118
1277
|
accounts,
|
|
1119
1278
|
switchToAccount,
|
|
1120
1279
|
refreshAccounts,
|
|
@@ -1159,6 +1318,10 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
1159
1318
|
useFollowHook,
|
|
1160
1319
|
showBottomSheetForContext,
|
|
1161
1320
|
openAvatarPicker,
|
|
1321
|
+
accountDialogController,
|
|
1322
|
+
accountDialogOpen,
|
|
1323
|
+
openAccountDialog,
|
|
1324
|
+
closeAccountDialog,
|
|
1162
1325
|
accounts,
|
|
1163
1326
|
switchToAccount,
|
|
1164
1327
|
refreshAccounts,
|
|
@@ -1221,6 +1384,10 @@ const LOADING_STATE: OxyContextState = {
|
|
|
1221
1384
|
clientId: null,
|
|
1222
1385
|
oxyServices: LOADING_STATE_OXY_SERVICES,
|
|
1223
1386
|
openAvatarPicker: () => {},
|
|
1387
|
+
accountDialogController: null,
|
|
1388
|
+
isAccountDialogOpen: false,
|
|
1389
|
+
openAccountDialog: () => {},
|
|
1390
|
+
closeAccountDialog: () => {},
|
|
1224
1391
|
accounts: [],
|
|
1225
1392
|
switchToAccount: () => rejectMissingProvider<void>(),
|
|
1226
1393
|
refreshAccounts: () => rejectMissingProvider<void>(),
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `OxyContextProvider` cold-boot opt-out (IdP mode).
|
|
3
|
+
*
|
|
4
|
+
* The provider is the ecosystem's device-first session authority by default:
|
|
5
|
+
* it runs `runSessionColdBoot` on mount and opens the signed-out device-state
|
|
6
|
+
* socket. The IdP host (`auth.oxy.so`) is NOT a session authority, so it mounts
|
|
7
|
+
* the provider with `coldBoot={false}` — the cold boot must never run and the
|
|
8
|
+
* device socket must never open, yet interactive sign-in must still commit a
|
|
9
|
+
* normal session on that origin.
|
|
10
|
+
*/
|
|
11
|
+
import { render, act, waitFor } from '@testing-library/react';
|
|
12
|
+
import type { ReactNode } from 'react';
|
|
13
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
14
|
+
import { OxyServices, runSessionColdBoot } from '@oxyhq/core';
|
|
15
|
+
import type { User } from '@oxyhq/core';
|
|
16
|
+
import type { LoginSessionResult } from '@oxyhq/contracts';
|
|
17
|
+
import { OxyContextProvider, useOxy, type OxyContextState } from '../OxyContext';
|
|
18
|
+
import { createSessionClient } from '../../session';
|
|
19
|
+
|
|
20
|
+
// Spy on the device-first cold boot without touching the rest of core.
|
|
21
|
+
jest.mock('@oxyhq/core', () => {
|
|
22
|
+
const actual = jest.requireActual('@oxyhq/core');
|
|
23
|
+
return {
|
|
24
|
+
__esModule: true,
|
|
25
|
+
...actual,
|
|
26
|
+
runSessionColdBoot: jest.fn(() => Promise.resolve()),
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Replace the real SessionClient with an inert fake so no socket connects and
|
|
31
|
+
// no network is hit. `getState()` returns null, so `syncFromClient` short-
|
|
32
|
+
// circuits and never reaches profile resolution.
|
|
33
|
+
jest.mock('../../session', () => {
|
|
34
|
+
const actual = jest.requireActual('../../session');
|
|
35
|
+
return {
|
|
36
|
+
__esModule: true,
|
|
37
|
+
...actual,
|
|
38
|
+
createSessionClient: jest.fn(() => {
|
|
39
|
+
const listeners = new Set<() => void>();
|
|
40
|
+
const client = {
|
|
41
|
+
getState: jest.fn(() => null),
|
|
42
|
+
subscribe: jest.fn((listener: () => void) => {
|
|
43
|
+
listeners.add(listener);
|
|
44
|
+
return () => {
|
|
45
|
+
listeners.delete(listener);
|
|
46
|
+
};
|
|
47
|
+
}),
|
|
48
|
+
start: jest.fn(() => Promise.resolve()),
|
|
49
|
+
bootstrap: jest.fn(() => Promise.resolve()),
|
|
50
|
+
addCurrentAccount: jest.fn(() => Promise.resolve()),
|
|
51
|
+
registerAndActivate: jest.fn(() => Promise.resolve()),
|
|
52
|
+
switchAccount: jest.fn(() => Promise.resolve()),
|
|
53
|
+
};
|
|
54
|
+
const host = { setCurrentAccountId: jest.fn() };
|
|
55
|
+
return { client, host };
|
|
56
|
+
}),
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const coldBootMock = runSessionColdBoot as jest.Mock;
|
|
61
|
+
const createSessionClientMock = createSessionClient as jest.Mock;
|
|
62
|
+
|
|
63
|
+
function renderProvider(props: { coldBoot?: boolean; oxyServices: OxyServices }) {
|
|
64
|
+
let latest: OxyContextState | null = null;
|
|
65
|
+
const Probe = (): ReactNode => {
|
|
66
|
+
latest = useOxy();
|
|
67
|
+
return null;
|
|
68
|
+
};
|
|
69
|
+
const queryClient = new QueryClient({
|
|
70
|
+
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
|
|
71
|
+
});
|
|
72
|
+
const view = render(
|
|
73
|
+
<QueryClientProvider client={queryClient}>
|
|
74
|
+
<OxyContextProvider oxyServices={props.oxyServices} coldBoot={props.coldBoot}>
|
|
75
|
+
<Probe />
|
|
76
|
+
</OxyContextProvider>
|
|
77
|
+
</QueryClientProvider>,
|
|
78
|
+
);
|
|
79
|
+
return { ...view, getState: (): OxyContextState | null => latest };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The most recently constructed fake SessionClient. */
|
|
83
|
+
function lastSessionClient(): { start: jest.Mock } {
|
|
84
|
+
const results = createSessionClientMock.mock.results;
|
|
85
|
+
return results[results.length - 1].value.client as { start: jest.Mock };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
beforeEach(() => {
|
|
89
|
+
coldBootMock.mockClear();
|
|
90
|
+
createSessionClientMock.mockClear();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('OxyContextProvider coldBoot opt-out (IdP mode)', () => {
|
|
94
|
+
it('coldBoot={false}: never runs the cold boot and resolves auth immediately as signed out', async () => {
|
|
95
|
+
const oxy = new OxyServices({ baseURL: 'https://api.test.local' });
|
|
96
|
+
const { getState } = renderProvider({ coldBoot: false, oxyServices: oxy });
|
|
97
|
+
|
|
98
|
+
await waitFor(() => {
|
|
99
|
+
expect(getState()?.isAuthResolved).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(coldBootMock).not.toHaveBeenCalled();
|
|
103
|
+
expect(getState()?.isAuthenticated).toBe(false);
|
|
104
|
+
expect(getState()?.user).toBeNull();
|
|
105
|
+
// No boot spinner: private-API readiness settles to a definitive "no".
|
|
106
|
+
expect(getState()?.isPrivateApiPending).toBe(false);
|
|
107
|
+
expect(getState()?.canUsePrivateApi).toBe(false);
|
|
108
|
+
// The signed-out device-state socket must never open in IdP mode.
|
|
109
|
+
expect(lastSessionClient().start).not.toHaveBeenCalled();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('coldBoot={false}: interactive password sign-in still commits a normal session', async () => {
|
|
113
|
+
const oxy = new OxyServices({ baseURL: 'https://api.test.local' });
|
|
114
|
+
const loginResult: LoginSessionResult = {
|
|
115
|
+
sessionId: 's-1',
|
|
116
|
+
deviceId: 'd-1',
|
|
117
|
+
expiresAt: new Date(Date.now() + 3_600_000).toISOString(),
|
|
118
|
+
accessToken: 'access-1',
|
|
119
|
+
refreshToken: 'refresh-1',
|
|
120
|
+
user: { id: 'u-1', username: 'alice' },
|
|
121
|
+
};
|
|
122
|
+
jest.spyOn(oxy, 'passwordSignIn').mockResolvedValue(loginResult);
|
|
123
|
+
jest.spyOn(oxy, 'getCurrentUser').mockResolvedValue({
|
|
124
|
+
id: 'u-1',
|
|
125
|
+
username: 'alice',
|
|
126
|
+
name: { displayName: 'Alice' },
|
|
127
|
+
} as User);
|
|
128
|
+
|
|
129
|
+
const { getState } = renderProvider({ coldBoot: false, oxyServices: oxy });
|
|
130
|
+
await waitFor(() => {
|
|
131
|
+
expect(getState()?.isAuthResolved).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const state = getState();
|
|
135
|
+
if (!state) {
|
|
136
|
+
throw new Error('context did not resolve');
|
|
137
|
+
}
|
|
138
|
+
await act(async () => {
|
|
139
|
+
const result = await state.signInWithPassword('alice', 'secret');
|
|
140
|
+
expect(result.status).toBe('ok');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
await waitFor(() => {
|
|
144
|
+
expect(getState()?.isAuthenticated).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
expect(getState()?.user?.id).toBe('u-1');
|
|
147
|
+
expect(oxy.passwordSignIn).toHaveBeenCalledWith(
|
|
148
|
+
'alice',
|
|
149
|
+
'secret',
|
|
150
|
+
expect.objectContaining({ deviceToken: undefined }),
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('default (coldBoot omitted): runs the device-first cold boot', async () => {
|
|
155
|
+
const oxy = new OxyServices({ baseURL: 'https://api.test.local' });
|
|
156
|
+
const { getState } = renderProvider({ oxyServices: oxy });
|
|
157
|
+
|
|
158
|
+
await waitFor(() => {
|
|
159
|
+
expect(coldBootMock).toHaveBeenCalledTimes(1);
|
|
160
|
+
});
|
|
161
|
+
await waitFor(() => {
|
|
162
|
+
expect(getState()?.isAuthResolved).toBe(true);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -109,7 +109,7 @@ export const useUpdateProfile = () => {
|
|
|
109
109
|
refreshAvatarInStore(activeSessionId, updates.avatar, oxyServices);
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
// Invalidate all related queries so every consumer (
|
|
112
|
+
// Invalidate all related queries so every consumer (the account dialog,
|
|
113
113
|
// session lists, managed accounts, etc.) refetches the fresh profile.
|
|
114
114
|
// This is critical right after `username` is set the first time, when
|
|
115
115
|
// every cached "session profile" still reports the user as unnamed.
|
package/src/ui/hooks/useAuth.ts
CHANGED
|
@@ -23,11 +23,10 @@
|
|
|
23
23
|
* - Manual sign-in: signIn() redirects to the IdP (web) or opens the auth sheet (native)
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
import { useCallback
|
|
26
|
+
import { useCallback } from 'react';
|
|
27
27
|
import { useOxy } from '../context/OxyContext';
|
|
28
28
|
import type { User } from '@oxyhq/core';
|
|
29
29
|
import { isWebBrowser } from '../utils/isWebBrowser';
|
|
30
|
-
import { showSignInModal } from '../components/SignInModal';
|
|
31
30
|
|
|
32
31
|
export interface AuthState {
|
|
33
32
|
/** Current authenticated user, null if not authenticated */
|
|
@@ -138,44 +137,33 @@ export function useAuth(): UseAuthReturn {
|
|
|
138
137
|
getPublicKey,
|
|
139
138
|
showBottomSheet,
|
|
140
139
|
openAvatarPicker,
|
|
140
|
+
openAccountDialog,
|
|
141
141
|
} = useOxy();
|
|
142
142
|
|
|
143
143
|
const signIn = useCallback(async (publicKey?: string): Promise<User> => {
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
// already restored a session if one existed; an explicit click presents the
|
|
147
|
-
// SDK sign-in surface (password / QR device flow / add account).
|
|
148
|
-
if (isWebBrowser() && !publicKey) {
|
|
149
|
-
showSignInModal();
|
|
150
|
-
// Resolves when the modal commits a session; the caller typically reacts
|
|
151
|
-
// to `isAuthenticated` rather than this promise.
|
|
152
|
-
return new Promise<User>(() => undefined);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Native: use the cryptographic identity directly when a public key is given.
|
|
144
|
+
// Native: sign in directly with the cryptographic identity when a public key
|
|
145
|
+
// is provided, or an existing keychain identity is found.
|
|
156
146
|
if (publicKey) {
|
|
157
147
|
return oxySignIn(publicKey);
|
|
158
148
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
149
|
+
if (!isWebBrowser()) {
|
|
150
|
+
const hasExisting = await hasIdentity();
|
|
151
|
+
if (hasExisting) {
|
|
152
|
+
const existingKey = await getPublicKey();
|
|
153
|
+
if (existingKey) {
|
|
154
|
+
return oxySignIn(existingKey);
|
|
155
|
+
}
|
|
166
156
|
}
|
|
167
157
|
}
|
|
168
158
|
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
throw new Error('No authentication method available');
|
|
178
|
-
}, [oxySignIn, hasIdentity, getPublicKey, showBottomSheet]);
|
|
159
|
+
// Web, or native without a keychain identity: open the unified account dialog
|
|
160
|
+
// on its sign-in view (device flow / QR / password hand-off). There is NO
|
|
161
|
+
// automatic navigation to a login page — the device-first cold boot already
|
|
162
|
+
// restored a session if one existed. The caller reacts to `isAuthenticated`,
|
|
163
|
+
// so this promise intentionally never resolves.
|
|
164
|
+
openAccountDialog('signin');
|
|
165
|
+
return new Promise<User>(() => undefined);
|
|
166
|
+
}, [oxySignIn, hasIdentity, getPublicKey, openAccountDialog]);
|
|
179
167
|
|
|
180
168
|
const signOut = useCallback(async (): Promise<void> => {
|
|
181
169
|
await logout();
|