@oxyhq/services 5.18.5 → 5.20.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 +51 -42
- package/lib/commonjs/core/mixins/OxyServices.fedcm.js +1 -1
- package/lib/commonjs/core/mixins/OxyServices.fedcm.js.map +1 -1
- package/lib/commonjs/ui/components/OxyProvider.js +106 -40
- package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
- package/lib/commonjs/ui/components/OxySignInButton.js +24 -17
- package/lib/commonjs/ui/components/OxySignInButton.js.map +1 -1
- package/lib/commonjs/ui/components/WebOxyProvider.js +13 -5
- package/lib/commonjs/ui/components/WebOxyProvider.js.map +1 -1
- package/lib/commonjs/ui/hooks/useAuth.js +33 -8
- package/lib/commonjs/ui/hooks/useAuth.js.map +1 -1
- package/lib/commonjs/ui/hooks/useWebSSO.js +55 -16
- package/lib/commonjs/ui/hooks/useWebSSO.js.map +1 -1
- package/lib/module/core/mixins/OxyServices.fedcm.js +1 -1
- package/lib/module/core/mixins/OxyServices.fedcm.js.map +1 -1
- package/lib/module/ui/components/OxyProvider.js +106 -39
- package/lib/module/ui/components/OxyProvider.js.map +1 -1
- package/lib/module/ui/components/OxySignInButton.js +24 -17
- package/lib/module/ui/components/OxySignInButton.js.map +1 -1
- package/lib/module/ui/components/WebOxyProvider.js +13 -5
- package/lib/module/ui/components/WebOxyProvider.js.map +1 -1
- package/lib/module/ui/hooks/useAuth.js +33 -8
- package/lib/module/ui/hooks/useAuth.js.map +1 -1
- package/lib/module/ui/hooks/useWebSSO.js +55 -16
- package/lib/module/ui/hooks/useWebSSO.js.map +1 -1
- package/lib/typescript/commonjs/core/mixins/OxyServices.fedcm.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/OxyProvider.d.ts +26 -3
- package/lib/typescript/commonjs/ui/components/OxyProvider.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/OxySignInButton.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/WebOxyProvider.d.ts +13 -5
- package/lib/typescript/commonjs/ui/components/WebOxyProvider.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts +8 -3
- package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useWebSSO.d.ts +29 -7
- package/lib/typescript/commonjs/ui/hooks/useWebSSO.d.ts.map +1 -1
- package/lib/typescript/module/core/mixins/OxyServices.fedcm.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/OxyProvider.d.ts +26 -3
- package/lib/typescript/module/ui/components/OxyProvider.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/OxySignInButton.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/WebOxyProvider.d.ts +13 -5
- package/lib/typescript/module/ui/components/WebOxyProvider.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useAuth.d.ts +8 -3
- package/lib/typescript/module/ui/hooks/useAuth.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useWebSSO.d.ts +29 -7
- package/lib/typescript/module/ui/hooks/useWebSSO.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core/mixins/OxyServices.fedcm.ts +1 -1
- package/src/ui/components/OxyProvider.tsx +112 -47
- package/src/ui/components/OxySignInButton.tsx +24 -17
- package/src/ui/components/WebOxyProvider.tsx +13 -5
- package/src/ui/hooks/useAuth.ts +42 -12
- package/src/ui/hooks/useWebSSO.ts +59 -15
|
@@ -16,6 +16,11 @@
|
|
|
16
16
|
* return <Welcome user={user} />;
|
|
17
17
|
* }
|
|
18
18
|
* ```
|
|
19
|
+
*
|
|
20
|
+
* Cross-domain SSO:
|
|
21
|
+
* - Web: Automatic via FedCM (Chrome 108+, Safari 16.4+)
|
|
22
|
+
* - Native: Automatic via shared Keychain/Account Manager
|
|
23
|
+
* - Manual sign-in: signIn() opens popup (web) or auth sheet (native)
|
|
19
24
|
*/
|
|
20
25
|
import { useOxy } from '../context/OxyContext';
|
|
21
26
|
import type { User } from '../../models/interfaces';
|
|
@@ -33,9 +38,9 @@ export interface AuthState {
|
|
|
33
38
|
}
|
|
34
39
|
export interface AuthActions {
|
|
35
40
|
/**
|
|
36
|
-
* Sign in
|
|
37
|
-
*
|
|
38
|
-
*
|
|
41
|
+
* Sign in
|
|
42
|
+
* - Web: Opens popup to auth.oxy.so (no public key needed)
|
|
43
|
+
* - Native: Uses cryptographic identity from keychain
|
|
39
44
|
*/
|
|
40
45
|
signIn: (publicKey?: string) => Promise<User>;
|
|
41
46
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAuth.d.ts","sourceRoot":"","sources":["../../../../../src/ui/hooks/useAuth.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"useAuth.d.ts","sourceRoot":"","sources":["../../../../../src/ui/hooks/useAuth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAGH,OAAO,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAC/C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAGpD,MAAM,WAAW,SAAS;IACxB,4DAA4D;IAC5D,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IAElB,oCAAoC;IACpC,eAAe,EAAE,OAAO,CAAC;IAEzB,4DAA4D;IAC5D,SAAS,EAAE,OAAO,CAAC;IAEnB,oDAAoD;IACpD,OAAO,EAAE,OAAO,CAAC;IAEjB,oCAAoC;IACpC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,MAAM,EAAE,CAAC,SAAS,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAE9C;;OAEG;IACH,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAE7B;;OAEG;IACH,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAEhC;;OAEG;IACH,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,aAAc,SAAQ,SAAS,EAAE,WAAW;IAC3D,6DAA6D;IAC7D,WAAW,EAAE,UAAU,CAAC,OAAO,MAAM,CAAC,CAAC,aAAa,CAAC,CAAC;CACvD;AAED;;;;;;;;GAQG;AACH,wBAAgB,OAAO,IAAI,aAAa,CAoGvC;AAGD,OAAO,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC"}
|
|
@@ -1,34 +1,56 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Web SSO Hook
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Handles cross-domain SSO for web apps using FedCM (Federated Credential Management).
|
|
5
|
+
*
|
|
6
|
+
* FedCM is the modern, privacy-preserving standard for cross-domain identity federation.
|
|
7
|
+
* It works across completely different TLDs (alia.onl, mention.earth, homiio.com, etc.)
|
|
8
|
+
* without relying on third-party cookies.
|
|
9
|
+
*
|
|
10
|
+
* For browsers without FedCM support, users will need to click a sign-in button
|
|
11
|
+
* which triggers a popup-based authentication flow.
|
|
7
12
|
*
|
|
8
13
|
* This is called automatically by OxyContext on web platforms.
|
|
14
|
+
*
|
|
15
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/FedCM_API
|
|
9
16
|
*/
|
|
10
17
|
import type { OxyServices } from '../../core/OxyServices';
|
|
11
18
|
import type { SessionLoginResponse } from '../../models/session';
|
|
12
19
|
interface UseWebSSOOptions {
|
|
13
20
|
oxyServices: OxyServices;
|
|
14
21
|
onSessionFound: (session: SessionLoginResponse) => Promise<void>;
|
|
22
|
+
onSSOUnavailable?: () => void;
|
|
15
23
|
onError?: (error: Error) => void;
|
|
16
24
|
enabled?: boolean;
|
|
17
25
|
}
|
|
18
26
|
interface UseWebSSOResult {
|
|
27
|
+
/** Manually trigger SSO check */
|
|
19
28
|
checkSSO: () => Promise<SessionLoginResponse | null>;
|
|
29
|
+
/** Whether SSO check is in progress */
|
|
20
30
|
isChecking: boolean;
|
|
31
|
+
/** Whether FedCM is supported in this browser */
|
|
32
|
+
isFedCMSupported: boolean;
|
|
21
33
|
}
|
|
22
34
|
/**
|
|
23
35
|
* Check if we're running in a web browser environment (not React Native)
|
|
24
36
|
*/
|
|
25
37
|
declare function isWebBrowser(): boolean;
|
|
26
38
|
/**
|
|
27
|
-
* Hook for automatic web SSO
|
|
39
|
+
* Hook for automatic cross-domain web SSO
|
|
40
|
+
*
|
|
41
|
+
* Uses FedCM (Federated Credential Management) - the modern browser-native
|
|
42
|
+
* identity federation API. This is the same technology that powers
|
|
43
|
+
* Google's cross-domain SSO (YouTube, Gmail, Maps, etc.).
|
|
44
|
+
*
|
|
45
|
+
* Key benefits:
|
|
46
|
+
* - Works across different TLDs (alia.onl ↔ mention.earth ↔ homiio.com)
|
|
47
|
+
* - No third-party cookies required
|
|
48
|
+
* - Privacy-preserving (browser mediates identity, IdP can't track)
|
|
49
|
+
* - Automatic silent sign-in after initial authentication
|
|
28
50
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
51
|
+
* For browsers without FedCM (Firefox, older browsers), automatic SSO
|
|
52
|
+
* is not possible. Users will see a sign-in button instead.
|
|
31
53
|
*/
|
|
32
|
-
export declare function useWebSSO({ oxyServices, onSessionFound, onError, enabled, }: UseWebSSOOptions): UseWebSSOResult;
|
|
54
|
+
export declare function useWebSSO({ oxyServices, onSessionFound, onSSOUnavailable, onError, enabled, }: UseWebSSOOptions): UseWebSSOResult;
|
|
33
55
|
export { isWebBrowser };
|
|
34
56
|
//# sourceMappingURL=useWebSSO.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useWebSSO.d.ts","sourceRoot":"","sources":["../../../../../src/ui/hooks/useWebSSO.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"useWebSSO.d.ts","sourceRoot":"","sources":["../../../../../src/ui/hooks/useWebSSO.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAEjE,UAAU,gBAAgB;IACxB,WAAW,EAAE,WAAW,CAAC;IACzB,cAAc,EAAE,CAAC,OAAO,EAAE,oBAAoB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjE,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;IAC9B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,UAAU,eAAe;IACvB,iCAAiC;IACjC,QAAQ,EAAE,MAAM,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAAC;IACrD,uCAAuC;IACvC,UAAU,EAAE,OAAO,CAAC;IACpB,iDAAiD;IACjD,gBAAgB,EAAE,OAAO,CAAC;CAC3B;AAED;;GAEG;AACH,iBAAS,YAAY,IAAI,OAAO,CAI/B;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,SAAS,CAAC,EACxB,WAAW,EACX,cAAc,EACd,gBAAgB,EAChB,OAAO,EACP,OAAc,GACf,EAAE,gBAAgB,GAAG,eAAe,CAiEpC;AAED,OAAO,EAAE,YAAY,EAAE,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxyhq/services",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.20.0",
|
|
4
4
|
"description": "Reusable OxyHQ module to handle authentication, user management, karma system, device-based session management and more 🚀",
|
|
5
5
|
"main": "lib/commonjs/index.js",
|
|
6
6
|
"module": "lib/module/index.js",
|
|
@@ -240,7 +240,7 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
240
240
|
public async exchangeIdTokenForSession(idToken: string): Promise<SessionLoginResponse> {
|
|
241
241
|
return this.makeRequest<SessionLoginResponse>(
|
|
242
242
|
'POST',
|
|
243
|
-
'/api/
|
|
243
|
+
'/api/fedcm/exchange',
|
|
244
244
|
{ id_token: idToken },
|
|
245
245
|
{ cache: false }
|
|
246
246
|
);
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import { useEffect, useRef, useState, type FC } from 'react';
|
|
2
|
-
import { AppState } from 'react-native';
|
|
2
|
+
import { AppState, Platform } from 'react-native';
|
|
3
3
|
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
|
4
4
|
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
|
5
|
-
import { KeyboardProvider } from 'react-native-keyboard-controller';
|
|
6
5
|
import type { OxyProviderProps } from '../types/navigation';
|
|
7
6
|
import { OxyContextProvider } from '../context/OxyContext';
|
|
8
7
|
import { QueryClientProvider, focusManager, onlineManager } from '@tanstack/react-query';
|
|
9
8
|
import { setupFonts } from './FontLoader';
|
|
10
|
-
import BottomSheetRouter from './BottomSheetRouter';
|
|
11
9
|
import { Toaster } from '../../lib/sonner';
|
|
12
10
|
import { createQueryClient } from '../hooks/queryClient';
|
|
13
11
|
import { createPlatformStorage, type StorageInterface } from '../utils/storageHelpers';
|
|
@@ -15,11 +13,51 @@ import { createPlatformStorage, type StorageInterface } from '../utils/storageHe
|
|
|
15
13
|
// Initialize fonts automatically
|
|
16
14
|
setupFonts();
|
|
17
15
|
|
|
16
|
+
// Detect if running on web
|
|
17
|
+
const isWeb = Platform.OS === 'web';
|
|
18
|
+
|
|
19
|
+
// Conditionally import native-only components
|
|
20
|
+
let KeyboardProvider: any = ({ children }: any) => children;
|
|
21
|
+
let BottomSheetRouter: any = () => null;
|
|
22
|
+
|
|
23
|
+
if (!isWeb) {
|
|
24
|
+
try {
|
|
25
|
+
// Only import on native platforms
|
|
26
|
+
KeyboardProvider = require('react-native-keyboard-controller').KeyboardProvider;
|
|
27
|
+
BottomSheetRouter = require('./BottomSheetRouter').default;
|
|
28
|
+
} catch {
|
|
29
|
+
// Fallback if imports fail
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
18
33
|
/**
|
|
19
|
-
* OxyProvider
|
|
34
|
+
* OxyProvider - Universal provider for Expo apps (native + web)
|
|
35
|
+
*
|
|
36
|
+
* Zero-config authentication and session management:
|
|
37
|
+
* - Native (iOS/Android): Keychain-based identity, bottom sheet auth UI
|
|
38
|
+
* - Web: FedCM cross-domain SSO, popup fallback
|
|
20
39
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
40
|
+
* Usage:
|
|
41
|
+
* ```tsx
|
|
42
|
+
* import { OxyProvider, useAuth } from '@oxyhq/services';
|
|
43
|
+
*
|
|
44
|
+
* function App() {
|
|
45
|
+
* return (
|
|
46
|
+
* <OxyProvider baseURL="https://api.oxy.so">
|
|
47
|
+
* <YourApp />
|
|
48
|
+
* </OxyProvider>
|
|
49
|
+
* );
|
|
50
|
+
* }
|
|
51
|
+
*
|
|
52
|
+
* function MyComponent() {
|
|
53
|
+
* const { isAuthenticated, user, signIn, signOut } = useAuth();
|
|
54
|
+
*
|
|
55
|
+
* if (!isAuthenticated) {
|
|
56
|
+
* return <OxySignInButton />;
|
|
57
|
+
* }
|
|
58
|
+
* return <Text>Welcome, {user?.username}!</Text>;
|
|
59
|
+
* }
|
|
60
|
+
* ```
|
|
23
61
|
*/
|
|
24
62
|
const OxyProvider: FC<OxyProviderProps> = ({
|
|
25
63
|
oxyServices,
|
|
@@ -72,14 +110,26 @@ const OxyProvider: FC<OxyProviderProps> = ({
|
|
|
72
110
|
};
|
|
73
111
|
}, [providedQueryClient]);
|
|
74
112
|
|
|
75
|
-
// Hook React Query focus manager into
|
|
113
|
+
// Hook React Query focus manager into app state (native) or visibility (web)
|
|
76
114
|
useEffect(() => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
115
|
+
if (isWeb) {
|
|
116
|
+
// Web: use document visibility
|
|
117
|
+
const handleVisibilityChange = () => {
|
|
118
|
+
focusManager.setFocused(document.visibilityState === 'visible');
|
|
119
|
+
};
|
|
120
|
+
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
121
|
+
return () => {
|
|
122
|
+
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
123
|
+
};
|
|
124
|
+
} else {
|
|
125
|
+
// Native: use AppState
|
|
126
|
+
const subscription = AppState.addEventListener('change', (state) => {
|
|
127
|
+
focusManager.setFocused(state === 'active');
|
|
128
|
+
});
|
|
129
|
+
return () => {
|
|
130
|
+
subscription.remove();
|
|
131
|
+
};
|
|
132
|
+
}
|
|
83
133
|
}, []);
|
|
84
134
|
|
|
85
135
|
// Setup network status monitoring for offline detection
|
|
@@ -88,35 +138,35 @@ const OxyProvider: FC<OxyProviderProps> = ({
|
|
|
88
138
|
|
|
89
139
|
const setupNetworkMonitoring = async () => {
|
|
90
140
|
try {
|
|
91
|
-
|
|
92
|
-
|
|
141
|
+
if (isWeb) {
|
|
142
|
+
// Web: use navigator.onLine
|
|
143
|
+
onlineManager.setOnline(navigator.onLine);
|
|
144
|
+
const handleOnline = () => onlineManager.setOnline(true);
|
|
145
|
+
const handleOffline = () => onlineManager.setOnline(false);
|
|
146
|
+
|
|
147
|
+
window.addEventListener('online', handleOnline);
|
|
148
|
+
window.addEventListener('offline', handleOffline);
|
|
149
|
+
|
|
150
|
+
cleanup = () => {
|
|
151
|
+
window.removeEventListener('online', handleOnline);
|
|
152
|
+
window.removeEventListener('offline', handleOffline);
|
|
153
|
+
};
|
|
154
|
+
} else {
|
|
155
|
+
// Native: try to use NetInfo
|
|
93
156
|
try {
|
|
94
157
|
const NetInfo = await import('@react-native-community/netinfo');
|
|
95
158
|
const state = await NetInfo.default.fetch();
|
|
96
159
|
onlineManager.setOnline(state.isConnected ?? true);
|
|
97
|
-
|
|
160
|
+
|
|
98
161
|
const unsubscribe = NetInfo.default.addEventListener((state: { isConnected: boolean | null }) => {
|
|
99
162
|
onlineManager.setOnline(state.isConnected ?? true);
|
|
100
163
|
});
|
|
101
|
-
|
|
164
|
+
|
|
102
165
|
cleanup = () => unsubscribe();
|
|
103
166
|
} catch {
|
|
104
167
|
// NetInfo not available, default to online
|
|
105
168
|
onlineManager.setOnline(true);
|
|
106
169
|
}
|
|
107
|
-
} else {
|
|
108
|
-
// For web, use navigator.onLine
|
|
109
|
-
onlineManager.setOnline(navigator.onLine);
|
|
110
|
-
const handleOnline = () => onlineManager.setOnline(true);
|
|
111
|
-
const handleOffline = () => onlineManager.setOnline(false);
|
|
112
|
-
|
|
113
|
-
window.addEventListener('online', handleOnline);
|
|
114
|
-
window.addEventListener('offline', handleOffline);
|
|
115
|
-
|
|
116
|
-
cleanup = () => {
|
|
117
|
-
window.removeEventListener('online', handleOnline);
|
|
118
|
-
window.removeEventListener('offline', handleOffline);
|
|
119
|
-
};
|
|
120
170
|
}
|
|
121
171
|
} catch (error) {
|
|
122
172
|
// Default to online if detection fails
|
|
@@ -133,30 +183,45 @@ const OxyProvider: FC<OxyProviderProps> = ({
|
|
|
133
183
|
|
|
134
184
|
// Ensure we have a valid QueryClient
|
|
135
185
|
if (!queryClient) {
|
|
136
|
-
// Return loading state or fallback
|
|
137
186
|
return null;
|
|
138
187
|
}
|
|
139
188
|
|
|
189
|
+
// Core content that works on all platforms
|
|
190
|
+
const coreContent = (
|
|
191
|
+
<QueryClientProvider client={queryClient}>
|
|
192
|
+
<OxyContextProvider
|
|
193
|
+
oxyServices={oxyServices as any}
|
|
194
|
+
baseURL={baseURL}
|
|
195
|
+
authWebUrl={authWebUrl}
|
|
196
|
+
authRedirectUri={authRedirectUri}
|
|
197
|
+
storageKeyPrefix={storageKeyPrefix}
|
|
198
|
+
onAuthStateChange={onAuthStateChange as any}
|
|
199
|
+
>
|
|
200
|
+
{children}
|
|
201
|
+
{/* Only render bottom sheet router on native */}
|
|
202
|
+
{!isWeb && <BottomSheetRouter />}
|
|
203
|
+
<Toaster />
|
|
204
|
+
</OxyContextProvider>
|
|
205
|
+
</QueryClientProvider>
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// On web, minimal wrappers (GestureHandler and SafeArea work via react-native-web)
|
|
209
|
+
if (isWeb) {
|
|
210
|
+
return (
|
|
211
|
+
<SafeAreaProvider>
|
|
212
|
+
<GestureHandlerRootView style={{ flex: 1 }}>
|
|
213
|
+
{coreContent}
|
|
214
|
+
</GestureHandlerRootView>
|
|
215
|
+
</SafeAreaProvider>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// On native, full wrappers including KeyboardProvider
|
|
140
220
|
return (
|
|
141
221
|
<SafeAreaProvider>
|
|
142
222
|
<GestureHandlerRootView style={{ flex: 1 }}>
|
|
143
223
|
<KeyboardProvider>
|
|
144
|
-
{
|
|
145
|
-
<QueryClientProvider client={queryClient}>
|
|
146
|
-
<OxyContextProvider
|
|
147
|
-
oxyServices={oxyServices as any}
|
|
148
|
-
baseURL={baseURL}
|
|
149
|
-
authWebUrl={authWebUrl}
|
|
150
|
-
authRedirectUri={authRedirectUri}
|
|
151
|
-
storageKeyPrefix={storageKeyPrefix}
|
|
152
|
-
onAuthStateChange={onAuthStateChange as any}
|
|
153
|
-
>
|
|
154
|
-
{children}
|
|
155
|
-
<BottomSheetRouter />
|
|
156
|
-
<Toaster />
|
|
157
|
-
</OxyContextProvider>
|
|
158
|
-
</QueryClientProvider>
|
|
159
|
-
)}
|
|
224
|
+
{coreContent}
|
|
160
225
|
</KeyboardProvider>
|
|
161
226
|
</GestureHandlerRootView>
|
|
162
227
|
</SafeAreaProvider>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type React from 'react';
|
|
2
|
+
import { useState } from 'react';
|
|
2
3
|
import { TouchableOpacity, Text, View, StyleSheet, type ViewStyle, type TextStyle, type StyleProp, Platform } from 'react-native';
|
|
3
|
-
import {
|
|
4
|
+
import { useAuth } from '../hooks/useAuth';
|
|
4
5
|
import OxyLogo from './OxyLogo';
|
|
5
6
|
|
|
6
7
|
export interface OxySignInButtonProps {
|
|
@@ -79,31 +80,37 @@ export const OxySignInButton: React.FC<OxySignInButtonProps> = ({
|
|
|
79
80
|
disabled = false,
|
|
80
81
|
showWhenAuthenticated = false,
|
|
81
82
|
}) => {
|
|
82
|
-
|
|
83
|
-
const
|
|
83
|
+
const { isAuthenticated, signIn, isLoading } = useAuth();
|
|
84
|
+
const [isSigningIn, setIsSigningIn] = useState(false);
|
|
84
85
|
|
|
85
86
|
// Don't show the button if already authenticated (unless explicitly overridden)
|
|
86
87
|
if (isAuthenticated && !showWhenAuthenticated) return null;
|
|
87
88
|
|
|
88
|
-
// Default handler that uses
|
|
89
|
-
|
|
89
|
+
// Default handler that uses useAuth's signIn method
|
|
90
|
+
// This works for both web (popup) and native (bottom sheet)
|
|
91
|
+
const handlePress = async () => {
|
|
90
92
|
if (onPress) {
|
|
91
93
|
onPress();
|
|
92
94
|
return;
|
|
93
95
|
}
|
|
94
96
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
if (isSigningIn) return;
|
|
98
|
+
|
|
99
|
+
setIsSigningIn(true);
|
|
100
|
+
try {
|
|
101
|
+
await signIn();
|
|
102
|
+
} catch (error) {
|
|
103
|
+
// Sign-in handled by the auth flow
|
|
99
104
|
if (__DEV__) {
|
|
100
|
-
console.
|
|
101
|
-
`OxySignInButton: showBottomSheet is not available. Make sure OxyProvider is set up correctly.`
|
|
102
|
-
);
|
|
105
|
+
console.log('OxySignInButton: Sign-in flow initiated', error);
|
|
103
106
|
}
|
|
107
|
+
} finally {
|
|
108
|
+
setIsSigningIn(false);
|
|
104
109
|
}
|
|
105
110
|
};
|
|
106
111
|
|
|
112
|
+
const isButtonDisabled = disabled || isLoading || isSigningIn;
|
|
113
|
+
|
|
107
114
|
// Determine the button style based on the variant
|
|
108
115
|
const getButtonStyle = () => {
|
|
109
116
|
switch (variant) {
|
|
@@ -130,9 +137,9 @@ export const OxySignInButton: React.FC<OxySignInButtonProps> = ({
|
|
|
130
137
|
|
|
131
138
|
return (
|
|
132
139
|
<TouchableOpacity
|
|
133
|
-
style={[styles.button, getButtonStyle(),
|
|
140
|
+
style={[styles.button, getButtonStyle(), isButtonDisabled && styles.buttonDisabled]}
|
|
134
141
|
onPress={handlePress}
|
|
135
|
-
disabled={
|
|
142
|
+
disabled={isButtonDisabled}
|
|
136
143
|
>
|
|
137
144
|
<View style={styles.buttonContent}>
|
|
138
145
|
<OxyLogo
|
|
@@ -140,10 +147,10 @@ export const OxySignInButton: React.FC<OxySignInButtonProps> = ({
|
|
|
140
147
|
height={20}
|
|
141
148
|
fillColor={variant === 'contained' ? 'white' : '#d169e5'}
|
|
142
149
|
secondaryFillColor={variant === 'contained' ? '#d169e5' : undefined}
|
|
143
|
-
style={
|
|
150
|
+
style={isButtonDisabled ? { opacity: 0.6 } : undefined}
|
|
144
151
|
/>
|
|
145
|
-
<Text style={[styles.text, getTextStyle(),
|
|
146
|
-
{text}
|
|
152
|
+
<Text style={[styles.text, getTextStyle(), isButtonDisabled && styles.textDisabled]}>
|
|
153
|
+
{isSigningIn ? 'Signing in...' : text}
|
|
147
154
|
</Text>
|
|
148
155
|
</View>
|
|
149
156
|
</TouchableOpacity>
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* WebOxyProvider -
|
|
2
|
+
* WebOxyProvider - Lightweight provider for pure React/Next.js apps
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Use this provider for web apps that DON'T use Expo/React Native.
|
|
5
|
+
* For Expo apps (native + web), use `OxyProvider` instead - it works on all platforms.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Automatic cross-domain SSO via FedCM (Chrome 108+, Safari 16.4+, Edge 108+)
|
|
9
|
+
* - No React Native dependencies
|
|
7
10
|
* - Session management
|
|
8
11
|
* - All useOxy/useAuth functionality
|
|
9
12
|
*
|
|
10
13
|
* Usage:
|
|
11
14
|
* ```tsx
|
|
15
|
+
* // For pure React/Next.js apps (no Expo):
|
|
12
16
|
* import { WebOxyProvider, useAuth } from '@oxyhq/services';
|
|
13
17
|
*
|
|
14
18
|
* function App() {
|
|
@@ -18,6 +22,9 @@
|
|
|
18
22
|
* </WebOxyProvider>
|
|
19
23
|
* );
|
|
20
24
|
* }
|
|
25
|
+
*
|
|
26
|
+
* // For Expo apps (native + web), use OxyProvider instead:
|
|
27
|
+
* import { OxyProvider, useAuth } from '@oxyhq/services';
|
|
21
28
|
* ```
|
|
22
29
|
*/
|
|
23
30
|
|
|
@@ -40,7 +47,8 @@ export interface WebOxyProviderProps {
|
|
|
40
47
|
* OxyProvider for web applications
|
|
41
48
|
*
|
|
42
49
|
* Features:
|
|
43
|
-
* - Automatic cross-domain SSO (
|
|
50
|
+
* - Automatic cross-domain SSO via FedCM (browser-native identity API)
|
|
51
|
+
* - Works across different TLDs (alia.onl, mention.earth, homiio.com, etc.)
|
|
44
52
|
* - Session persistence in localStorage
|
|
45
53
|
* - TanStack Query for data fetching
|
|
46
54
|
* - No React Native dependencies
|
package/src/ui/hooks/useAuth.ts
CHANGED
|
@@ -16,11 +16,17 @@
|
|
|
16
16
|
* return <Welcome user={user} />;
|
|
17
17
|
* }
|
|
18
18
|
* ```
|
|
19
|
+
*
|
|
20
|
+
* Cross-domain SSO:
|
|
21
|
+
* - Web: Automatic via FedCM (Chrome 108+, Safari 16.4+)
|
|
22
|
+
* - Native: Automatic via shared Keychain/Account Manager
|
|
23
|
+
* - Manual sign-in: signIn() opens popup (web) or auth sheet (native)
|
|
19
24
|
*/
|
|
20
25
|
|
|
21
|
-
import { useCallback } from 'react';
|
|
26
|
+
import { useCallback, useState } from 'react';
|
|
22
27
|
import { useOxy } from '../context/OxyContext';
|
|
23
28
|
import type { User } from '../../models/interfaces';
|
|
29
|
+
import { isWebBrowser } from './useWebSSO';
|
|
24
30
|
|
|
25
31
|
export interface AuthState {
|
|
26
32
|
/** Current authenticated user, null if not authenticated */
|
|
@@ -41,9 +47,9 @@ export interface AuthState {
|
|
|
41
47
|
|
|
42
48
|
export interface AuthActions {
|
|
43
49
|
/**
|
|
44
|
-
* Sign in
|
|
45
|
-
*
|
|
46
|
-
*
|
|
50
|
+
* Sign in
|
|
51
|
+
* - Web: Opens popup to auth.oxy.so (no public key needed)
|
|
52
|
+
* - Native: Uses cryptographic identity from keychain
|
|
47
53
|
*/
|
|
48
54
|
signIn: (publicKey?: string) => Promise<User>;
|
|
49
55
|
|
|
@@ -95,6 +101,35 @@ export function useAuth(): UseAuthReturn {
|
|
|
95
101
|
} = useOxy();
|
|
96
102
|
|
|
97
103
|
const signIn = useCallback(async (publicKey?: string): Promise<User> => {
|
|
104
|
+
// Web: Use popup-based authentication
|
|
105
|
+
if (isWebBrowser() && !publicKey) {
|
|
106
|
+
try {
|
|
107
|
+
// Try FedCM first (instant if user already signed in)
|
|
108
|
+
if ((oxyServices as any).isFedCMSupported?.()) {
|
|
109
|
+
const fedcmSession = await (oxyServices as any).signInWithFedCM?.();
|
|
110
|
+
if (fedcmSession?.user) {
|
|
111
|
+
return fedcmSession.user;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Fallback to popup (opens auth.oxy.so in popup window)
|
|
116
|
+
const popupSession = await (oxyServices as any).signInWithPopup?.();
|
|
117
|
+
if (popupSession?.user) {
|
|
118
|
+
return popupSession.user;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
throw new Error('Sign-in failed');
|
|
122
|
+
} catch (error) {
|
|
123
|
+
// If popup blocked or FedCM failed, suggest redirect
|
|
124
|
+
throw new Error(
|
|
125
|
+
error instanceof Error && error.message.includes('blocked')
|
|
126
|
+
? 'Popup blocked. Please allow popups or try again.'
|
|
127
|
+
: 'Sign-in failed. Please try again.'
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Native: Use cryptographic identity
|
|
98
133
|
// If public key provided, use it directly
|
|
99
134
|
if (publicKey) {
|
|
100
135
|
return oxySignIn(publicKey);
|
|
@@ -110,19 +145,14 @@ export function useAuth(): UseAuthReturn {
|
|
|
110
145
|
}
|
|
111
146
|
}
|
|
112
147
|
|
|
113
|
-
// No identity - show auth UI
|
|
114
|
-
// On native: shows bottom sheet for identity creation
|
|
115
|
-
// On web: could trigger popup auth
|
|
148
|
+
// No identity - show auth UI (native bottom sheet)
|
|
116
149
|
showBottomSheet?.('OxyAuth');
|
|
117
150
|
|
|
118
151
|
// Return a promise that resolves when auth completes
|
|
119
|
-
|
|
120
|
-
// wait for the auth flow to complete
|
|
121
|
-
return new Promise((resolve, reject) => {
|
|
122
|
-
// For now, just reject - the bottom sheet handles the flow
|
|
152
|
+
return new Promise((_, reject) => {
|
|
123
153
|
reject(new Error('Please complete sign-in in the auth sheet'));
|
|
124
154
|
});
|
|
125
|
-
}, [oxySignIn, hasIdentity, getPublicKey, showBottomSheet]);
|
|
155
|
+
}, [oxySignIn, hasIdentity, getPublicKey, showBottomSheet, oxyServices]);
|
|
126
156
|
|
|
127
157
|
const signOut = useCallback(async (): Promise<void> => {
|
|
128
158
|
await logout();
|