@ph-cms/client-sdk 0.1.4 → 0.1.6
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 +617 -22
- package/dist/auth/firebase-provider.d.ts +2 -1
- package/dist/auth/firebase-provider.js +3 -0
- package/dist/auth/interfaces.d.ts +6 -0
- package/dist/auth/local-provider.d.ts +1 -0
- package/dist/auth/local-provider.js +3 -0
- package/dist/client.d.ts +4 -2
- package/dist/client.js +6 -2
- package/dist/context.d.ts +4 -2
- package/dist/context.js +45 -14
- package/dist/hooks/useAuth.d.ts +25 -1
- package/dist/hooks/useAuth.js +12 -10
- package/dist/hooks/useFirebaseAuthSync.d.ts +108 -0
- package/dist/hooks/useFirebaseAuthSync.js +221 -0
- package/dist/index.d.ts +7 -6
- package/dist/index.js +7 -6
- package/package.json +1 -1
|
@@ -14,6 +14,9 @@ class FirebaseAuthProvider {
|
|
|
14
14
|
this.refreshToken = localStorage.getItem(`${this.storageKeyPrefix}refresh_token`);
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
+
hasToken() {
|
|
18
|
+
return this.accessToken !== null || this.refreshToken !== null;
|
|
19
|
+
}
|
|
17
20
|
setTokens(accessToken, refreshToken) {
|
|
18
21
|
this.accessToken = accessToken;
|
|
19
22
|
this.refreshToken = refreshToken;
|
|
@@ -5,6 +5,12 @@ export interface AuthProvider {
|
|
|
5
5
|
* Should handle refreshing if necessary (or return null/throw if expired and can't refresh).
|
|
6
6
|
*/
|
|
7
7
|
getToken(): Promise<string | null>;
|
|
8
|
+
/**
|
|
9
|
+
* Returns true if the provider currently holds a token (access or refresh).
|
|
10
|
+
* This is a synchronous check used to decide whether to attempt API calls
|
|
11
|
+
* that require authentication (e.g. /auth/me).
|
|
12
|
+
*/
|
|
13
|
+
hasToken(): boolean;
|
|
8
14
|
/**
|
|
9
15
|
* Sets a callback to be called when the token is known to be expired by the external world (e.g. 401 response).
|
|
10
16
|
*/
|
|
@@ -11,6 +11,7 @@ export declare class LocalAuthProvider implements AuthProvider {
|
|
|
11
11
|
refreshToken: string;
|
|
12
12
|
}>) | undefined);
|
|
13
13
|
setTokens(accessToken: string, refreshToken: string): void;
|
|
14
|
+
hasToken(): boolean;
|
|
14
15
|
getToken(): Promise<string | null>;
|
|
15
16
|
onTokenExpired(callback: () => Promise<void>): void;
|
|
16
17
|
logout(): Promise<void>;
|
|
@@ -22,6 +22,9 @@ class LocalAuthProvider {
|
|
|
22
22
|
localStorage.setItem(`${this.storageKeyPrefix}refresh_token`, refreshToken);
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
+
hasToken() {
|
|
26
|
+
return this.accessToken !== null || this.refreshToken !== null;
|
|
27
|
+
}
|
|
25
28
|
async getToken() {
|
|
26
29
|
// Ideally check expiration here using jwt-decode, but for now return what we have.
|
|
27
30
|
// If it's expired, the API will return 401, triggering the interceptor which calls onTokenExpired.
|
package/dist/client.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { AxiosInstance } from 'axios';
|
|
2
2
|
import { AuthProvider } from './auth/interfaces';
|
|
3
3
|
import { AuthModule } from './modules/auth';
|
|
4
|
-
import { ContentModule } from './modules/content';
|
|
5
4
|
import { ChannelModule } from './modules/channel';
|
|
6
|
-
import {
|
|
5
|
+
import { ContentModule } from './modules/content';
|
|
7
6
|
import { MediaModule } from './modules/media';
|
|
7
|
+
import { TermsModule } from './modules/terms';
|
|
8
8
|
export interface PHCMSClientConfig {
|
|
9
9
|
baseURL: string;
|
|
10
10
|
apiPrefix?: string;
|
|
@@ -19,5 +19,7 @@ export declare class PHCMSClient {
|
|
|
19
19
|
readonly channel: ChannelModule;
|
|
20
20
|
readonly terms: TermsModule;
|
|
21
21
|
readonly media: MediaModule;
|
|
22
|
+
/** Exposes the auth provider so UI layers can check token state synchronously. */
|
|
23
|
+
get authProvider(): AuthProvider | undefined;
|
|
22
24
|
constructor(config: PHCMSClientConfig);
|
|
23
25
|
}
|
package/dist/client.js
CHANGED
|
@@ -7,11 +7,15 @@ exports.PHCMSClient = void 0;
|
|
|
7
7
|
const axios_1 = __importDefault(require("axios"));
|
|
8
8
|
const errors_1 = require("./errors");
|
|
9
9
|
const auth_1 = require("./modules/auth");
|
|
10
|
-
const content_1 = require("./modules/content");
|
|
11
10
|
const channel_1 = require("./modules/channel");
|
|
12
|
-
const
|
|
11
|
+
const content_1 = require("./modules/content");
|
|
13
12
|
const media_1 = require("./modules/media");
|
|
13
|
+
const terms_1 = require("./modules/terms");
|
|
14
14
|
class PHCMSClient {
|
|
15
|
+
/** Exposes the auth provider so UI layers can check token state synchronously. */
|
|
16
|
+
get authProvider() {
|
|
17
|
+
return this.config.auth;
|
|
18
|
+
}
|
|
15
19
|
constructor(config) {
|
|
16
20
|
this.config = config;
|
|
17
21
|
const normalizedApiPrefix = `/${(config.apiPrefix || '/api').replace(/^\/+|\/+$/g, '')}`;
|
package/dist/context.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { UserDto } from '@ph-cms/api-contract';
|
|
2
2
|
import { QueryClient } from '@tanstack/react-query';
|
|
3
|
+
import React, { ReactNode } from 'react';
|
|
3
4
|
import { PHCMSClient } from './client';
|
|
4
|
-
import { UserDto } from '@ph-cms/api-contract';
|
|
5
5
|
export interface PHCMSContextType {
|
|
6
6
|
client: PHCMSClient;
|
|
7
7
|
user: UserDto | null;
|
|
8
8
|
isAuthenticated: boolean;
|
|
9
9
|
isLoading: boolean;
|
|
10
|
+
/** Manually trigger a refetch of the current user profile. */
|
|
11
|
+
refreshUser: () => Promise<void>;
|
|
10
12
|
}
|
|
11
13
|
export interface PHCMSProviderProps {
|
|
12
14
|
client: PHCMSClient;
|
package/dist/context.js
CHANGED
|
@@ -34,40 +34,71 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.usePHCMS = exports.usePHCMSContext = exports.PHCMSProvider = void 0;
|
|
37
|
-
const react_1 = __importStar(require("react"));
|
|
38
37
|
const react_query_1 = require("@tanstack/react-query");
|
|
38
|
+
const react_1 = __importStar(require("react"));
|
|
39
39
|
const PHCMSContext = (0, react_1.createContext)(null);
|
|
40
|
+
const AUTH_ME_QUERY_KEY = ['auth', 'me'];
|
|
40
41
|
/**
|
|
41
42
|
* Internal component to handle auth state and provide to context
|
|
42
43
|
*/
|
|
43
44
|
const PHCMSInternalProvider = ({ client, children }) => {
|
|
44
|
-
const
|
|
45
|
-
|
|
45
|
+
const queryClient = (0, react_query_1.useQueryClient)();
|
|
46
|
+
// Track token presence in state to trigger re-renders when it changes.
|
|
47
|
+
// Initial value from provider.
|
|
48
|
+
const [hasToken, setHasToken] = (0, react_1.useState)(() => client.authProvider?.hasToken() ?? false);
|
|
49
|
+
// The 'me' query is only enabled if we have a token.
|
|
50
|
+
const { data: user, isLoading, isError, refetch } = (0, react_query_1.useQuery)({
|
|
51
|
+
queryKey: [...AUTH_ME_QUERY_KEY],
|
|
46
52
|
queryFn: () => client.auth.me(),
|
|
53
|
+
enabled: hasToken,
|
|
47
54
|
retry: false,
|
|
48
55
|
staleTime: 1000 * 60 * 5,
|
|
49
56
|
});
|
|
57
|
+
// Function to update token state and optionally refetch
|
|
58
|
+
const refreshUser = (0, react_1.useCallback)(async () => {
|
|
59
|
+
const currentHasToken = client.authProvider?.hasToken() ?? false;
|
|
60
|
+
setHasToken(currentHasToken);
|
|
61
|
+
if (currentHasToken) {
|
|
62
|
+
await refetch();
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
// If no token, clear the query data
|
|
66
|
+
queryClient.setQueryData([...AUTH_ME_QUERY_KEY], null);
|
|
67
|
+
}
|
|
68
|
+
}, [client.authProvider, refetch, queryClient]);
|
|
69
|
+
// Effect to sync state if provider changes externally (rare but possible)
|
|
70
|
+
(0, react_1.useEffect)(() => {
|
|
71
|
+
const interval = setInterval(() => {
|
|
72
|
+
const currentHasToken = client.authProvider?.hasToken() ?? false;
|
|
73
|
+
if (currentHasToken !== hasToken) {
|
|
74
|
+
setHasToken(currentHasToken);
|
|
75
|
+
}
|
|
76
|
+
}, 2000); // Check every 2s as a fallback
|
|
77
|
+
return () => clearInterval(interval);
|
|
78
|
+
}, [client.authProvider, hasToken]);
|
|
50
79
|
const value = (0, react_1.useMemo)(() => ({
|
|
51
80
|
client,
|
|
52
81
|
user: user || null,
|
|
53
|
-
isAuthenticated: !!user && !isError,
|
|
54
|
-
isLoading,
|
|
55
|
-
|
|
56
|
-
|
|
82
|
+
isAuthenticated: !!user && !isError && hasToken,
|
|
83
|
+
isLoading: hasToken ? isLoading : false,
|
|
84
|
+
refreshUser,
|
|
85
|
+
}), [client, user, isError, isLoading, hasToken, refreshUser]);
|
|
86
|
+
return react_1.default.createElement(PHCMSContext.Provider, { value: value }, children);
|
|
57
87
|
};
|
|
58
88
|
/**
|
|
59
89
|
* Root Provider for PH-CMS
|
|
60
90
|
* Automatically includes QueryClientProvider
|
|
61
91
|
*/
|
|
62
92
|
const PHCMSProvider = ({ client, queryClient, children }) => {
|
|
63
|
-
const internalQueryClient = (0, react_1.useMemo)(() => queryClient ??
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
93
|
+
const internalQueryClient = (0, react_1.useMemo)(() => queryClient ??
|
|
94
|
+
new react_query_1.QueryClient({
|
|
95
|
+
defaultOptions: {
|
|
96
|
+
queries: {
|
|
97
|
+
refetchOnWindowFocus: false,
|
|
98
|
+
retry: false,
|
|
99
|
+
},
|
|
68
100
|
},
|
|
69
|
-
},
|
|
70
|
-
}), [queryClient]);
|
|
101
|
+
}), [queryClient]);
|
|
71
102
|
return (react_1.default.createElement(react_query_1.QueryClientProvider, { client: internalQueryClient },
|
|
72
103
|
react_1.default.createElement(PHCMSInternalProvider, { client: client }, children)));
|
|
73
104
|
};
|
package/dist/hooks/useAuth.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Unified Auth Hook
|
|
3
|
-
* Returns both the current auth status and the actions
|
|
3
|
+
* Returns both the current auth status and the actions.
|
|
4
4
|
*/
|
|
5
5
|
export declare const useAuth: () => {
|
|
6
6
|
user: {
|
|
@@ -128,6 +128,30 @@ export declare const useAuth: () => {
|
|
|
128
128
|
email: string;
|
|
129
129
|
password: string;
|
|
130
130
|
}, unknown>;
|
|
131
|
+
loginWithFirebaseStatus: import("@tanstack/react-query").UseMutationResult<{
|
|
132
|
+
refreshToken: string;
|
|
133
|
+
user: {
|
|
134
|
+
uid: string;
|
|
135
|
+
email: string;
|
|
136
|
+
username: string | null;
|
|
137
|
+
display_name: string;
|
|
138
|
+
avatar_url: string | null;
|
|
139
|
+
phone_number: string | null;
|
|
140
|
+
email_verified_at: string | null;
|
|
141
|
+
phone_verified_at: string | null;
|
|
142
|
+
locale: string;
|
|
143
|
+
timezone: string;
|
|
144
|
+
status: string;
|
|
145
|
+
role: string[];
|
|
146
|
+
profile_data: Record<string, any>;
|
|
147
|
+
last_login_at: string | null;
|
|
148
|
+
created_at: string;
|
|
149
|
+
updated_at: string;
|
|
150
|
+
};
|
|
151
|
+
accessToken: string;
|
|
152
|
+
}, Error, {
|
|
153
|
+
idToken: string;
|
|
154
|
+
}, unknown>;
|
|
131
155
|
registerStatus: import("@tanstack/react-query").UseMutationResult<{
|
|
132
156
|
refreshToken: string;
|
|
133
157
|
user: {
|
package/dist/hooks/useAuth.js
CHANGED
|
@@ -3,36 +3,37 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.useLogout = exports.useLogin = exports.useUser = exports.useAuth = void 0;
|
|
4
4
|
const react_query_1 = require("@tanstack/react-query");
|
|
5
5
|
const context_1 = require("../context");
|
|
6
|
+
const AUTH_ME_QUERY_KEY = ['auth', 'me'];
|
|
6
7
|
/**
|
|
7
8
|
* Unified Auth Hook
|
|
8
|
-
* Returns both the current auth status and the actions
|
|
9
|
+
* Returns both the current auth status and the actions.
|
|
9
10
|
*/
|
|
10
11
|
const useAuth = () => {
|
|
11
|
-
const { user, isAuthenticated, isLoading } = (0, context_1.usePHCMSContext)();
|
|
12
|
+
const { user, isAuthenticated, isLoading, refreshUser } = (0, context_1.usePHCMSContext)();
|
|
12
13
|
const client = (0, context_1.usePHCMS)();
|
|
13
14
|
const queryClient = (0, react_query_1.useQueryClient)();
|
|
14
15
|
const loginMutation = (0, react_query_1.useMutation)({
|
|
15
16
|
mutationFn: (data) => client.auth.login(data),
|
|
16
|
-
onSuccess: () => {
|
|
17
|
-
|
|
17
|
+
onSuccess: async () => {
|
|
18
|
+
await refreshUser();
|
|
18
19
|
},
|
|
19
20
|
});
|
|
20
21
|
const loginWithFirebaseMutation = (0, react_query_1.useMutation)({
|
|
21
22
|
mutationFn: (data) => client.auth.loginWithFirebase(data),
|
|
22
|
-
onSuccess: () => {
|
|
23
|
-
|
|
23
|
+
onSuccess: async () => {
|
|
24
|
+
await refreshUser();
|
|
24
25
|
},
|
|
25
26
|
});
|
|
26
27
|
const registerMutation = (0, react_query_1.useMutation)({
|
|
27
28
|
mutationFn: (data) => client.auth.register(data),
|
|
28
|
-
onSuccess: () => {
|
|
29
|
-
|
|
29
|
+
onSuccess: async () => {
|
|
30
|
+
await refreshUser();
|
|
30
31
|
},
|
|
31
32
|
});
|
|
32
33
|
const logoutMutation = (0, react_query_1.useMutation)({
|
|
33
34
|
mutationFn: () => client.auth.logout(),
|
|
34
|
-
onSuccess: () => {
|
|
35
|
-
|
|
35
|
+
onSuccess: async () => {
|
|
36
|
+
await refreshUser(); // This will clear state since hasToken will be false
|
|
36
37
|
queryClient.invalidateQueries();
|
|
37
38
|
},
|
|
38
39
|
});
|
|
@@ -46,6 +47,7 @@ const useAuth = () => {
|
|
|
46
47
|
logout: logoutMutation.mutateAsync,
|
|
47
48
|
// Access to mutation objects for loading/error states if needed
|
|
48
49
|
loginStatus: loginMutation,
|
|
50
|
+
loginWithFirebaseStatus: loginWithFirebaseMutation,
|
|
49
51
|
registerStatus: registerMutation,
|
|
50
52
|
logoutStatus: logoutMutation,
|
|
51
53
|
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { Auth } from 'firebase/auth';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
export interface UseFirebaseAuthSyncOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Firebase Auth instance.
|
|
6
|
+
* Must be provided so the hook can subscribe to `onAuthStateChanged`.
|
|
7
|
+
*/
|
|
8
|
+
firebaseAuth: Auth;
|
|
9
|
+
/**
|
|
10
|
+
* If true, automatically call `logout()` on the PH-CMS side
|
|
11
|
+
* when the Firebase user signs out.
|
|
12
|
+
* @default true
|
|
13
|
+
*/
|
|
14
|
+
logoutOnFirebaseSignOut?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Called when the sync exchange succeeds.
|
|
17
|
+
*/
|
|
18
|
+
onSyncSuccess?: () => void;
|
|
19
|
+
/**
|
|
20
|
+
* Called when the sync exchange fails.
|
|
21
|
+
*/
|
|
22
|
+
onSyncError?: (error: unknown) => void;
|
|
23
|
+
}
|
|
24
|
+
export interface UseFirebaseAuthSyncReturn {
|
|
25
|
+
/** Whether a token-exchange request is currently in flight. */
|
|
26
|
+
isSyncing: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* `useFirebaseAuthSync`
|
|
30
|
+
*
|
|
31
|
+
* Subscribes to Firebase `onAuthStateChanged` and automatically synchronizes
|
|
32
|
+
* the Firebase authentication state with the PH-CMS backend:
|
|
33
|
+
*
|
|
34
|
+
* - **Firebase user signs in** and PH-CMS is not yet authenticated →
|
|
35
|
+
* obtains a Firebase ID token and calls `loginWithFirebase({ idToken })`
|
|
36
|
+
* which exchanges the token for PH-CMS access/refresh tokens and then
|
|
37
|
+
* fetches the user profile via `/auth/me`.
|
|
38
|
+
*
|
|
39
|
+
* - **Firebase user signs out** and PH-CMS is still authenticated →
|
|
40
|
+
* calls `logout()` on PH-CMS (configurable via `logoutOnFirebaseSignOut`).
|
|
41
|
+
*
|
|
42
|
+
* The hook must be rendered **inside** `<PHCMSProvider>` because it relies
|
|
43
|
+
* on context values (`isAuthenticated`, `isLoading`) and the `loginWithFirebase`
|
|
44
|
+
* / `logout` actions exposed by the auth module.
|
|
45
|
+
*
|
|
46
|
+
* ### Example
|
|
47
|
+
*
|
|
48
|
+
* ```tsx
|
|
49
|
+
* import { useFirebaseAuthSync } from '@ph-cms/client-sdk';
|
|
50
|
+
* import { getAuth } from 'firebase/auth';
|
|
51
|
+
*
|
|
52
|
+
* const firebaseAuth = getAuth(firebaseApp);
|
|
53
|
+
*
|
|
54
|
+
* function App() {
|
|
55
|
+
* useFirebaseAuthSync({ firebaseAuth });
|
|
56
|
+
* return <MainContent />;
|
|
57
|
+
* }
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export declare const useFirebaseAuthSync: (options: UseFirebaseAuthSyncOptions) => UseFirebaseAuthSyncReturn;
|
|
61
|
+
export interface FirebaseAuthSyncProps {
|
|
62
|
+
/**
|
|
63
|
+
* Firebase Auth instance.
|
|
64
|
+
*/
|
|
65
|
+
firebaseAuth: Auth;
|
|
66
|
+
/**
|
|
67
|
+
* If true, automatically call `logout()` on the PH-CMS side
|
|
68
|
+
* when the Firebase user signs out.
|
|
69
|
+
* @default true
|
|
70
|
+
*/
|
|
71
|
+
logoutOnFirebaseSignOut?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Called when the sync exchange succeeds.
|
|
74
|
+
*/
|
|
75
|
+
onSyncSuccess?: () => void;
|
|
76
|
+
/**
|
|
77
|
+
* Called when the sync exchange fails.
|
|
78
|
+
*/
|
|
79
|
+
onSyncError?: (error: unknown) => void;
|
|
80
|
+
children: React.ReactNode;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* `<FirebaseAuthSync>`
|
|
84
|
+
*
|
|
85
|
+
* A convenience wrapper component that calls `useFirebaseAuthSync` internally.
|
|
86
|
+
* Place it **inside** `<PHCMSProvider>` and wrap the part of the tree that
|
|
87
|
+
* should wait for or react to Firebase↔PH-CMS auth synchronization.
|
|
88
|
+
*
|
|
89
|
+
* ### Example
|
|
90
|
+
*
|
|
91
|
+
* ```tsx
|
|
92
|
+
* import { PHCMSProvider, FirebaseAuthSync } from '@ph-cms/client-sdk';
|
|
93
|
+
* import { getAuth } from 'firebase/auth';
|
|
94
|
+
*
|
|
95
|
+
* const firebaseAuth = getAuth(firebaseApp);
|
|
96
|
+
*
|
|
97
|
+
* function App() {
|
|
98
|
+
* return (
|
|
99
|
+
* <PHCMSProvider client={client}>
|
|
100
|
+
* <FirebaseAuthSync firebaseAuth={firebaseAuth}>
|
|
101
|
+
* <MainContent />
|
|
102
|
+
* </FirebaseAuthSync>
|
|
103
|
+
* </PHCMSProvider>
|
|
104
|
+
* );
|
|
105
|
+
* }
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
export declare const FirebaseAuthSync: React.FC<FirebaseAuthSyncProps>;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.FirebaseAuthSync = exports.useFirebaseAuthSync = void 0;
|
|
37
|
+
const react_1 = __importStar(require("react"));
|
|
38
|
+
const context_1 = require("../context");
|
|
39
|
+
/**
|
|
40
|
+
* `useFirebaseAuthSync`
|
|
41
|
+
*
|
|
42
|
+
* Subscribes to Firebase `onAuthStateChanged` and automatically synchronizes
|
|
43
|
+
* the Firebase authentication state with the PH-CMS backend:
|
|
44
|
+
*
|
|
45
|
+
* - **Firebase user signs in** and PH-CMS is not yet authenticated →
|
|
46
|
+
* obtains a Firebase ID token and calls `loginWithFirebase({ idToken })`
|
|
47
|
+
* which exchanges the token for PH-CMS access/refresh tokens and then
|
|
48
|
+
* fetches the user profile via `/auth/me`.
|
|
49
|
+
*
|
|
50
|
+
* - **Firebase user signs out** and PH-CMS is still authenticated →
|
|
51
|
+
* calls `logout()` on PH-CMS (configurable via `logoutOnFirebaseSignOut`).
|
|
52
|
+
*
|
|
53
|
+
* The hook must be rendered **inside** `<PHCMSProvider>` because it relies
|
|
54
|
+
* on context values (`isAuthenticated`, `isLoading`) and the `loginWithFirebase`
|
|
55
|
+
* / `logout` actions exposed by the auth module.
|
|
56
|
+
*
|
|
57
|
+
* ### Example
|
|
58
|
+
*
|
|
59
|
+
* ```tsx
|
|
60
|
+
* import { useFirebaseAuthSync } from '@ph-cms/client-sdk';
|
|
61
|
+
* import { getAuth } from 'firebase/auth';
|
|
62
|
+
*
|
|
63
|
+
* const firebaseAuth = getAuth(firebaseApp);
|
|
64
|
+
*
|
|
65
|
+
* function App() {
|
|
66
|
+
* useFirebaseAuthSync({ firebaseAuth });
|
|
67
|
+
* return <MainContent />;
|
|
68
|
+
* }
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
const useFirebaseAuthSync = (options) => {
|
|
72
|
+
const { firebaseAuth, logoutOnFirebaseSignOut = true, onSyncSuccess, onSyncError, } = options;
|
|
73
|
+
const { client, isAuthenticated, isLoading, refreshUser } = (0, context_1.usePHCMSContext)();
|
|
74
|
+
// Use refs for values that change frequently but should not re-trigger
|
|
75
|
+
// the effect (which would tear down and re-create the Firebase listener).
|
|
76
|
+
const isAuthenticatedRef = (0, react_1.useRef)(isAuthenticated);
|
|
77
|
+
const isLoadingRef = (0, react_1.useRef)(isLoading);
|
|
78
|
+
const syncInProgressRef = (0, react_1.useRef)(false);
|
|
79
|
+
const isSyncingStateRef = (0, react_1.useRef)(false);
|
|
80
|
+
const [isSyncing, setIsSyncing] = react_1.default.useState(false);
|
|
81
|
+
// Keep refs up to date.
|
|
82
|
+
(0, react_1.useEffect)(() => {
|
|
83
|
+
isAuthenticatedRef.current = isAuthenticated;
|
|
84
|
+
}, [isAuthenticated]);
|
|
85
|
+
(0, react_1.useEffect)(() => {
|
|
86
|
+
isLoadingRef.current = isLoading;
|
|
87
|
+
}, [isLoading]);
|
|
88
|
+
// Stable callback refs so the effect closure doesn't go stale.
|
|
89
|
+
const onSyncSuccessRef = (0, react_1.useRef)(onSyncSuccess);
|
|
90
|
+
const onSyncErrorRef = (0, react_1.useRef)(onSyncError);
|
|
91
|
+
(0, react_1.useEffect)(() => {
|
|
92
|
+
onSyncSuccessRef.current = onSyncSuccess;
|
|
93
|
+
}, [onSyncSuccess]);
|
|
94
|
+
(0, react_1.useEffect)(() => {
|
|
95
|
+
onSyncErrorRef.current = onSyncError;
|
|
96
|
+
}, [onSyncError]);
|
|
97
|
+
const handleFirebaseUser = (0, react_1.useCallback)(async (fbUser) => {
|
|
98
|
+
if (fbUser) {
|
|
99
|
+
// Firebase user is signed in.
|
|
100
|
+
// Only exchange if PH-CMS is not already authenticated and not loading.
|
|
101
|
+
if (!isAuthenticatedRef.current &&
|
|
102
|
+
!isLoadingRef.current &&
|
|
103
|
+
!syncInProgressRef.current) {
|
|
104
|
+
syncInProgressRef.current = true;
|
|
105
|
+
isSyncingStateRef.current = true;
|
|
106
|
+
setIsSyncing(true);
|
|
107
|
+
try {
|
|
108
|
+
const idToken = await fbUser.getIdToken();
|
|
109
|
+
await client.auth.loginWithFirebase({ idToken });
|
|
110
|
+
// loginWithFirebase stores the tokens in the provider.
|
|
111
|
+
// Now refresh the context so `isAuthenticated` and `user` update.
|
|
112
|
+
await refreshUser();
|
|
113
|
+
onSyncSuccessRef.current?.();
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
console.error('[useFirebaseAuthSync] Token exchange failed:', error);
|
|
117
|
+
onSyncErrorRef.current?.(error);
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
syncInProgressRef.current = false;
|
|
121
|
+
isSyncingStateRef.current = false;
|
|
122
|
+
setIsSyncing(false);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
// Firebase user signed out.
|
|
128
|
+
if (logoutOnFirebaseSignOut && isAuthenticatedRef.current) {
|
|
129
|
+
try {
|
|
130
|
+
await client.auth.logout();
|
|
131
|
+
await refreshUser();
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
console.error('[useFirebaseAuthSync] Logout failed:', error);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
// `client`, `refreshUser`, and `logoutOnFirebaseSignOut` are stable
|
|
140
|
+
// across renders (client is a singleton, refreshUser is memoized,
|
|
141
|
+
// logoutOnFirebaseSignOut is typically a constant).
|
|
142
|
+
[client, refreshUser, logoutOnFirebaseSignOut]);
|
|
143
|
+
(0, react_1.useEffect)(() => {
|
|
144
|
+
// Dynamically import the listener so that this file has zero runtime
|
|
145
|
+
// dependency on firebase/auth at the module level. The `Auth` object
|
|
146
|
+
// passed in already carries the implementation; we just need the
|
|
147
|
+
// `onAuthStateChanged` function reference from it.
|
|
148
|
+
//
|
|
149
|
+
// Firebase Auth's `onAuthStateChanged` is available on the Auth object
|
|
150
|
+
// via the modular API through the standalone function, but we can also
|
|
151
|
+
// call it as `firebaseAuth.onAuthStateChanged(...)` which works on the
|
|
152
|
+
// Auth instance directly (compat style). However, the recommended
|
|
153
|
+
// modular approach is to use the standalone function.
|
|
154
|
+
//
|
|
155
|
+
// To avoid directly importing from 'firebase/auth' at the top level
|
|
156
|
+
// (since it's an optional peer dep), we use a dynamic import wrapped
|
|
157
|
+
// in a try-catch. If the import fails, we fall back to a noop.
|
|
158
|
+
let unsubscribe = null;
|
|
159
|
+
let cancelled = false;
|
|
160
|
+
const setup = async () => {
|
|
161
|
+
try {
|
|
162
|
+
// Dynamic import so builds without firebase don't break.
|
|
163
|
+
const firebaseAuthModule = await Promise.resolve().then(() => __importStar(require('firebase/auth')));
|
|
164
|
+
if (cancelled)
|
|
165
|
+
return;
|
|
166
|
+
unsubscribe = firebaseAuthModule.onAuthStateChanged(firebaseAuth, (fbUser) => {
|
|
167
|
+
// Fire and forget; errors are caught inside handleFirebaseUser.
|
|
168
|
+
void handleFirebaseUser(fbUser);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// firebase/auth is not installed — this is fine, it's optional.
|
|
173
|
+
console.warn('[useFirebaseAuthSync] firebase/auth could not be imported. ' +
|
|
174
|
+
'Make sure it is installed if you intend to use Firebase auth sync.');
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
void setup();
|
|
178
|
+
return () => {
|
|
179
|
+
cancelled = true;
|
|
180
|
+
unsubscribe?.();
|
|
181
|
+
};
|
|
182
|
+
}, [firebaseAuth, handleFirebaseUser]);
|
|
183
|
+
return { isSyncing };
|
|
184
|
+
};
|
|
185
|
+
exports.useFirebaseAuthSync = useFirebaseAuthSync;
|
|
186
|
+
/**
|
|
187
|
+
* `<FirebaseAuthSync>`
|
|
188
|
+
*
|
|
189
|
+
* A convenience wrapper component that calls `useFirebaseAuthSync` internally.
|
|
190
|
+
* Place it **inside** `<PHCMSProvider>` and wrap the part of the tree that
|
|
191
|
+
* should wait for or react to Firebase↔PH-CMS auth synchronization.
|
|
192
|
+
*
|
|
193
|
+
* ### Example
|
|
194
|
+
*
|
|
195
|
+
* ```tsx
|
|
196
|
+
* import { PHCMSProvider, FirebaseAuthSync } from '@ph-cms/client-sdk';
|
|
197
|
+
* import { getAuth } from 'firebase/auth';
|
|
198
|
+
*
|
|
199
|
+
* const firebaseAuth = getAuth(firebaseApp);
|
|
200
|
+
*
|
|
201
|
+
* function App() {
|
|
202
|
+
* return (
|
|
203
|
+
* <PHCMSProvider client={client}>
|
|
204
|
+
* <FirebaseAuthSync firebaseAuth={firebaseAuth}>
|
|
205
|
+
* <MainContent />
|
|
206
|
+
* </FirebaseAuthSync>
|
|
207
|
+
* </PHCMSProvider>
|
|
208
|
+
* );
|
|
209
|
+
* }
|
|
210
|
+
* ```
|
|
211
|
+
*/
|
|
212
|
+
const FirebaseAuthSync = ({ firebaseAuth, logoutOnFirebaseSignOut, onSyncSuccess, onSyncError, children, }) => {
|
|
213
|
+
(0, exports.useFirebaseAuthSync)({
|
|
214
|
+
firebaseAuth,
|
|
215
|
+
logoutOnFirebaseSignOut,
|
|
216
|
+
onSyncSuccess,
|
|
217
|
+
onSyncError,
|
|
218
|
+
});
|
|
219
|
+
return react_1.default.createElement(react_1.default.Fragment, null, children);
|
|
220
|
+
};
|
|
221
|
+
exports.FirebaseAuthSync = FirebaseAuthSync;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
+
export * from './auth/firebase-provider';
|
|
1
2
|
export * from './auth/interfaces';
|
|
2
3
|
export * from './auth/local-provider';
|
|
3
|
-
export * from './auth/firebase-provider';
|
|
4
|
-
export * from './errors';
|
|
5
4
|
export * from './client';
|
|
5
|
+
export * from './errors';
|
|
6
6
|
export * from './modules/auth';
|
|
7
|
-
export * from './modules/content';
|
|
8
7
|
export * from './modules/channel';
|
|
9
|
-
export * from './modules/
|
|
8
|
+
export * from './modules/content';
|
|
10
9
|
export * from './modules/media';
|
|
10
|
+
export * from './modules/terms';
|
|
11
11
|
export * from './context';
|
|
12
12
|
export * from './hooks/useAuth';
|
|
13
|
-
export * from './hooks/useContent';
|
|
14
13
|
export * from './hooks/useChannel';
|
|
15
|
-
export * from './hooks/
|
|
14
|
+
export * from './hooks/useContent';
|
|
15
|
+
export * from './hooks/useFirebaseAuthSync';
|
|
16
16
|
export * from './hooks/useMedia';
|
|
17
|
+
export * from './hooks/useTerms';
|
|
17
18
|
export * from './types';
|
package/dist/index.js
CHANGED
|
@@ -14,20 +14,21 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./auth/firebase-provider"), exports);
|
|
17
18
|
__exportStar(require("./auth/interfaces"), exports);
|
|
18
19
|
__exportStar(require("./auth/local-provider"), exports);
|
|
19
|
-
__exportStar(require("./auth/firebase-provider"), exports);
|
|
20
|
-
__exportStar(require("./errors"), exports);
|
|
21
20
|
__exportStar(require("./client"), exports);
|
|
21
|
+
__exportStar(require("./errors"), exports);
|
|
22
22
|
__exportStar(require("./modules/auth"), exports);
|
|
23
|
-
__exportStar(require("./modules/content"), exports);
|
|
24
23
|
__exportStar(require("./modules/channel"), exports);
|
|
25
|
-
__exportStar(require("./modules/
|
|
24
|
+
__exportStar(require("./modules/content"), exports);
|
|
26
25
|
__exportStar(require("./modules/media"), exports);
|
|
26
|
+
__exportStar(require("./modules/terms"), exports);
|
|
27
27
|
__exportStar(require("./context"), exports);
|
|
28
28
|
__exportStar(require("./hooks/useAuth"), exports);
|
|
29
|
-
__exportStar(require("./hooks/useContent"), exports);
|
|
30
29
|
__exportStar(require("./hooks/useChannel"), exports);
|
|
31
|
-
__exportStar(require("./hooks/
|
|
30
|
+
__exportStar(require("./hooks/useContent"), exports);
|
|
31
|
+
__exportStar(require("./hooks/useFirebaseAuthSync"), exports);
|
|
32
32
|
__exportStar(require("./hooks/useMedia"), exports);
|
|
33
|
+
__exportStar(require("./hooks/useTerms"), exports);
|
|
33
34
|
__exportStar(require("./types"), exports);
|