@cohostvip/cohost-react 0.2.4 → 0.3.3
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/dist/__tests__/setup.d.ts +2 -0
- package/dist/__tests__/setup.d.ts.map +1 -0
- package/dist/__tests__/setup.js +1 -0
- package/dist/auth/AuthContext.d.ts +57 -0
- package/dist/auth/AuthContext.d.ts.map +1 -0
- package/dist/auth/AuthContext.js +53 -0
- package/dist/auth/AuthGuard.d.ts +43 -0
- package/dist/auth/AuthGuard.d.ts.map +1 -0
- package/dist/auth/AuthGuard.js +46 -0
- package/dist/auth/__tests__/auth.test.d.ts +2 -0
- package/dist/auth/__tests__/auth.test.d.ts.map +1 -0
- package/dist/auth/__tests__/auth.test.js +241 -0
- package/dist/auth/hooks.d.ts +60 -0
- package/dist/auth/hooks.d.ts.map +1 -0
- package/dist/auth/hooks.js +71 -0
- package/dist/auth/index.d.ts +4 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +6 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/package.json +3 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/__tests__/setup.ts"],"names":[],"mappings":"AAAA,OAAO,kCAAkC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '@testing-library/jest-dom/vitest';
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { AuthClient, type AuthConfig, type AuthState, type AuthUser, type OTPType, type SetAuthenticatedInput } from '@cohostvip/cohost-auth';
|
|
3
|
+
/**
|
|
4
|
+
* Value provided by AuthContext
|
|
5
|
+
*/
|
|
6
|
+
export interface AuthContextValue {
|
|
7
|
+
/** Current auth state */
|
|
8
|
+
state: AuthState;
|
|
9
|
+
/** The underlying AuthClient instance */
|
|
10
|
+
client: AuthClient;
|
|
11
|
+
/** Request OTP to be sent to contact (email or phone) */
|
|
12
|
+
requestOTP: (contact: string, type?: OTPType) => Promise<boolean>;
|
|
13
|
+
/** Verify OTP and sign in */
|
|
14
|
+
verifyOTP: (contact: string, code: string) => Promise<AuthUser>;
|
|
15
|
+
/** Sign out the current user */
|
|
16
|
+
signOut: () => Promise<void>;
|
|
17
|
+
/** Get current access token (refreshing if needed) */
|
|
18
|
+
getToken: () => Promise<string | null>;
|
|
19
|
+
/** Manually set authenticated state (for custom auth flows like passkey) */
|
|
20
|
+
setAuthenticated: (input: SetAuthenticatedInput) => void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Props for AuthProvider
|
|
24
|
+
*/
|
|
25
|
+
export type AuthProviderProps = {
|
|
26
|
+
/** Children to render */
|
|
27
|
+
children: React.ReactNode;
|
|
28
|
+
} & ({
|
|
29
|
+
/** Auth configuration (creates a new client) */
|
|
30
|
+
config: AuthConfig;
|
|
31
|
+
/** Pre-existing client (mutually exclusive with config) */
|
|
32
|
+
client?: never;
|
|
33
|
+
} | {
|
|
34
|
+
/** Auth configuration */
|
|
35
|
+
config?: never;
|
|
36
|
+
/** Pre-existing AuthClient instance */
|
|
37
|
+
client: AuthClient;
|
|
38
|
+
});
|
|
39
|
+
export declare const AuthContext: React.Context<AuthContextValue | null>;
|
|
40
|
+
/**
|
|
41
|
+
* Provider component that wraps your app and provides auth context
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```tsx
|
|
45
|
+
* import { AuthProvider } from '@cohostvip/cohost-react';
|
|
46
|
+
*
|
|
47
|
+
* function App() {
|
|
48
|
+
* return (
|
|
49
|
+
* <AuthProvider config={{ apiUrl: '/api' }}>
|
|
50
|
+
* <MyApp />
|
|
51
|
+
* </AuthProvider>
|
|
52
|
+
* );
|
|
53
|
+
* }
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export declare const AuthProvider: React.FC<AuthProviderProps>;
|
|
57
|
+
//# sourceMappingURL=AuthContext.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AuthContext.d.ts","sourceRoot":"","sources":["../../src/auth/AuthContext.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,EACL,UAAU,EAEV,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,OAAO,EACZ,KAAK,qBAAqB,EAC3B,MAAM,wBAAwB,CAAC;AAEhC;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,yBAAyB;IACzB,KAAK,EAAE,SAAS,CAAC;IACjB,yCAAyC;IACzC,MAAM,EAAE,UAAU,CAAC;IACnB,yDAAyD;IACzD,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAClE,6BAA6B;IAC7B,SAAS,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IAChE,gCAAgC;IAChC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,sDAAsD;IACtD,QAAQ,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACvC,4EAA4E;IAC5E,gBAAgB,EAAE,CAAC,KAAK,EAAE,qBAAqB,KAAK,IAAI,CAAC;CAC1D;AAED;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,yBAAyB;IACzB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B,GAAG,CACA;IACE,gDAAgD;IAChD,MAAM,EAAE,UAAU,CAAC;IACnB,2DAA2D;IAC3D,MAAM,CAAC,EAAE,KAAK,CAAC;CAChB,GACD;IACE,yBAAyB;IACzB,MAAM,CAAC,EAAE,KAAK,CAAC;IACf,uCAAuC;IACvC,MAAM,EAAE,UAAU,CAAC;CACpB,CACJ,CAAC;AAEF,eAAO,MAAM,WAAW,wCAA+C,CAAC;AAExE;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CA6CpD,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { createAuthClient, } from '@cohostvip/cohost-auth';
|
|
4
|
+
export const AuthContext = createContext(null);
|
|
5
|
+
/**
|
|
6
|
+
* Provider component that wraps your app and provides auth context
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```tsx
|
|
10
|
+
* import { AuthProvider } from '@cohostvip/cohost-react';
|
|
11
|
+
*
|
|
12
|
+
* function App() {
|
|
13
|
+
* return (
|
|
14
|
+
* <AuthProvider config={{ apiUrl: '/api' }}>
|
|
15
|
+
* <MyApp />
|
|
16
|
+
* </AuthProvider>
|
|
17
|
+
* );
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export const AuthProvider = ({ children, config, client: providedClient, }) => {
|
|
22
|
+
// Create or use provided client (stable reference)
|
|
23
|
+
const clientRef = useRef(null);
|
|
24
|
+
if (!clientRef.current) {
|
|
25
|
+
clientRef.current = providedClient ?? createAuthClient(config);
|
|
26
|
+
}
|
|
27
|
+
const client = clientRef.current;
|
|
28
|
+
// Track auth state
|
|
29
|
+
const [state, setState] = useState(() => client.getState());
|
|
30
|
+
// Initialize client and subscribe to state changes
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
// Initialize on mount
|
|
33
|
+
client.initialize();
|
|
34
|
+
// Subscribe to state changes
|
|
35
|
+
const unsubscribe = client.onAuthStateChanged((newState) => {
|
|
36
|
+
setState(newState);
|
|
37
|
+
});
|
|
38
|
+
return () => {
|
|
39
|
+
unsubscribe();
|
|
40
|
+
};
|
|
41
|
+
}, [client]);
|
|
42
|
+
// Memoize context value to prevent unnecessary re-renders
|
|
43
|
+
const value = useMemo(() => ({
|
|
44
|
+
state,
|
|
45
|
+
client,
|
|
46
|
+
requestOTP: (contact, type) => client.requestOTP(contact, type),
|
|
47
|
+
verifyOTP: (contact, code) => client.verifyOTP(contact, code),
|
|
48
|
+
signOut: () => client.signOut(),
|
|
49
|
+
getToken: () => client.getToken(),
|
|
50
|
+
setAuthenticated: (input) => client.setAuthenticated(input),
|
|
51
|
+
}), [state, client]);
|
|
52
|
+
return _jsx(AuthContext.Provider, { value: value, children: children });
|
|
53
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Props for AuthGuard component
|
|
4
|
+
*/
|
|
5
|
+
export interface AuthGuardProps {
|
|
6
|
+
/** Content to render when authenticated */
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
/** Content to render while loading auth state (default: null) */
|
|
9
|
+
loadingFallback?: React.ReactNode;
|
|
10
|
+
/** Content to render when not authenticated (default: null) */
|
|
11
|
+
unauthenticatedFallback?: React.ReactNode;
|
|
12
|
+
/** Callback when user is not authenticated (e.g., for redirects) */
|
|
13
|
+
onUnauthenticated?: () => void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Component that only renders children when authenticated
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```tsx
|
|
20
|
+
* // Basic usage
|
|
21
|
+
* <AuthGuard>
|
|
22
|
+
* <ProtectedContent />
|
|
23
|
+
* </AuthGuard>
|
|
24
|
+
*
|
|
25
|
+
* // With fallbacks
|
|
26
|
+
* <AuthGuard
|
|
27
|
+
* loadingFallback={<Spinner />}
|
|
28
|
+
* unauthenticatedFallback={<LoginPrompt />}
|
|
29
|
+
* >
|
|
30
|
+
* <Dashboard />
|
|
31
|
+
* </AuthGuard>
|
|
32
|
+
*
|
|
33
|
+
* // With redirect callback
|
|
34
|
+
* <AuthGuard
|
|
35
|
+
* onUnauthenticated={() => router.push('/login')}
|
|
36
|
+
* loadingFallback={<Spinner />}
|
|
37
|
+
* >
|
|
38
|
+
* <AdminPanel />
|
|
39
|
+
* </AuthGuard>
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export declare const AuthGuard: React.FC<AuthGuardProps>;
|
|
43
|
+
//# sourceMappingURL=AuthGuard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AuthGuard.d.ts","sourceRoot":"","sources":["../../src/auth/AuthGuard.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,2CAA2C;IAC3C,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,iEAAiE;IACjE,eAAe,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAClC,+DAA+D;IAC/D,uBAAuB,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1C,oEAAoE;IACpE,iBAAiB,CAAC,EAAE,MAAM,IAAI,CAAC;CAChC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,SAAS,EAAE,KAAK,CAAC,EAAE,CAAC,cAAc,CAwB9C,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useAuth } from './hooks';
|
|
3
|
+
/**
|
|
4
|
+
* Component that only renders children when authenticated
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```tsx
|
|
8
|
+
* // Basic usage
|
|
9
|
+
* <AuthGuard>
|
|
10
|
+
* <ProtectedContent />
|
|
11
|
+
* </AuthGuard>
|
|
12
|
+
*
|
|
13
|
+
* // With fallbacks
|
|
14
|
+
* <AuthGuard
|
|
15
|
+
* loadingFallback={<Spinner />}
|
|
16
|
+
* unauthenticatedFallback={<LoginPrompt />}
|
|
17
|
+
* >
|
|
18
|
+
* <Dashboard />
|
|
19
|
+
* </AuthGuard>
|
|
20
|
+
*
|
|
21
|
+
* // With redirect callback
|
|
22
|
+
* <AuthGuard
|
|
23
|
+
* onUnauthenticated={() => router.push('/login')}
|
|
24
|
+
* loadingFallback={<Spinner />}
|
|
25
|
+
* >
|
|
26
|
+
* <AdminPanel />
|
|
27
|
+
* </AuthGuard>
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export const AuthGuard = ({ children, loadingFallback = null, unauthenticatedFallback = null, onUnauthenticated, }) => {
|
|
31
|
+
const { state } = useAuth();
|
|
32
|
+
// Show loading fallback while auth state is being determined
|
|
33
|
+
if (state.isLoading) {
|
|
34
|
+
return _jsx(_Fragment, { children: loadingFallback });
|
|
35
|
+
}
|
|
36
|
+
// Handle unauthenticated state
|
|
37
|
+
if (!state.isAuthenticated) {
|
|
38
|
+
// Call redirect callback if provided
|
|
39
|
+
if (onUnauthenticated) {
|
|
40
|
+
onUnauthenticated();
|
|
41
|
+
}
|
|
42
|
+
return _jsx(_Fragment, { children: unauthenticatedFallback });
|
|
43
|
+
}
|
|
44
|
+
// User is authenticated, render children
|
|
45
|
+
return _jsx(_Fragment, { children: children });
|
|
46
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.test.d.ts","sourceRoot":"","sources":["../../../src/auth/__tests__/auth.test.tsx"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
import { AuthProvider, useAuth, useUser, useAuthClient, AuthGuard } from '../index';
|
|
6
|
+
// Mock fetch globally
|
|
7
|
+
const mockFetch = vi.fn();
|
|
8
|
+
global.fetch = mockFetch;
|
|
9
|
+
const mockOkResponse = (data) => ({
|
|
10
|
+
ok: true,
|
|
11
|
+
status: 200,
|
|
12
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
13
|
+
json: async () => ({ status: 'ok', data }),
|
|
14
|
+
});
|
|
15
|
+
const defaultConfig = {
|
|
16
|
+
apiUrl: 'https://api.example.com',
|
|
17
|
+
storage: 'memory',
|
|
18
|
+
autoRefresh: false,
|
|
19
|
+
};
|
|
20
|
+
describe('AuthProvider', () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
mockFetch.mockReset();
|
|
23
|
+
});
|
|
24
|
+
it('should render children', () => {
|
|
25
|
+
render(_jsx(AuthProvider, { config: defaultConfig, children: _jsx("div", { "data-testid": "child", children: "Hello" }) }));
|
|
26
|
+
expect(screen.getByTestId('child')).toBeInTheDocument();
|
|
27
|
+
});
|
|
28
|
+
it('should provide auth context to children', () => {
|
|
29
|
+
function TestComponent() {
|
|
30
|
+
const auth = useAuth();
|
|
31
|
+
return _jsx("div", { "data-testid": "has-auth", children: auth ? 'yes' : 'no' });
|
|
32
|
+
}
|
|
33
|
+
render(_jsx(AuthProvider, { config: defaultConfig, children: _jsx(TestComponent, {}) }));
|
|
34
|
+
expect(screen.getByTestId('has-auth')).toHaveTextContent('yes');
|
|
35
|
+
});
|
|
36
|
+
it('should initialize and eventually set not authenticated', async () => {
|
|
37
|
+
function TestComponent() {
|
|
38
|
+
const { state } = useAuth();
|
|
39
|
+
return (_jsxs("div", { children: [_jsx("span", { "data-testid": "loading", children: state.isLoading ? 'yes' : 'no' }), _jsx("span", { "data-testid": "authenticated", children: state.isAuthenticated ? 'yes' : 'no' })] }));
|
|
40
|
+
}
|
|
41
|
+
render(_jsx(AuthProvider, { config: defaultConfig, children: _jsx(TestComponent, {}) }));
|
|
42
|
+
// Wait for initialization to complete
|
|
43
|
+
await waitFor(() => {
|
|
44
|
+
expect(screen.getByTestId('loading')).toHaveTextContent('no');
|
|
45
|
+
});
|
|
46
|
+
expect(screen.getByTestId('authenticated')).toHaveTextContent('no');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
describe('useAuth', () => {
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
mockFetch.mockReset();
|
|
52
|
+
});
|
|
53
|
+
it('should throw if used outside AuthProvider', () => {
|
|
54
|
+
// Suppress console.error for this test
|
|
55
|
+
const spy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
56
|
+
function TestComponent() {
|
|
57
|
+
useAuth();
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
expect(() => render(_jsx(TestComponent, {}))).toThrow('useAuth must be used within an AuthProvider');
|
|
61
|
+
spy.mockRestore();
|
|
62
|
+
});
|
|
63
|
+
it('should provide auth methods', () => {
|
|
64
|
+
function TestComponent() {
|
|
65
|
+
const { requestOTP, verifyOTP, signOut, getToken } = useAuth();
|
|
66
|
+
return (_jsx("div", { "data-testid": "has-methods", children: typeof requestOTP === 'function' &&
|
|
67
|
+
typeof verifyOTP === 'function' &&
|
|
68
|
+
typeof signOut === 'function' &&
|
|
69
|
+
typeof getToken === 'function'
|
|
70
|
+
? 'yes'
|
|
71
|
+
: 'no' }));
|
|
72
|
+
}
|
|
73
|
+
render(_jsx(AuthProvider, { config: defaultConfig, children: _jsx(TestComponent, {}) }));
|
|
74
|
+
expect(screen.getByTestId('has-methods')).toHaveTextContent('yes');
|
|
75
|
+
});
|
|
76
|
+
it('should call requestOTP successfully', async () => {
|
|
77
|
+
mockFetch.mockResolvedValue(mockOkResponse({ sent: true }));
|
|
78
|
+
let requestResult = null;
|
|
79
|
+
function TestComponent() {
|
|
80
|
+
const { requestOTP } = useAuth();
|
|
81
|
+
React.useEffect(() => {
|
|
82
|
+
requestOTP('test@example.com').then((result) => {
|
|
83
|
+
requestResult = result;
|
|
84
|
+
});
|
|
85
|
+
}, [requestOTP]);
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
render(_jsx(AuthProvider, { config: defaultConfig, children: _jsx(TestComponent, {}) }));
|
|
89
|
+
await waitFor(() => {
|
|
90
|
+
expect(requestResult).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/otp/request', expect.objectContaining({
|
|
93
|
+
method: 'POST',
|
|
94
|
+
body: expect.stringContaining('test@example.com'),
|
|
95
|
+
}));
|
|
96
|
+
});
|
|
97
|
+
it('should authenticate user with verifyOTP', async () => {
|
|
98
|
+
const authResult = {
|
|
99
|
+
user: {
|
|
100
|
+
uid: '123',
|
|
101
|
+
email: 'test@example.com',
|
|
102
|
+
emailVerified: true,
|
|
103
|
+
provider: 'otp',
|
|
104
|
+
providerId: 'cohost',
|
|
105
|
+
},
|
|
106
|
+
customToken: 'jwt-token',
|
|
107
|
+
isNewUser: false,
|
|
108
|
+
};
|
|
109
|
+
mockFetch.mockResolvedValue(mockOkResponse(authResult));
|
|
110
|
+
function TestComponent() {
|
|
111
|
+
const { state, verifyOTP } = useAuth();
|
|
112
|
+
const [verified, setVerified] = React.useState(false);
|
|
113
|
+
React.useEffect(() => {
|
|
114
|
+
if (!verified) {
|
|
115
|
+
setVerified(true);
|
|
116
|
+
verifyOTP('test@example.com', '123456');
|
|
117
|
+
}
|
|
118
|
+
}, [verified, verifyOTP]);
|
|
119
|
+
return (_jsxs("div", { children: [_jsx("span", { "data-testid": "authenticated", children: state.isAuthenticated ? 'yes' : 'no' }), _jsx("span", { "data-testid": "email", children: state.user?.email || 'none' })] }));
|
|
120
|
+
}
|
|
121
|
+
render(_jsx(AuthProvider, { config: defaultConfig, children: _jsx(TestComponent, {}) }));
|
|
122
|
+
await waitFor(() => {
|
|
123
|
+
expect(screen.getByTestId('authenticated')).toHaveTextContent('yes');
|
|
124
|
+
});
|
|
125
|
+
expect(screen.getByTestId('email')).toHaveTextContent('test@example.com');
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
describe('useUser', () => {
|
|
129
|
+
beforeEach(() => {
|
|
130
|
+
mockFetch.mockReset();
|
|
131
|
+
});
|
|
132
|
+
it('should return null when not authenticated', async () => {
|
|
133
|
+
function TestComponent() {
|
|
134
|
+
const user = useUser();
|
|
135
|
+
const { state } = useAuth();
|
|
136
|
+
return (_jsxs("div", { children: [_jsx("span", { "data-testid": "loading", children: state.isLoading ? 'yes' : 'no' }), _jsx("span", { "data-testid": "user", children: user ? user.email : 'null' })] }));
|
|
137
|
+
}
|
|
138
|
+
render(_jsx(AuthProvider, { config: defaultConfig, children: _jsx(TestComponent, {}) }));
|
|
139
|
+
await waitFor(() => {
|
|
140
|
+
expect(screen.getByTestId('loading')).toHaveTextContent('no');
|
|
141
|
+
});
|
|
142
|
+
expect(screen.getByTestId('user')).toHaveTextContent('null');
|
|
143
|
+
});
|
|
144
|
+
it('should return user when authenticated', async () => {
|
|
145
|
+
const authResult = {
|
|
146
|
+
user: {
|
|
147
|
+
uid: '123',
|
|
148
|
+
email: 'test@example.com',
|
|
149
|
+
emailVerified: true,
|
|
150
|
+
provider: 'otp',
|
|
151
|
+
providerId: 'cohost',
|
|
152
|
+
},
|
|
153
|
+
customToken: 'jwt-token',
|
|
154
|
+
isNewUser: false,
|
|
155
|
+
};
|
|
156
|
+
mockFetch.mockResolvedValue(mockOkResponse(authResult));
|
|
157
|
+
function TestComponent() {
|
|
158
|
+
const { verifyOTP } = useAuth();
|
|
159
|
+
const user = useUser();
|
|
160
|
+
const [verified, setVerified] = React.useState(false);
|
|
161
|
+
React.useEffect(() => {
|
|
162
|
+
if (!verified) {
|
|
163
|
+
setVerified(true);
|
|
164
|
+
verifyOTP('test@example.com', '123456');
|
|
165
|
+
}
|
|
166
|
+
}, [verified, verifyOTP]);
|
|
167
|
+
return _jsx("div", { "data-testid": "user", children: user ? user.email : 'null' });
|
|
168
|
+
}
|
|
169
|
+
render(_jsx(AuthProvider, { config: defaultConfig, children: _jsx(TestComponent, {}) }));
|
|
170
|
+
await waitFor(() => {
|
|
171
|
+
expect(screen.getByTestId('user')).toHaveTextContent('test@example.com');
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
describe('useAuthClient', () => {
|
|
176
|
+
it('should return the AuthClient instance', () => {
|
|
177
|
+
function TestComponent() {
|
|
178
|
+
const client = useAuthClient();
|
|
179
|
+
return (_jsx("div", { "data-testid": "has-client", children: client && typeof client.initialize === 'function' ? 'yes' : 'no' }));
|
|
180
|
+
}
|
|
181
|
+
render(_jsx(AuthProvider, { config: defaultConfig, children: _jsx(TestComponent, {}) }));
|
|
182
|
+
expect(screen.getByTestId('has-client')).toHaveTextContent('yes');
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
describe('AuthGuard', () => {
|
|
186
|
+
beforeEach(() => {
|
|
187
|
+
mockFetch.mockReset();
|
|
188
|
+
});
|
|
189
|
+
it('should show unauthenticated fallback when not authenticated', async () => {
|
|
190
|
+
function TestComponent() {
|
|
191
|
+
return (_jsx(AuthGuard, { loadingFallback: _jsx("div", { "data-testid": "loading", children: "Loading..." }), unauthenticatedFallback: _jsx("div", { "data-testid": "login", children: "Please log in" }), children: _jsx("div", { "data-testid": "protected", children: "Protected Content" }) }));
|
|
192
|
+
}
|
|
193
|
+
render(_jsx(AuthProvider, { config: defaultConfig, children: _jsx(TestComponent, {}) }));
|
|
194
|
+
// Wait for initialization to complete
|
|
195
|
+
await waitFor(() => {
|
|
196
|
+
expect(screen.queryByTestId('loading')).not.toBeInTheDocument();
|
|
197
|
+
});
|
|
198
|
+
expect(screen.getByTestId('login')).toBeInTheDocument();
|
|
199
|
+
expect(screen.queryByTestId('protected')).not.toBeInTheDocument();
|
|
200
|
+
});
|
|
201
|
+
it('should call onUnauthenticated callback when not authenticated', async () => {
|
|
202
|
+
const onUnauthenticated = vi.fn();
|
|
203
|
+
function TestComponent() {
|
|
204
|
+
return (_jsx(AuthGuard, { onUnauthenticated: onUnauthenticated, children: _jsx("div", { "data-testid": "protected", children: "Protected Content" }) }));
|
|
205
|
+
}
|
|
206
|
+
render(_jsx(AuthProvider, { config: defaultConfig, children: _jsx(TestComponent, {}) }));
|
|
207
|
+
await waitFor(() => {
|
|
208
|
+
expect(onUnauthenticated).toHaveBeenCalled();
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
it('should show children when authenticated', async () => {
|
|
212
|
+
const authResult = {
|
|
213
|
+
user: {
|
|
214
|
+
uid: '123',
|
|
215
|
+
email: 'test@example.com',
|
|
216
|
+
emailVerified: true,
|
|
217
|
+
provider: 'otp',
|
|
218
|
+
providerId: 'cohost',
|
|
219
|
+
},
|
|
220
|
+
customToken: 'jwt-token',
|
|
221
|
+
isNewUser: false,
|
|
222
|
+
};
|
|
223
|
+
mockFetch.mockResolvedValue(mockOkResponse(authResult));
|
|
224
|
+
function Wrapper() {
|
|
225
|
+
const { verifyOTP, state } = useAuth();
|
|
226
|
+
const [verified, setVerified] = React.useState(false);
|
|
227
|
+
React.useEffect(() => {
|
|
228
|
+
if (!verified && !state.isAuthenticated) {
|
|
229
|
+
setVerified(true);
|
|
230
|
+
verifyOTP('test@example.com', '123456');
|
|
231
|
+
}
|
|
232
|
+
}, [verified, verifyOTP, state.isAuthenticated]);
|
|
233
|
+
return (_jsx(AuthGuard, { loadingFallback: _jsx("div", { "data-testid": "loading", children: "Loading..." }), unauthenticatedFallback: _jsx("div", { "data-testid": "login", children: "Please log in" }), children: _jsx("div", { "data-testid": "protected", children: "Protected Content" }) }));
|
|
234
|
+
}
|
|
235
|
+
render(_jsx(AuthProvider, { config: defaultConfig, children: _jsx(Wrapper, {}) }));
|
|
236
|
+
await waitFor(() => {
|
|
237
|
+
expect(screen.getByTestId('protected')).toBeInTheDocument();
|
|
238
|
+
});
|
|
239
|
+
expect(screen.queryByTestId('login')).not.toBeInTheDocument();
|
|
240
|
+
});
|
|
241
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { AuthClient, AuthUser } from '@cohostvip/cohost-auth';
|
|
2
|
+
import { type AuthContextValue } from './AuthContext';
|
|
3
|
+
/**
|
|
4
|
+
* Hook to access auth state and methods
|
|
5
|
+
*
|
|
6
|
+
* @throws Error if used outside of AuthProvider
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```tsx
|
|
10
|
+
* function LoginButton() {
|
|
11
|
+
* const { state, requestOTP, verifyOTP, signOut } = useAuth();
|
|
12
|
+
*
|
|
13
|
+
* if (state.isLoading) return <Loading />;
|
|
14
|
+
* if (state.isAuthenticated) {
|
|
15
|
+
* return <button onClick={signOut}>Sign Out</button>;
|
|
16
|
+
* }
|
|
17
|
+
* return <button onClick={() => requestOTP('user@example.com')}>Sign In</button>;
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare function useAuth(): AuthContextValue;
|
|
22
|
+
/**
|
|
23
|
+
* Hook to access the current user
|
|
24
|
+
* Returns null if not authenticated
|
|
25
|
+
*
|
|
26
|
+
* @throws Error if used outside of AuthProvider
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```tsx
|
|
30
|
+
* function Profile() {
|
|
31
|
+
* const user = useUser();
|
|
32
|
+
*
|
|
33
|
+
* if (!user) return <div>Not logged in</div>;
|
|
34
|
+
* return <div>Welcome, {user.displayName || user.email}</div>;
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare function useUser(): AuthUser | null;
|
|
39
|
+
/**
|
|
40
|
+
* Hook to access the raw AuthClient instance
|
|
41
|
+
* Useful for advanced use cases or accessing methods not exposed by useAuth
|
|
42
|
+
*
|
|
43
|
+
* @throws Error if used outside of AuthProvider
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```tsx
|
|
47
|
+
* function TokenDisplay() {
|
|
48
|
+
* const client = useAuthClient();
|
|
49
|
+
* const [token, setToken] = useState<string | null>(null);
|
|
50
|
+
*
|
|
51
|
+
* useEffect(() => {
|
|
52
|
+
* client.getToken().then(setToken);
|
|
53
|
+
* }, [client]);
|
|
54
|
+
*
|
|
55
|
+
* return <div>Token: {token}</div>;
|
|
56
|
+
* }
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export declare function useAuthClient(): AuthClient;
|
|
60
|
+
//# sourceMappingURL=hooks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../src/auth/hooks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,EAAe,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEnE;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,OAAO,IAAI,gBAAgB,CAM1C;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,OAAO,IAAI,QAAQ,GAAG,IAAI,CAGzC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,aAAa,IAAI,UAAU,CAG1C"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { AuthContext } from './AuthContext';
|
|
3
|
+
/**
|
|
4
|
+
* Hook to access auth state and methods
|
|
5
|
+
*
|
|
6
|
+
* @throws Error if used outside of AuthProvider
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```tsx
|
|
10
|
+
* function LoginButton() {
|
|
11
|
+
* const { state, requestOTP, verifyOTP, signOut } = useAuth();
|
|
12
|
+
*
|
|
13
|
+
* if (state.isLoading) return <Loading />;
|
|
14
|
+
* if (state.isAuthenticated) {
|
|
15
|
+
* return <button onClick={signOut}>Sign Out</button>;
|
|
16
|
+
* }
|
|
17
|
+
* return <button onClick={() => requestOTP('user@example.com')}>Sign In</button>;
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function useAuth() {
|
|
22
|
+
const ctx = useContext(AuthContext);
|
|
23
|
+
if (!ctx) {
|
|
24
|
+
throw new Error('useAuth must be used within an AuthProvider');
|
|
25
|
+
}
|
|
26
|
+
return ctx;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Hook to access the current user
|
|
30
|
+
* Returns null if not authenticated
|
|
31
|
+
*
|
|
32
|
+
* @throws Error if used outside of AuthProvider
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```tsx
|
|
36
|
+
* function Profile() {
|
|
37
|
+
* const user = useUser();
|
|
38
|
+
*
|
|
39
|
+
* if (!user) return <div>Not logged in</div>;
|
|
40
|
+
* return <div>Welcome, {user.displayName || user.email}</div>;
|
|
41
|
+
* }
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export function useUser() {
|
|
45
|
+
const { state } = useAuth();
|
|
46
|
+
return state.user;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Hook to access the raw AuthClient instance
|
|
50
|
+
* Useful for advanced use cases or accessing methods not exposed by useAuth
|
|
51
|
+
*
|
|
52
|
+
* @throws Error if used outside of AuthProvider
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```tsx
|
|
56
|
+
* function TokenDisplay() {
|
|
57
|
+
* const client = useAuthClient();
|
|
58
|
+
* const [token, setToken] = useState<string | null>(null);
|
|
59
|
+
*
|
|
60
|
+
* useEffect(() => {
|
|
61
|
+
* client.getToken().then(setToken);
|
|
62
|
+
* }, [client]);
|
|
63
|
+
*
|
|
64
|
+
* return <div>Token: {token}</div>;
|
|
65
|
+
* }
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export function useAuthClient() {
|
|
69
|
+
const { client } = useAuth();
|
|
70
|
+
return client;
|
|
71
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAG5F,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAG1D,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,4 +6,5 @@ export { CohostStartCheckoutProvider } from './provider/CohostStartCheckoutProvi
|
|
|
6
6
|
export { CreditCardInformation } from './lib/tokenizers/types';
|
|
7
7
|
export { useCohost } from './hooks/useCohost';
|
|
8
8
|
export { formatCurrency } from './lib/utils';
|
|
9
|
+
export { AuthProvider, type AuthProviderProps, type AuthContextValue, useAuth, useUser, useAuthClient, AuthGuard, type AuthGuardProps, } from './auth';
|
|
9
10
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACnH,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnF,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,KAAK,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AACpG,OAAO,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AAC5F,OAAO,EAAE,2BAA2B,EAAE,MAAM,wCAAwC,CAAC;AACrF,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACnH,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnF,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,KAAK,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AACpG,OAAO,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AAC5F,OAAO,EAAE,2BAA2B,EAAE,MAAM,wCAAwC,CAAC;AACrF,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAG7C,OAAO,EACL,YAAY,EACZ,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,OAAO,EACP,OAAO,EACP,aAAa,EACb,SAAS,EACT,KAAK,cAAc,GACpB,MAAM,QAAQ,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -5,3 +5,5 @@ export { PaymentElementProvider, usePaymentElement } from './context/PaymentElem
|
|
|
5
5
|
export { CohostStartCheckoutProvider } from './provider/CohostStartCheckoutProvider';
|
|
6
6
|
export { useCohost } from './hooks/useCohost';
|
|
7
7
|
export { formatCurrency } from './lib/utils';
|
|
8
|
+
// Auth bindings
|
|
9
|
+
export { AuthProvider, useAuth, useUser, useAuthClient, AuthGuard, } from './auth';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cohostvip/cohost-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "React bindings for the Cohost API",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@cohostvip/cohost-node": "
|
|
30
|
+
"@cohostvip/cohost-node": "workspace:^",
|
|
31
|
+
"@cohostvip/cohost-auth": "workspace:*"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
33
34
|
"@testing-library/jest-dom": "6.6.3",
|