@oxyhq/services 10.2.0 → 10.2.2
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 +9 -13
- package/lib/commonjs/index.js +10 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/components/AccountMenu.js +297 -226
- package/lib/commonjs/ui/components/AccountMenu.js.map +1 -1
- package/lib/commonjs/ui/components/AccountMenuButton.js.map +1 -1
- package/lib/commonjs/ui/components/FollowButton.js +3 -1
- package/lib/commonjs/ui/components/FollowButton.js.map +1 -1
- package/lib/commonjs/ui/components/OxySignInButton.js +1 -1
- package/lib/commonjs/ui/components/SignInModal.js +11 -12
- package/lib/commonjs/ui/components/SignInModal.js.map +1 -1
- package/lib/commonjs/ui/components/accountMenuRows.js +18 -30
- package/lib/commonjs/ui/components/accountMenuRows.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +57 -78
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js +7 -13
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/commonjs/ui/hooks/useAuth.js +10 -40
- package/lib/commonjs/ui/hooks/useAuth.js.map +1 -1
- package/lib/commonjs/ui/hooks/useDeviceAccounts.js +285 -0
- package/lib/commonjs/ui/hooks/useDeviceAccounts.js.map +1 -0
- package/lib/commonjs/ui/hooks/useFollow.js +21 -7
- package/lib/commonjs/ui/hooks/useFollow.js.map +1 -1
- package/lib/commonjs/ui/hooks/useSessionManagement.js +5 -6
- package/lib/commonjs/ui/hooks/useSessionManagement.js.map +1 -1
- package/lib/commonjs/ui/hooks/useSessionSocket.js +4 -5
- package/lib/commonjs/ui/hooks/useSessionSocket.js.map +1 -1
- package/lib/commonjs/ui/hooks/useWebSSO.js +1 -1
- package/lib/commonjs/ui/navigation/routes.js +7 -7
- package/lib/commonjs/ui/navigation/routes.js.map +1 -1
- package/lib/commonjs/ui/screens/OxyAuthScreen.js +6 -7
- package/lib/commonjs/ui/screens/OxyAuthScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/ProfileScreen.js +18 -20
- package/lib/commonjs/ui/screens/ProfileScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js +4 -4
- package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/{karma/KarmaAboutScreen.js → trust/TrustAboutScreen.js} +11 -11
- package/lib/commonjs/ui/screens/trust/TrustAboutScreen.js.map +1 -0
- package/lib/commonjs/ui/screens/{karma/KarmaCenterScreen.js → trust/TrustCenterScreen.js} +91 -41
- package/lib/commonjs/ui/screens/trust/TrustCenterScreen.js.map +1 -0
- package/lib/commonjs/ui/screens/{karma/KarmaFAQScreen.js → trust/TrustFAQScreen.js} +11 -11
- package/lib/commonjs/ui/screens/{karma/KarmaFAQScreen.js.map → trust/TrustFAQScreen.js.map} +1 -1
- package/lib/commonjs/ui/screens/{karma/KarmaLeaderboardScreen.js → trust/TrustLeaderboardScreen.js} +63 -42
- package/lib/commonjs/ui/screens/trust/TrustLeaderboardScreen.js.map +1 -0
- package/lib/commonjs/ui/screens/{karma/KarmaRewardsScreen.js → trust/TrustRewardsScreen.js} +54 -54
- package/lib/commonjs/ui/screens/trust/TrustRewardsScreen.js.map +1 -0
- package/lib/commonjs/ui/screens/{karma/KarmaRulesScreen.js → trust/TrustRulesScreen.js} +45 -16
- package/lib/commonjs/ui/screens/trust/TrustRulesScreen.js.map +1 -0
- package/lib/commonjs/ui/screens/trust/trustTier.js +23 -0
- package/lib/commonjs/ui/screens/trust/trustTier.js.map +1 -0
- package/lib/commonjs/utils/deviceFlowSignIn.js +12 -10
- package/lib/commonjs/utils/deviceFlowSignIn.js.map +1 -1
- package/lib/module/index.js +3 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/components/AccountMenu.js +297 -226
- package/lib/module/ui/components/AccountMenu.js.map +1 -1
- package/lib/module/ui/components/AccountMenuButton.js.map +1 -1
- package/lib/module/ui/components/FollowButton.js +3 -1
- package/lib/module/ui/components/FollowButton.js.map +1 -1
- package/lib/module/ui/components/OxySignInButton.js +1 -1
- package/lib/module/ui/components/SignInModal.js +11 -12
- package/lib/module/ui/components/SignInModal.js.map +1 -1
- package/lib/module/ui/components/accountMenuRows.js +18 -30
- package/lib/module/ui/components/accountMenuRows.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +58 -79
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/context/hooks/useAuthOperations.js +7 -13
- package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/module/ui/hooks/useAuth.js +10 -40
- package/lib/module/ui/hooks/useAuth.js.map +1 -1
- package/lib/module/ui/hooks/useDeviceAccounts.js +281 -0
- package/lib/module/ui/hooks/useDeviceAccounts.js.map +1 -0
- package/lib/module/ui/hooks/useFollow.js +21 -7
- package/lib/module/ui/hooks/useFollow.js.map +1 -1
- package/lib/module/ui/hooks/useSessionManagement.js +5 -6
- package/lib/module/ui/hooks/useSessionManagement.js.map +1 -1
- package/lib/module/ui/hooks/useSessionSocket.js +4 -5
- package/lib/module/ui/hooks/useSessionSocket.js.map +1 -1
- package/lib/module/ui/hooks/useWebSSO.js +1 -1
- package/lib/module/ui/navigation/routes.js +7 -7
- package/lib/module/ui/navigation/routes.js.map +1 -1
- package/lib/module/ui/screens/OxyAuthScreen.js +6 -7
- package/lib/module/ui/screens/OxyAuthScreen.js.map +1 -1
- package/lib/module/ui/screens/ProfileScreen.js +18 -20
- package/lib/module/ui/screens/ProfileScreen.js.map +1 -1
- package/lib/module/ui/screens/WelcomeNewUserScreen.js +4 -4
- package/lib/module/ui/screens/WelcomeNewUserScreen.js.map +1 -1
- package/lib/module/ui/screens/{karma/KarmaAboutScreen.js → trust/TrustAboutScreen.js} +11 -11
- package/lib/module/ui/screens/trust/TrustAboutScreen.js.map +1 -0
- package/lib/module/ui/screens/{karma/KarmaCenterScreen.js → trust/TrustCenterScreen.js} +92 -42
- package/lib/module/ui/screens/trust/TrustCenterScreen.js.map +1 -0
- package/lib/module/ui/screens/{karma/KarmaFAQScreen.js → trust/TrustFAQScreen.js} +11 -11
- package/lib/module/ui/screens/{karma/KarmaFAQScreen.js.map → trust/TrustFAQScreen.js.map} +1 -1
- package/lib/module/ui/screens/{karma/KarmaLeaderboardScreen.js → trust/TrustLeaderboardScreen.js} +63 -42
- package/lib/module/ui/screens/trust/TrustLeaderboardScreen.js.map +1 -0
- package/lib/module/ui/screens/{karma/KarmaRewardsScreen.js → trust/TrustRewardsScreen.js} +54 -54
- package/lib/module/ui/screens/trust/TrustRewardsScreen.js.map +1 -0
- package/lib/module/ui/screens/{karma/KarmaRulesScreen.js → trust/TrustRulesScreen.js} +45 -16
- package/lib/module/ui/screens/trust/TrustRulesScreen.js.map +1 -0
- package/lib/module/ui/screens/trust/trustTier.js +19 -0
- package/lib/module/ui/screens/trust/trustTier.js.map +1 -0
- package/lib/module/utils/deviceFlowSignIn.js +13 -10
- package/lib/module/utils/deviceFlowSignIn.js.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +3 -1
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/AccountMenu.d.ts +30 -10
- package/lib/typescript/commonjs/ui/components/AccountMenu.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/SignInModal.d.ts +1 -1
- package/lib/typescript/commonjs/ui/components/SignInModal.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/accountMenuRows.d.ts +19 -12
- package/lib/typescript/commonjs/ui/components/accountMenuRows.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +3 -3
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +1 -7
- package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts +3 -7
- package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useDeviceAccounts.d.ts +133 -0
- package/lib/typescript/commonjs/ui/hooks/useDeviceAccounts.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/hooks/useFollow.d.ts +1 -1
- package/lib/typescript/commonjs/ui/hooks/useFollow.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useSessionManagement.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useSessionSocket.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useWebSSO.d.ts +1 -1
- package/lib/typescript/commonjs/ui/navigation/routes.d.ts +1 -1
- package/lib/typescript/commonjs/ui/screens/OxyAuthScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/ProfileScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/trust/TrustAboutScreen.d.ts +5 -0
- package/lib/typescript/commonjs/ui/screens/{karma/KarmaAboutScreen.d.ts.map → trust/TrustAboutScreen.d.ts.map} +1 -1
- package/lib/typescript/commonjs/ui/screens/trust/TrustCenterScreen.d.ts +5 -0
- package/lib/typescript/commonjs/ui/screens/trust/TrustCenterScreen.d.ts.map +1 -0
- package/lib/typescript/{module/ui/screens/karma/KarmaFAQScreen.d.ts → commonjs/ui/screens/trust/TrustFAQScreen.d.ts} +1 -1
- package/lib/typescript/commonjs/ui/screens/trust/TrustFAQScreen.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/screens/trust/TrustLeaderboardScreen.d.ts +5 -0
- package/lib/typescript/commonjs/ui/screens/trust/TrustLeaderboardScreen.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/screens/trust/TrustRewardsScreen.d.ts +5 -0
- package/lib/typescript/commonjs/ui/screens/{karma/KarmaRewardsScreen.d.ts.map → trust/TrustRewardsScreen.d.ts.map} +1 -1
- package/lib/typescript/commonjs/ui/screens/trust/TrustRulesScreen.d.ts +5 -0
- package/lib/typescript/commonjs/ui/screens/trust/TrustRulesScreen.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/screens/trust/trustTier.d.ts +9 -0
- package/lib/typescript/commonjs/ui/screens/trust/trustTier.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/types/navigation.d.ts +1 -1
- package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts +11 -9
- package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +3 -1
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/AccountMenu.d.ts +30 -10
- package/lib/typescript/module/ui/components/AccountMenu.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/SignInModal.d.ts +1 -1
- package/lib/typescript/module/ui/components/SignInModal.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/accountMenuRows.d.ts +19 -12
- package/lib/typescript/module/ui/components/accountMenuRows.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/OxyContext.d.ts +3 -3
- package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +1 -7
- package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useAuth.d.ts +3 -7
- package/lib/typescript/module/ui/hooks/useAuth.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useDeviceAccounts.d.ts +133 -0
- package/lib/typescript/module/ui/hooks/useDeviceAccounts.d.ts.map +1 -0
- package/lib/typescript/module/ui/hooks/useFollow.d.ts +1 -1
- package/lib/typescript/module/ui/hooks/useFollow.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useSessionManagement.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useSessionSocket.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useWebSSO.d.ts +1 -1
- package/lib/typescript/module/ui/navigation/routes.d.ts +1 -1
- package/lib/typescript/module/ui/screens/OxyAuthScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/ProfileScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/trust/TrustAboutScreen.d.ts +5 -0
- package/lib/typescript/module/ui/screens/{karma/KarmaAboutScreen.d.ts.map → trust/TrustAboutScreen.d.ts.map} +1 -1
- package/lib/typescript/module/ui/screens/trust/TrustCenterScreen.d.ts +5 -0
- package/lib/typescript/module/ui/screens/trust/TrustCenterScreen.d.ts.map +1 -0
- package/lib/typescript/{commonjs/ui/screens/karma/KarmaFAQScreen.d.ts → module/ui/screens/trust/TrustFAQScreen.d.ts} +1 -1
- package/lib/typescript/module/ui/screens/trust/TrustFAQScreen.d.ts.map +1 -0
- package/lib/typescript/module/ui/screens/trust/TrustLeaderboardScreen.d.ts +5 -0
- package/lib/typescript/module/ui/screens/trust/TrustLeaderboardScreen.d.ts.map +1 -0
- package/lib/typescript/module/ui/screens/trust/TrustRewardsScreen.d.ts +5 -0
- package/lib/typescript/module/ui/screens/{karma/KarmaRewardsScreen.d.ts.map → trust/TrustRewardsScreen.d.ts.map} +1 -1
- package/lib/typescript/module/ui/screens/trust/TrustRulesScreen.d.ts +5 -0
- package/lib/typescript/module/ui/screens/trust/TrustRulesScreen.d.ts.map +1 -0
- package/lib/typescript/module/ui/screens/trust/trustTier.d.ts +9 -0
- package/lib/typescript/module/ui/screens/trust/trustTier.d.ts.map +1 -0
- package/lib/typescript/module/ui/types/navigation.d.ts +1 -1
- package/lib/typescript/module/utils/deviceFlowSignIn.d.ts +11 -9
- package/lib/typescript/module/utils/deviceFlowSignIn.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +10 -1
- package/src/ui/components/AccountMenu.tsx +311 -253
- package/src/ui/components/AccountMenuButton.tsx +2 -2
- package/src/ui/components/FollowButton.tsx +2 -2
- package/src/ui/components/OxySignInButton.tsx +1 -1
- package/src/ui/components/SignInModal.tsx +13 -14
- package/src/ui/components/accountMenuRows.ts +28 -40
- package/src/ui/context/OxyContext.tsx +71 -74
- package/src/ui/context/hooks/useAuthOperations.ts +7 -13
- package/src/ui/hooks/useAuth.ts +12 -49
- package/src/ui/hooks/useDeviceAccounts.ts +348 -0
- package/src/ui/hooks/useFollow.ts +16 -8
- package/src/ui/hooks/useSessionManagement.ts +5 -14
- package/src/ui/hooks/useSessionSocket.ts +4 -5
- package/src/ui/hooks/useWebSSO.ts +1 -1
- package/src/ui/navigation/routes.ts +13 -13
- package/src/ui/screens/OxyAuthScreen.tsx +6 -7
- package/src/ui/screens/ProfileScreen.tsx +15 -17
- package/src/ui/screens/WelcomeNewUserScreen.tsx +2 -2
- package/src/ui/screens/{karma/KarmaAboutScreen.tsx → trust/TrustAboutScreen.tsx} +15 -15
- package/src/ui/screens/{karma/KarmaCenterScreen.tsx → trust/TrustCenterScreen.tsx} +87 -41
- package/src/ui/screens/{karma/KarmaFAQScreen.tsx → trust/TrustFAQScreen.tsx} +10 -10
- package/src/ui/screens/trust/TrustLeaderboardScreen.tsx +101 -0
- package/src/ui/screens/{karma/KarmaRewardsScreen.tsx → trust/TrustRewardsScreen.tsx} +54 -54
- package/src/ui/screens/{karma/KarmaRulesScreen.tsx → trust/TrustRulesScreen.tsx} +27 -13
- package/src/ui/screens/trust/trustTier.ts +20 -0
- package/src/ui/types/navigation.ts +1 -2
- package/src/utils/__tests__/deviceFlowSignIn.test.ts +2 -3
- package/src/utils/deviceFlowSignIn.ts +18 -12
- package/lib/commonjs/ui/screens/karma/KarmaAboutScreen.js.map +0 -1
- package/lib/commonjs/ui/screens/karma/KarmaCenterScreen.js.map +0 -1
- package/lib/commonjs/ui/screens/karma/KarmaLeaderboardScreen.js.map +0 -1
- package/lib/commonjs/ui/screens/karma/KarmaRewardsScreen.js.map +0 -1
- package/lib/commonjs/ui/screens/karma/KarmaRulesScreen.js.map +0 -1
- package/lib/module/ui/screens/karma/KarmaAboutScreen.js.map +0 -1
- package/lib/module/ui/screens/karma/KarmaCenterScreen.js.map +0 -1
- package/lib/module/ui/screens/karma/KarmaLeaderboardScreen.js.map +0 -1
- package/lib/module/ui/screens/karma/KarmaRewardsScreen.js.map +0 -1
- package/lib/module/ui/screens/karma/KarmaRulesScreen.js.map +0 -1
- package/lib/typescript/commonjs/ui/screens/karma/KarmaAboutScreen.d.ts +0 -5
- package/lib/typescript/commonjs/ui/screens/karma/KarmaCenterScreen.d.ts +0 -5
- package/lib/typescript/commonjs/ui/screens/karma/KarmaCenterScreen.d.ts.map +0 -1
- package/lib/typescript/commonjs/ui/screens/karma/KarmaFAQScreen.d.ts.map +0 -1
- package/lib/typescript/commonjs/ui/screens/karma/KarmaLeaderboardScreen.d.ts +0 -5
- package/lib/typescript/commonjs/ui/screens/karma/KarmaLeaderboardScreen.d.ts.map +0 -1
- package/lib/typescript/commonjs/ui/screens/karma/KarmaRewardsScreen.d.ts +0 -5
- package/lib/typescript/commonjs/ui/screens/karma/KarmaRulesScreen.d.ts +0 -5
- package/lib/typescript/commonjs/ui/screens/karma/KarmaRulesScreen.d.ts.map +0 -1
- package/lib/typescript/module/ui/screens/karma/KarmaAboutScreen.d.ts +0 -5
- package/lib/typescript/module/ui/screens/karma/KarmaCenterScreen.d.ts +0 -5
- package/lib/typescript/module/ui/screens/karma/KarmaCenterScreen.d.ts.map +0 -1
- package/lib/typescript/module/ui/screens/karma/KarmaFAQScreen.d.ts.map +0 -1
- package/lib/typescript/module/ui/screens/karma/KarmaLeaderboardScreen.d.ts +0 -5
- package/lib/typescript/module/ui/screens/karma/KarmaLeaderboardScreen.d.ts.map +0 -1
- package/lib/typescript/module/ui/screens/karma/KarmaRewardsScreen.d.ts +0 -5
- package/lib/typescript/module/ui/screens/karma/KarmaRulesScreen.d.ts +0 -5
- package/lib/typescript/module/ui/screens/karma/KarmaRulesScreen.d.ts.map +0 -1
- package/src/ui/screens/karma/KarmaLeaderboardScreen.tsx +0 -88
package/src/ui/hooks/useAuth.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* Cross-domain SSO:
|
|
21
21
|
* - Web: Automatic via FedCM (Chrome 108+, Safari 16.4+)
|
|
22
22
|
* - Native: Automatic via shared Keychain/Account Manager
|
|
23
|
-
* - Manual sign-in: signIn()
|
|
23
|
+
* - Manual sign-in: signIn() redirects to the IdP (web) or opens the auth sheet (native)
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import { useCallback, useState } from 'react';
|
|
@@ -60,16 +60,12 @@ export interface AuthState {
|
|
|
60
60
|
export interface AuthActions {
|
|
61
61
|
/**
|
|
62
62
|
* Sign in
|
|
63
|
-
* - Web:
|
|
63
|
+
* - Web: Redirects to auth.oxy.so (no public key needed)
|
|
64
64
|
* - Native: Uses cryptographic identity from keychain
|
|
65
65
|
*
|
|
66
66
|
* @param publicKey - Native: identity public key. Ignored on web.
|
|
67
|
-
* @param preOpenedPopup - Web only: a popup the caller already opened
|
|
68
|
-
* SYNCHRONOUSLY on the raw click via `oxyServices.openBlankPopup()`. This
|
|
69
|
-
* keeps Chrome's user-activation alive across any prior `await` and is
|
|
70
|
-
* the only reliable way to avoid the popup blocker on cross-domain flows.
|
|
71
67
|
*/
|
|
72
|
-
signIn: (publicKey?: string
|
|
68
|
+
signIn: (publicKey?: string) => Promise<User>;
|
|
73
69
|
|
|
74
70
|
/**
|
|
75
71
|
* Sign out current session
|
|
@@ -114,7 +110,6 @@ export function useAuth(): UseAuthReturn {
|
|
|
114
110
|
isAuthResolved,
|
|
115
111
|
error,
|
|
116
112
|
signIn: oxySignIn,
|
|
117
|
-
handlePopupSession,
|
|
118
113
|
logout,
|
|
119
114
|
logoutAll,
|
|
120
115
|
refreshSessions,
|
|
@@ -125,7 +120,7 @@ export function useAuth(): UseAuthReturn {
|
|
|
125
120
|
openAvatarPicker,
|
|
126
121
|
} = useOxy();
|
|
127
122
|
|
|
128
|
-
const signIn = useCallback(async (publicKey?: string
|
|
123
|
+
const signIn = useCallback(async (publicKey?: string): Promise<User> => {
|
|
129
124
|
// Check if we're on the identity provider itself
|
|
130
125
|
// Only the IdP has local login forms - other apps are client apps
|
|
131
126
|
const authWebUrl = oxyServices.config?.authWebUrl;
|
|
@@ -136,44 +131,13 @@ export function useAuth(): UseAuthReturn {
|
|
|
136
131
|
const isIdentityProvider = isWebBrowser() &&
|
|
137
132
|
window.location.hostname === idpHostname;
|
|
138
133
|
|
|
139
|
-
// Web (not on IdP):
|
|
140
|
-
//
|
|
141
|
-
// FedCM silent SSO already runs on page load via useWebSSO
|
|
142
|
-
// If user is clicking "Sign In", they need interactive auth NOW
|
|
134
|
+
// Web (not on IdP): use the tokenless redirect SSO flow. FedCM / silent SSO
|
|
135
|
+
// already run on page load; an explicit click needs interactive auth.
|
|
143
136
|
if (isWebBrowser() && !publicKey && !isIdentityProvider) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
// ahead of their own awaits), reuse it.
|
|
149
|
-
const popup: Window | null = preOpenedPopup
|
|
150
|
-
?? oxyServices.openBlankPopup?.()
|
|
151
|
-
?? null;
|
|
152
|
-
try {
|
|
153
|
-
const popupSession = await oxyServices.signInWithPopup?.({
|
|
154
|
-
popup,
|
|
155
|
-
});
|
|
156
|
-
if (popupSession?.user) {
|
|
157
|
-
// The popup auth flow fetches full user data, so the session user
|
|
158
|
-
// contains full User fields even though the base type is MinimalUserData.
|
|
159
|
-
// Cast to the expected shape for handlePopupSession.
|
|
160
|
-
const sessionWithUser = {
|
|
161
|
-
...popupSession,
|
|
162
|
-
user: popupSession.user as unknown as User,
|
|
163
|
-
};
|
|
164
|
-
await handlePopupSession(sessionWithUser);
|
|
165
|
-
return sessionWithUser.user;
|
|
166
|
-
}
|
|
167
|
-
throw new Error('Sign-in failed. Please try again.');
|
|
168
|
-
} catch (popupError) {
|
|
169
|
-
if (popup && !popup.closed) {
|
|
170
|
-
popup.close();
|
|
171
|
-
}
|
|
172
|
-
if (popupError instanceof Error && popupError.message.includes('blocked')) {
|
|
173
|
-
throw new Error('Popup blocked. Please allow popups for this site.');
|
|
174
|
-
}
|
|
175
|
-
throw popupError;
|
|
176
|
-
}
|
|
137
|
+
oxyServices.signInWithRedirect?.({
|
|
138
|
+
redirectUri: window.location.href,
|
|
139
|
+
});
|
|
140
|
+
return new Promise<User>(() => undefined);
|
|
177
141
|
}
|
|
178
142
|
|
|
179
143
|
// Native: Use cryptographic identity
|
|
@@ -212,7 +176,7 @@ export function useAuth(): UseAuthReturn {
|
|
|
212
176
|
}
|
|
213
177
|
|
|
214
178
|
throw new Error('No authentication method available');
|
|
215
|
-
}, [oxySignIn, hasIdentity, getPublicKey, showBottomSheet, oxyServices
|
|
179
|
+
}, [oxySignIn, hasIdentity, getPublicKey, showBottomSheet, oxyServices]);
|
|
216
180
|
|
|
217
181
|
const signOut = useCallback(async (): Promise<void> => {
|
|
218
182
|
await logout();
|
|
@@ -230,7 +194,7 @@ export function useAuth(): UseAuthReturn {
|
|
|
230
194
|
// State
|
|
231
195
|
user,
|
|
232
196
|
isAuthenticated,
|
|
233
|
-
isLoading,
|
|
197
|
+
isLoading: isLoading || !isAuthResolved,
|
|
234
198
|
isReady: isTokenReady,
|
|
235
199
|
isAuthResolved,
|
|
236
200
|
error,
|
|
@@ -247,4 +211,3 @@ export function useAuth(): UseAuthReturn {
|
|
|
247
211
|
openAvatarPicker,
|
|
248
212
|
};
|
|
249
213
|
}
|
|
250
|
-
|
|
@@ -0,0 +1,348 @@
|
|
|
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
|
+
RefreshAllAccountUser,
|
|
9
|
+
User,
|
|
10
|
+
ClientSession,
|
|
11
|
+
} from '@oxyhq/core';
|
|
12
|
+
import { useOxy } from '../context/OxyContext';
|
|
13
|
+
import { useI18n } from './useI18n';
|
|
14
|
+
import { queryKeys } from './queries/queryKeys';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The per-account user shape carried by a {@link DeviceAccount}. Either:
|
|
18
|
+
* - the minimal projection returned by `POST /auth/refresh-all`
|
|
19
|
+
* ({@link RefreshAllAccountUser}, the shared apex path), or
|
|
20
|
+
* - the SDK's canonical {@link User} document (the active row on the local
|
|
21
|
+
* fallback path, which already has the full user loaded in `useOxy()`).
|
|
22
|
+
*
|
|
23
|
+
* Both satisfy `getAccountDisplayName`'s `DisplayNameUserShape`, so display
|
|
24
|
+
* resolution is uniform across paths.
|
|
25
|
+
*/
|
|
26
|
+
export type DeviceAccountUser = RefreshAllAccountUser | User;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* One signed-in account on this device, fully hydrated for the account
|
|
30
|
+
* chooser. Unlike the old `AccountMenu` behaviour (which only carried the
|
|
31
|
+
* ACTIVE session's user), EVERY entry here carries real per-account
|
|
32
|
+
* `displayName` / `email` / `avatarUrl` / `color`.
|
|
33
|
+
*/
|
|
34
|
+
export interface DeviceAccount {
|
|
35
|
+
/** Session id used to switch to this account on native. */
|
|
36
|
+
sessionId: string;
|
|
37
|
+
/**
|
|
38
|
+
* Device-local refresh-cookie slot index (0..N-1), present only on the
|
|
39
|
+
* shared apex path. Web silent-switch (`refreshTokenViaCookie({ authuser }`)
|
|
40
|
+
* keys off this; absent on the local fallback and on native.
|
|
41
|
+
*/
|
|
42
|
+
authuser?: number;
|
|
43
|
+
/** Whether this account is the currently-active session. */
|
|
44
|
+
isCurrent: boolean;
|
|
45
|
+
/** Friendly display name (never blank — falls back to a handle/sentinel). */
|
|
46
|
+
displayName: string;
|
|
47
|
+
/**
|
|
48
|
+
* Real account email, or `null` when the account genuinely has none.
|
|
49
|
+
* NEVER a synthesized `username@oxy.so` — a missing email stays `null` and
|
|
50
|
+
* the UI falls back to the `@handle` secondary line.
|
|
51
|
+
*/
|
|
52
|
+
email: string | null;
|
|
53
|
+
/** Resolved avatar thumbnail URL, or `undefined` when the account has no avatar. */
|
|
54
|
+
avatarUrl?: string;
|
|
55
|
+
/** Account's preferred Bloom color preset, or `null` when unset. */
|
|
56
|
+
color: string | null;
|
|
57
|
+
/** The underlying per-account user payload (shared projection or full User). */
|
|
58
|
+
user: DeviceAccountUser;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface UseDeviceAccountsResult {
|
|
62
|
+
/** Every account signed in on this device, current one(s) flagged `isCurrent`. */
|
|
63
|
+
accounts: DeviceAccount[];
|
|
64
|
+
/** True until the first detection settles. */
|
|
65
|
+
isLoading: boolean;
|
|
66
|
+
/** The currently-active session id (mirrors `useOxy().activeSessionId`). */
|
|
67
|
+
currentSessionId: string | null;
|
|
68
|
+
/**
|
|
69
|
+
* `true` when the list came from the shared apex path
|
|
70
|
+
* (`refreshAllSessions()` returned >0 accounts — the cross-domain SSO cookie
|
|
71
|
+
* set on `*.oxy.so`). `false` when it came from the local `useOxy()`
|
|
72
|
+
* fallback (native, or cross-domain web where the apex cookie is absent).
|
|
73
|
+
*/
|
|
74
|
+
fromSharedApex: boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Resolve which entries are the current account, robustly.
|
|
79
|
+
*
|
|
80
|
+
* Primary signal: `entry.sessionId === activeSessionId`. On the web shared-apex
|
|
81
|
+
* path, `refreshAllSessions()` returns SERVER-side session ids that may not
|
|
82
|
+
* equal the locally-stored `activeSessionId` (a different storage namespace /
|
|
83
|
+
* server perspective), so a pure `sessionId` match can flag NO row as current —
|
|
84
|
+
* the bug that surfaced as "Not signed in" even while authenticated.
|
|
85
|
+
*
|
|
86
|
+
* Fallbacks, applied only when the `sessionId` match found nothing AND the user
|
|
87
|
+
* is authenticated:
|
|
88
|
+
* 1. Match the live `user.id` against each entry's per-account user id.
|
|
89
|
+
* 2. If still nothing and there is exactly one account, mark that one current.
|
|
90
|
+
*
|
|
91
|
+
* At most ONE entry is ever marked current.
|
|
92
|
+
*
|
|
93
|
+
* Pure & side-effect free so it is unit-testable without rendering React.
|
|
94
|
+
*/
|
|
95
|
+
export function markCurrentAccount(
|
|
96
|
+
accounts: DeviceAccount[],
|
|
97
|
+
activeSessionId: string | null | undefined,
|
|
98
|
+
liveUserId: string | null | undefined,
|
|
99
|
+
isAuthenticated: boolean,
|
|
100
|
+
): DeviceAccount[] {
|
|
101
|
+
const bySession = accounts.map((account): DeviceAccount => ({
|
|
102
|
+
...account,
|
|
103
|
+
isCurrent: Boolean(activeSessionId) && account.sessionId === activeSessionId,
|
|
104
|
+
}));
|
|
105
|
+
|
|
106
|
+
if (bySession.some((account) => account.isCurrent) || !isAuthenticated) {
|
|
107
|
+
return bySession;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// No row matched by session id: fall back to the live user's id, marking at
|
|
111
|
+
// most the FIRST matching entry current (never more than one).
|
|
112
|
+
if (liveUserId) {
|
|
113
|
+
let matched = false;
|
|
114
|
+
const byUser = bySession.map((account): DeviceAccount => {
|
|
115
|
+
if (!matched && account.user.id === liveUserId) {
|
|
116
|
+
matched = true;
|
|
117
|
+
return { ...account, isCurrent: true };
|
|
118
|
+
}
|
|
119
|
+
return account;
|
|
120
|
+
});
|
|
121
|
+
if (matched) {
|
|
122
|
+
return byUser;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Authenticated, nothing matched, but there is exactly one account → it must
|
|
127
|
+
// be the current one (single-account shared-apex / cross-domain case).
|
|
128
|
+
if (bySession.length === 1) {
|
|
129
|
+
return [{ ...bySession[0], isCurrent: true }];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return bySession;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Resolve every account signed in on this device for the unified account
|
|
137
|
+
* switcher, with real per-account name / email / avatar / color.
|
|
138
|
+
*
|
|
139
|
+
* ## Data sources (web vs native)
|
|
140
|
+
*
|
|
141
|
+
* - **Web on a `*.oxy.so` host** → `oxyServices.refreshAllSessions()` returns
|
|
142
|
+
* the full apex device-account list (identical to what `auth.oxy.so` sees,
|
|
143
|
+
* because the `Domain=oxy.so` refresh cookies reach `api.oxy.so`). Used
|
|
144
|
+
* directly — this is the `fromSharedApex: true` path.
|
|
145
|
+
* - **Cross-domain web (non-`oxy.so` apex)** OR **native (no browser cookies)**
|
|
146
|
+
* → `refreshAllSessions()` yields `{ accounts: [] }` (the apex cookies never
|
|
147
|
+
* reach the request: cross-domain by `Domain`, native by having no cookie
|
|
148
|
+
* jar at all; a 401 is also normalised to `{ accounts: [] }` inside the
|
|
149
|
+
* core mixin). In that case we FALL BACK to the SDK's local multi-account
|
|
150
|
+
* list from `useOxy()` (`sessions` + `activeSessionId` + the active `user`).
|
|
151
|
+
* This is the `fromSharedApex: false` path.
|
|
152
|
+
*
|
|
153
|
+
* The fallback decision is purely data-driven: **if `refreshAllSessions()`
|
|
154
|
+
* returns >0 accounts, use the shared path; otherwise fall back to local
|
|
155
|
+
* `useOxy()` sessions.** No host sniffing is needed — the cookie scoping does
|
|
156
|
+
* the discrimination for us, and the same code path works on native (where the
|
|
157
|
+
* fetch returns `{ accounts: [] }`).
|
|
158
|
+
*
|
|
159
|
+
* ## Why React Query (not a `useRef`/`useState` start-once like the auth app)
|
|
160
|
+
*
|
|
161
|
+
* The auth app's `use-device-accounts.ts` hand-rolls a `startedRef` because it
|
|
162
|
+
* has no React Query. The SDK does. `refreshAllSessions()` ROTATES single-use
|
|
163
|
+
* refresh cookies on every call, so it must run AT MOST ONCE per page load:
|
|
164
|
+
* we model that with `staleTime: Infinity` + `gcTime: Infinity` +
|
|
165
|
+
* `refetchOnWindowFocus/Reconnect/Mount: false` + `retry: false`. React Query
|
|
166
|
+
* dedupes concurrent mounts and caches the single result for the page's
|
|
167
|
+
* lifetime — the exact "run once" guarantee the auth app documents, but
|
|
168
|
+
* without the manual ref/state machinery.
|
|
169
|
+
*
|
|
170
|
+
* ## Validation note (intentionally NO zod re-parse)
|
|
171
|
+
*
|
|
172
|
+
* The auth app's hook calls `fetch` directly, so IT must `safeParse` the wire
|
|
173
|
+
* response with `@oxyhq/contracts`. This hook calls
|
|
174
|
+
* `oxyServices.refreshAllSessions()`, whose core mixin ALREADY validates and
|
|
175
|
+
* normalises the wire response (skips entries missing required fields,
|
|
176
|
+
* normalises `authuser` null→0, builds the `RefreshAllAccountUser` shape) — the
|
|
177
|
+
* mixin is the single source of truth. Re-validating the SDK's own already-typed
|
|
178
|
+
* output here would be redundant double-validation, so we do not re-parse.
|
|
179
|
+
*
|
|
180
|
+
* ## Error handling
|
|
181
|
+
*
|
|
182
|
+
* `refreshAllSessions()` already maps 401/404/abort to `{ accounts: [] }`
|
|
183
|
+
* internally. Any OTHER failure (network, 5xx) propagates as a thrown error and
|
|
184
|
+
* is surfaced by React Query (`query.isError`) — never swallowed. Callers that
|
|
185
|
+
* don't care can ignore it; the hook still returns the local fallback list so
|
|
186
|
+
* the chooser stays usable.
|
|
187
|
+
*/
|
|
188
|
+
export function useDeviceAccounts(): UseDeviceAccountsResult {
|
|
189
|
+
const {
|
|
190
|
+
oxyServices,
|
|
191
|
+
sessions,
|
|
192
|
+
activeSessionId,
|
|
193
|
+
user,
|
|
194
|
+
isAuthenticated,
|
|
195
|
+
} = useOxy();
|
|
196
|
+
const { locale } = useI18n();
|
|
197
|
+
|
|
198
|
+
// Stable, per-API-origin query key. `refreshAllSessions` resolves against
|
|
199
|
+
// the session base url derived from `getBaseURL()`, so keying on it scopes
|
|
200
|
+
// the cached result to the API the provider is pointed at.
|
|
201
|
+
const baseURL = oxyServices.getBaseURL();
|
|
202
|
+
|
|
203
|
+
const query = useQuery({
|
|
204
|
+
queryKey: [...queryKeys.accounts.all, 'deviceAccounts', baseURL] as const,
|
|
205
|
+
queryFn: () => oxyServices.refreshAllSessions(),
|
|
206
|
+
// Only attempt the shared apex path while signed in. When logged out
|
|
207
|
+
// there is nothing to enumerate and the fallback (also empty) is used.
|
|
208
|
+
enabled: isAuthenticated,
|
|
209
|
+
// Single-use cookie rotation → run at most once per page load.
|
|
210
|
+
staleTime: Number.POSITIVE_INFINITY,
|
|
211
|
+
gcTime: Number.POSITIVE_INFINITY,
|
|
212
|
+
refetchOnWindowFocus: false,
|
|
213
|
+
refetchOnReconnect: false,
|
|
214
|
+
refetchOnMount: false,
|
|
215
|
+
retry: false,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const sharedAccounts = query.data?.accounts ?? [];
|
|
219
|
+
const fromSharedApex = sharedAccounts.length > 0;
|
|
220
|
+
|
|
221
|
+
const accounts = useMemo<DeviceAccount[]>(() => {
|
|
222
|
+
const resolveAvatarUrl = (avatar: string | null | undefined): string | undefined =>
|
|
223
|
+
avatar ? oxyServices.getFileDownloadUrl(avatar, 'thumb') : undefined;
|
|
224
|
+
|
|
225
|
+
// Build a single current-account row from the live `useOxy().user`. Used
|
|
226
|
+
// as a last-resort fallback when neither the shared-apex probe nor the
|
|
227
|
+
// local session store yielded a row but the user IS authenticated (e.g.
|
|
228
|
+
// a cross-domain host where the apex probe returned empty before the
|
|
229
|
+
// local session store hydrated). The signed-in user must ALWAYS be
|
|
230
|
+
// represented. Email is the REAL email or the `@handle` line — never a
|
|
231
|
+
// synthesized `username@oxy.so`.
|
|
232
|
+
const liveUserRow = (): DeviceAccount[] => {
|
|
233
|
+
if (!isAuthenticated || !user || !activeSessionId) {
|
|
234
|
+
return [];
|
|
235
|
+
}
|
|
236
|
+
const displayName = getAccountDisplayName(user, locale);
|
|
237
|
+
const handle = getAccountFallbackHandle(user);
|
|
238
|
+
const secondaryHandle = handle ? `@${handle}` : null;
|
|
239
|
+
return [{
|
|
240
|
+
sessionId: activeSessionId,
|
|
241
|
+
authuser: undefined,
|
|
242
|
+
isCurrent: true,
|
|
243
|
+
displayName,
|
|
244
|
+
email: user.email ?? secondaryHandle,
|
|
245
|
+
avatarUrl: resolveAvatarUrl(user.avatar),
|
|
246
|
+
color: user.color ?? null,
|
|
247
|
+
user,
|
|
248
|
+
}];
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
let built: DeviceAccount[];
|
|
252
|
+
|
|
253
|
+
if (fromSharedApex) {
|
|
254
|
+
// Shared apex path: every entry carries a real per-account user.
|
|
255
|
+
built = sharedAccounts.map((entry): DeviceAccount => {
|
|
256
|
+
// `entry.user` is non-null on the refresh-all path; the core
|
|
257
|
+
// mixin skips entries without a valid user. The fallback below
|
|
258
|
+
// keeps rendering defensive if a server response is incomplete.
|
|
259
|
+
const accountUser: DeviceAccountUser = entry.user ?? {
|
|
260
|
+
id: '',
|
|
261
|
+
username: '',
|
|
262
|
+
};
|
|
263
|
+
const displayName = getAccountDisplayName(accountUser, locale);
|
|
264
|
+
const handle = getAccountFallbackHandle(accountUser);
|
|
265
|
+
const email = entry.user?.email ?? null;
|
|
266
|
+
const secondaryHandle = handle ? `@${handle}` : null;
|
|
267
|
+
return {
|
|
268
|
+
sessionId: entry.sessionId,
|
|
269
|
+
authuser: entry.authuser,
|
|
270
|
+
// Provisional; finalised by `markCurrentAccount` below so the
|
|
271
|
+
// shared-apex path is robust to server/local session-id skew.
|
|
272
|
+
isCurrent: false,
|
|
273
|
+
displayName,
|
|
274
|
+
// Real email, or null (NEVER synthesized). The UI uses the
|
|
275
|
+
// `@handle` line only when email is genuinely absent.
|
|
276
|
+
email: email ?? secondaryHandle,
|
|
277
|
+
avatarUrl: resolveAvatarUrl(entry.user?.avatar),
|
|
278
|
+
color: entry.user?.color ?? null,
|
|
279
|
+
user: accountUser,
|
|
280
|
+
};
|
|
281
|
+
});
|
|
282
|
+
} else {
|
|
283
|
+
// Local fallback path: build from the SDK's multi-session store. The
|
|
284
|
+
// active session row gets the full loaded `user`; inactive fallback
|
|
285
|
+
// rows carry only what the `ClientSession` exposes (no synthesized
|
|
286
|
+
// identity — they show the active user's data only when active).
|
|
287
|
+
built = (sessions ?? []).map((session: ClientSession): DeviceAccount => {
|
|
288
|
+
const isCurrent = session.sessionId === activeSessionId;
|
|
289
|
+
const accountUser: DeviceAccountUser = isCurrent && user
|
|
290
|
+
? user
|
|
291
|
+
: { id: session.userId ?? '', username: '' };
|
|
292
|
+
const displayName = getAccountDisplayName(accountUser, locale);
|
|
293
|
+
const handle = getAccountFallbackHandle(accountUser);
|
|
294
|
+
const email = isCurrent && user?.email ? user.email : null;
|
|
295
|
+
const secondaryHandle = handle ? `@${handle}` : null;
|
|
296
|
+
const avatar = isCurrent && user ? user.avatar : undefined;
|
|
297
|
+
const color = isCurrent && user?.color ? user.color : null;
|
|
298
|
+
return {
|
|
299
|
+
sessionId: session.sessionId,
|
|
300
|
+
authuser: session.authuser,
|
|
301
|
+
isCurrent,
|
|
302
|
+
displayName,
|
|
303
|
+
email: email ?? secondaryHandle,
|
|
304
|
+
avatarUrl: resolveAvatarUrl(avatar),
|
|
305
|
+
color,
|
|
306
|
+
user: accountUser,
|
|
307
|
+
};
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Robust current-account detection: tolerate server/local session-id
|
|
312
|
+
// skew on the shared-apex path by falling back to the live user's id and
|
|
313
|
+
// the single-account heuristic.
|
|
314
|
+
const flagged = markCurrentAccount(
|
|
315
|
+
built,
|
|
316
|
+
activeSessionId,
|
|
317
|
+
user?.id ?? null,
|
|
318
|
+
isAuthenticated,
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
// The signed-in user must ALWAYS be represented. If detection produced
|
|
322
|
+
// an empty list yet the user is authenticated, synthesize a single
|
|
323
|
+
// current row from the live `useOxy().user`.
|
|
324
|
+
if (flagged.length === 0) {
|
|
325
|
+
return liveUserRow();
|
|
326
|
+
}
|
|
327
|
+
return flagged;
|
|
328
|
+
}, [
|
|
329
|
+
fromSharedApex,
|
|
330
|
+
sharedAccounts,
|
|
331
|
+
sessions,
|
|
332
|
+
activeSessionId,
|
|
333
|
+
user,
|
|
334
|
+
isAuthenticated,
|
|
335
|
+
locale,
|
|
336
|
+
oxyServices,
|
|
337
|
+
]);
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
accounts,
|
|
341
|
+
// `isLoading` only reflects the shared probe while it's the relevant
|
|
342
|
+
// source. Once we know we're on the fallback (probe settled with 0
|
|
343
|
+
// accounts) the local list is synchronously available.
|
|
344
|
+
isLoading: isAuthenticated && query.isLoading,
|
|
345
|
+
currentSessionId: activeSessionId ?? null,
|
|
346
|
+
fromSharedApex,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useCallback, useMemo, useEffect } from 'react';
|
|
2
2
|
import { useFollowStore } from '../stores/followStore';
|
|
3
3
|
import { useOxy } from '../context/OxyContext';
|
|
4
|
-
import type
|
|
4
|
+
import { logger as loggerUtil, type OxyServices } from '@oxyhq/core';
|
|
5
5
|
import { useShallow } from 'zustand/react/shallow';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -18,7 +18,8 @@ import { useShallow } from 'zustand/react/shallow';
|
|
|
18
18
|
* them in selectors would cause unnecessary selector recalculations).
|
|
19
19
|
*/
|
|
20
20
|
export const useFollow = (userId?: string | string[]) => {
|
|
21
|
-
const { oxyServices } = useOxy();
|
|
21
|
+
const { oxyServices, isAuthenticated, isAuthResolved, isTokenReady } = useOxy();
|
|
22
|
+
const canUsePrivateApi = isAuthResolved && isTokenReady && isAuthenticated;
|
|
22
23
|
const userIds = useMemo(() => (Array.isArray(userId) ? userId : userId ? [userId] : []), [userId]);
|
|
23
24
|
const isSingleUser = typeof userId === 'string';
|
|
24
25
|
|
|
@@ -75,9 +76,10 @@ export const useFollow = (userId?: string | string[]) => {
|
|
|
75
76
|
// Store actions are accessed via getState() to avoid subscribing to them.
|
|
76
77
|
const toggleFollow = useCallback(async () => {
|
|
77
78
|
if (!isSingleUser || !userId) throw new Error('toggleFollow is only available for single user mode');
|
|
79
|
+
if (!canUsePrivateApi) throw new Error('Authentication is required to follow users');
|
|
78
80
|
const currentlyFollowing = useFollowStore.getState().followingUsers[userId] ?? false;
|
|
79
81
|
await useFollowStore.getState().toggleFollowUser(userId, oxyServices, currentlyFollowing);
|
|
80
|
-
}, [isSingleUser, userId, oxyServices]);
|
|
82
|
+
}, [isSingleUser, userId, canUsePrivateApi, oxyServices]);
|
|
81
83
|
|
|
82
84
|
const setFollowStatus = useCallback((following: boolean) => {
|
|
83
85
|
if (!isSingleUser || !userId) throw new Error('setFollowStatus is only available for single user mode');
|
|
@@ -86,8 +88,9 @@ export const useFollow = (userId?: string | string[]) => {
|
|
|
86
88
|
|
|
87
89
|
const fetchStatus = useCallback(async () => {
|
|
88
90
|
if (!isSingleUser || !userId) throw new Error('fetchStatus is only available for single user mode');
|
|
91
|
+
if (!canUsePrivateApi) return;
|
|
89
92
|
await useFollowStore.getState().fetchFollowStatus(userId, oxyServices);
|
|
90
|
-
}, [isSingleUser, userId, oxyServices]);
|
|
93
|
+
}, [isSingleUser, userId, canUsePrivateApi, oxyServices]);
|
|
91
94
|
|
|
92
95
|
const clearError = useCallback(() => {
|
|
93
96
|
if (!isSingleUser || !userId) throw new Error('clearError is only available for single user mode');
|
|
@@ -114,28 +117,33 @@ export const useFollow = (userId?: string | string[]) => {
|
|
|
114
117
|
if (!isSingleUser || !userId) return;
|
|
115
118
|
|
|
116
119
|
if ((followerCount === null || followingCount === null) && !isLoadingCounts) {
|
|
117
|
-
fetchUserCounts().catch((
|
|
120
|
+
fetchUserCounts().catch((error: unknown) => {
|
|
121
|
+
loggerUtil.warn('useFollow: fetchUserCounts failed', { component: 'useFollow' }, error);
|
|
122
|
+
});
|
|
118
123
|
}
|
|
119
124
|
}, [isSingleUser, userId, followerCount, followingCount, isLoadingCounts, fetchUserCounts]);
|
|
120
125
|
|
|
121
126
|
// Multi-user callbacks
|
|
122
127
|
const toggleFollowForUser = useCallback(async (targetUserId: string) => {
|
|
128
|
+
if (!canUsePrivateApi) throw new Error('Authentication is required to follow users');
|
|
123
129
|
const currentState = useFollowStore.getState().followingUsers[targetUserId] ?? false;
|
|
124
130
|
await useFollowStore.getState().toggleFollowUser(targetUserId, oxyServices, currentState);
|
|
125
|
-
}, [oxyServices]);
|
|
131
|
+
}, [canUsePrivateApi, oxyServices]);
|
|
126
132
|
|
|
127
133
|
const setFollowStatusForUser = useCallback((targetUserId: string, following: boolean) => {
|
|
128
134
|
useFollowStore.getState().setFollowingStatus(targetUserId, following);
|
|
129
135
|
}, []);
|
|
130
136
|
|
|
131
137
|
const fetchStatusForUser = useCallback(async (targetUserId: string) => {
|
|
138
|
+
if (!canUsePrivateApi) return;
|
|
132
139
|
await useFollowStore.getState().fetchFollowStatus(targetUserId, oxyServices);
|
|
133
|
-
}, [oxyServices]);
|
|
140
|
+
}, [canUsePrivateApi, oxyServices]);
|
|
134
141
|
|
|
135
142
|
const fetchAllStatuses = useCallback(async () => {
|
|
143
|
+
if (!canUsePrivateApi) return;
|
|
136
144
|
const store = useFollowStore.getState();
|
|
137
145
|
await Promise.all(userIds.map(uid => store.fetchFollowStatus(uid, oxyServices)));
|
|
138
|
-
}, [userIds, oxyServices]);
|
|
146
|
+
}, [canUsePrivateApi, userIds, oxyServices]);
|
|
139
147
|
|
|
140
148
|
const clearErrorForUser = useCallback((targetUserId: string) => {
|
|
141
149
|
useFollowStore.getState().clearFollowError(targetUserId);
|
|
@@ -199,7 +199,6 @@ export const useSessionManagement = ({
|
|
|
199
199
|
|
|
200
200
|
const activateSession = useCallback(
|
|
201
201
|
async (sessionId: string, user: User): Promise<void> => {
|
|
202
|
-
await oxyServices.getTokenBySession(sessionId);
|
|
203
202
|
setTokenReady?.(true);
|
|
204
203
|
setActiveSessionId(sessionId);
|
|
205
204
|
loginSuccess(user);
|
|
@@ -207,14 +206,7 @@ export const useSessionManagement = ({
|
|
|
207
206
|
await applyLanguagePreference(user);
|
|
208
207
|
onAuthStateChange?.(user);
|
|
209
208
|
},
|
|
210
|
-
[
|
|
211
|
-
applyLanguagePreference,
|
|
212
|
-
loginSuccess,
|
|
213
|
-
onAuthStateChange,
|
|
214
|
-
oxyServices,
|
|
215
|
-
saveActiveSessionId,
|
|
216
|
-
setTokenReady,
|
|
217
|
-
],
|
|
209
|
+
[applyLanguagePreference, loginSuccess, onAuthStateChange, saveActiveSessionId, setTokenReady],
|
|
218
210
|
);
|
|
219
211
|
|
|
220
212
|
const removalTimerIdsRef = useRef<Set<ReturnType<typeof setTimeout>>>(new Set());
|
|
@@ -271,10 +263,10 @@ export const useSessionManagement = ({
|
|
|
271
263
|
// `refreshAllSessions` it carries its `authuser` slot index. We
|
|
272
264
|
// proactively plant that slot's access token via the httpOnly
|
|
273
265
|
// refresh cookie BEFORE validating, so the bearer-protected
|
|
274
|
-
// validate/getCurrentUser calls
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
// `
|
|
266
|
+
// validate/getCurrentUser calls have the correct in-memory token
|
|
267
|
+
// after a cold reload or account switch in another tab. The native
|
|
268
|
+
// path arrives here only after a bearer has been planted by
|
|
269
|
+
// `claimSessionByToken` or secure shared-session restore.
|
|
278
270
|
if (isWebBrowser()) {
|
|
279
271
|
const targetSession = sessionsRef.current.find((s) => s.sessionId === sessionId);
|
|
280
272
|
const targetAuthuser = targetSession?.authuser;
|
|
@@ -449,4 +441,3 @@ export const useSessionManagement = ({
|
|
|
449
441
|
};
|
|
450
442
|
};
|
|
451
443
|
|
|
452
|
-
|
|
@@ -149,8 +149,7 @@ export function useSessionSocket({
|
|
|
149
149
|
// appear in the switch. Anything unknown falls through to `default`,
|
|
150
150
|
// which is intentionally a no-op for session lifecycle — it only logs
|
|
151
151
|
// in dev. This guards against future server-side event additions
|
|
152
|
-
//
|
|
153
|
-
// "legacy fallback" branch.
|
|
152
|
+
// accidentally triggering sign-out.
|
|
154
153
|
switch (data.type) {
|
|
155
154
|
case 'session_removed': {
|
|
156
155
|
if (data.sessionId && onSessionRemovedRef.current) {
|
|
@@ -199,9 +198,9 @@ export function useSessionSocket({
|
|
|
199
198
|
case 'session_created':
|
|
200
199
|
case 'session_update': {
|
|
201
200
|
// Lifecycle event for the current user. Just resync the sessions
|
|
202
|
-
// list — never sign out.
|
|
203
|
-
//
|
|
204
|
-
//
|
|
201
|
+
// list — never sign out. A prior implementation compared
|
|
202
|
+
// `data.sessionId === currentActiveSessionId` and signed the user
|
|
203
|
+
// out if true, which was catastrophic: a
|
|
205
204
|
// `session_created` event fired immediately after a successful
|
|
206
205
|
// sign-in carries the user's NEW (now-active) session id, which
|
|
207
206
|
// matched and triggered an instant remote sign-out toast on every
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* without relying on third-party cookies.
|
|
9
9
|
*
|
|
10
10
|
* For browsers without FedCM support, users will need to click a sign-in button
|
|
11
|
-
* which triggers
|
|
11
|
+
* which triggers redirect authentication.
|
|
12
12
|
*
|
|
13
13
|
* This is called automatically by OxyContext on web platforms.
|
|
14
14
|
*
|
|
@@ -28,12 +28,12 @@ export type RouteName =
|
|
|
28
28
|
| 'SavesCollections'
|
|
29
29
|
| 'EditProfileField' // Dedicated screen for editing a single profile field
|
|
30
30
|
| 'LearnMoreUsernames' // Informational screen about usernames
|
|
31
|
-
| '
|
|
32
|
-
| '
|
|
33
|
-
| '
|
|
34
|
-
| '
|
|
35
|
-
| '
|
|
36
|
-
| '
|
|
31
|
+
| 'TrustCenter'
|
|
32
|
+
| 'TrustLeaderboard'
|
|
33
|
+
| 'TrustRewards'
|
|
34
|
+
| 'TrustRules'
|
|
35
|
+
| 'AboutTrust'
|
|
36
|
+
| 'TrustFAQ'
|
|
37
37
|
| 'FollowersList' // List of user's followers
|
|
38
38
|
| 'FollowingList' // List of users being followed
|
|
39
39
|
| 'CreateManagedAccount' // Create a new managed sub-account
|
|
@@ -67,13 +67,13 @@ const screenLoaders: Record<RouteName, () => ComponentType<BaseScreenProps>> = {
|
|
|
67
67
|
EditProfileField: () => require('../screens/EditProfileFieldScreen').default,
|
|
68
68
|
// Informational screens
|
|
69
69
|
LearnMoreUsernames: () => require('../screens/LearnMoreUsernamesScreen').default,
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
70
|
+
// Oxy Trust screens
|
|
71
|
+
TrustCenter: () => require('../screens/trust/TrustCenterScreen').default,
|
|
72
|
+
TrustLeaderboard: () => require('../screens/trust/TrustLeaderboardScreen').default,
|
|
73
|
+
TrustRewards: () => require('../screens/trust/TrustRewardsScreen').default,
|
|
74
|
+
TrustRules: () => require('../screens/trust/TrustRulesScreen').default,
|
|
75
|
+
AboutTrust: () => require('../screens/trust/TrustAboutScreen').default,
|
|
76
|
+
TrustFAQ: () => require('../screens/trust/TrustFAQScreen').default,
|
|
77
77
|
// User list screens (followers/following)
|
|
78
78
|
FollowersList: () => require('../screens/FollowersListScreen').default,
|
|
79
79
|
FollowingList: () => require('../screens/FollowingListScreen').default,
|