cloudflare-next-intl 0.7.1 → 0.7.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/README.md +11 -0
- package/dist/src/client/components/client_provider.d.ts +4 -2
- package/dist/src/client/components/client_provider.js +5 -3
- package/dist/src/client/components/client_provider_static.d.ts +4 -2
- package/dist/src/client/components/client_provider_static.js +3 -2
- package/dist/src/cookie_consent/client/components/auto_analytics_events.d.ts +12 -0
- package/dist/src/cookie_consent/client/components/auto_analytics_events.js +58 -0
- package/dist/src/firebase_auth/client/auth_actions.d.ts +4 -4
- package/dist/src/firebase_auth/client/auth_actions.js +4 -4
- package/dist/src/firebase_auth/client/auth_user_provider.d.ts +2 -2
- package/dist/src/firebase_auth/client/auth_user_provider.js +2 -2
- package/dist/src/firebase_auth/client/components/auto_firebase_performance_events.d.ts +10 -0
- package/dist/src/firebase_auth/client/components/auto_firebase_performance_events.js +111 -0
- package/dist/src/firebase_auth/client/firebase_client.d.ts +3 -0
- package/dist/src/firebase_auth/client/firebase_client.js +14 -1
- package/dist/src/firebase_auth/index.d.ts +1 -1
- package/dist/src/firebase_auth/index.js +1 -1
- package/dist/src/firebase_auth/middleware/update_session.js +31 -1
- package/dist/src/firebase_auth/types.d.ts +15 -0
- package/dist/src/server/components/server_provider.js +1 -1
- package/dist/src/server/components/server_provider_static.js +1 -1
- package/dist/src/types/types.d.ts +36 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -247,12 +247,23 @@ export async function generateMetadata({ params }) {
|
|
|
247
247
|
|
|
248
248
|
### Theme switcher
|
|
249
249
|
|
|
250
|
+
### Firebase Auth
|
|
251
|
+
|
|
252
|
+
Set `firebaseAuth` on your `RoutingConfig` to enable — `IntlProvider` then
|
|
253
|
+
auto-wires `AuthUserProvider` and server-side `getAuthUser` session validation.
|
|
254
|
+
|
|
255
|
+
Features include:
|
|
256
|
+
- `createForgotPasswordAction(locale, actionCodeSettings?)`: Accepts optional Firebase `AuthActionCodeSettings` (e.g. `url` redirect link).
|
|
257
|
+
- `sendVerificationEmail(actionCodeSettings?)` on `useAuthUser()`: Custom action email settings when resending email verification.
|
|
258
|
+
- `followSameOriginContinueUrl`: Automatically forwards emailed action links with `continueUrl` to the specified path (or external URL) directly from `intlMiddleware` (default `true`; set `false` on `firebaseAuth` config to disable).
|
|
259
|
+
|
|
250
260
|
```tsx
|
|
251
261
|
import ThemeSwitcher from "cloudflare-next-intl/ThemeSwitcher";
|
|
252
262
|
|
|
253
263
|
<ThemeSwitcher lightLabelText="Light" darkLabelText="Dark" />
|
|
254
264
|
```
|
|
255
265
|
|
|
266
|
+
|
|
256
267
|
### Cookie consent
|
|
257
268
|
|
|
258
269
|
Set `cookieConsent` on your `RoutingConfig` to enable — `IntlProvider` then
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { TranslationObject } from "../../types/types";
|
|
2
2
|
import type { SerializedAuthUser } from "../../firebase_auth/types";
|
|
3
|
-
import type { CookieConsentAnalyticsConfig } from "../../types/types";
|
|
3
|
+
import type { CookieConsentAnalyticsConfig, AutoAnalyticsEventsConfig } from "../../types/types";
|
|
4
4
|
import type { CookieConsentDialogProps } from "../../cookie_consent/client/components/cookie_consent_dialog";
|
|
5
5
|
import type { PrivacyPolicyUpdateDialogProps } from "../../cookie_consent/client/components/privacy_policy_update_dialog";
|
|
6
6
|
interface LocaleContextType {
|
|
@@ -8,7 +8,7 @@ interface LocaleContextType {
|
|
|
8
8
|
messages: TranslationObject;
|
|
9
9
|
}
|
|
10
10
|
export declare const LocaleContext: import("react").Context<LocaleContextType | undefined>;
|
|
11
|
-
export default function LocationzationClientProvider({ language, messages, initialAuthUser, skipAuthProvider, analyticsConfig, requiresConsent, autoWireDialogs, dialogProps, updateDialogProps, children }: {
|
|
11
|
+
export default function LocationzationClientProvider({ language, messages, initialAuthUser, skipAuthProvider, analyticsConfig, autoAnalyticsEventsConfig, requiresConsent, autoWireDialogs, dialogProps, updateDialogProps, children }: {
|
|
12
12
|
language: string;
|
|
13
13
|
messages: TranslationObject;
|
|
14
14
|
initialAuthUser?: SerializedAuthUser | null;
|
|
@@ -16,6 +16,8 @@ export default function LocationzationClientProvider({ language, messages, initi
|
|
|
16
16
|
skipAuthProvider?: boolean;
|
|
17
17
|
/** Resolved server-side from `cookieConsent.analytics`/`getAnalytics` when `autoWireAnalytics` isn't `false`. */
|
|
18
18
|
analyticsConfig?: CookieConsentAnalyticsConfig;
|
|
19
|
+
/** From `cookieConsent.autoAnalyticsEvents` — forwarded as-is to the auto-wired `AutoAnalyticsEvents`. */
|
|
20
|
+
autoAnalyticsEventsConfig?: AutoAnalyticsEventsConfig;
|
|
19
21
|
/**
|
|
20
22
|
* Resolved server-side from `cookieConsent.getCountryCode`/`gdprCountries`.
|
|
21
23
|
* `false` means the visitor's country doesn't require the consent
|
|
@@ -15,11 +15,13 @@ export const LocaleContext = createContext(undefined);
|
|
|
15
15
|
// update (and a `getIdToken(true)` refresh) that causes another render —
|
|
16
16
|
// an infinite loop of session-cookie writes, one per render.
|
|
17
17
|
const AuthUserProvider = dynamic(() => import("../../firebase_auth/client/auth_user_provider"));
|
|
18
|
+
const AutoFirebasePerformanceEvents = dynamic(() => import("../../firebase_auth/client/components/auto_firebase_performance_events"));
|
|
18
19
|
const CookieConsentProvider = dynamic(() => import("../../cookie_consent/client/cookie_consent_provider"));
|
|
19
20
|
const CookieConsentAnalytics = dynamic(() => import("../../cookie_consent/client/components/cookie_consent_analytics"));
|
|
21
|
+
const AutoAnalyticsEvents = dynamic(() => import("../../cookie_consent/client/components/auto_analytics_events"));
|
|
20
22
|
const CookieConsentDialog = dynamic(() => import("../../cookie_consent/client/components/cookie_consent_dialog"));
|
|
21
23
|
const PrivacyPolicyUpdateDialog = dynamic(() => import("../../cookie_consent/client/components/privacy_policy_update_dialog"));
|
|
22
|
-
export default function LocationzationClientProvider({ language, messages, initialAuthUser = null, skipAuthProvider = false, analyticsConfig, requiresConsent = true, autoWireDialogs = true, dialogProps, updateDialogProps, children }) {
|
|
24
|
+
export default function LocationzationClientProvider({ language, messages, initialAuthUser = null, skipAuthProvider = false, analyticsConfig, autoAnalyticsEventsConfig, requiresConsent = true, autoWireDialogs = true, dialogProps, updateDialogProps, children }) {
|
|
23
25
|
setLocaleCache(language);
|
|
24
26
|
setMessageForLocaleCache(language, messages);
|
|
25
27
|
installConsoleErrorOverride(config, true);
|
|
@@ -31,10 +33,10 @@ export default function LocationzationClientProvider({ language, messages, initi
|
|
|
31
33
|
// the provider.
|
|
32
34
|
let providedChildren = children;
|
|
33
35
|
if (config.firebaseAuth && !skipAuthProvider) {
|
|
34
|
-
providedChildren =
|
|
36
|
+
providedChildren = _jsxs(AuthUserProvider, { initialUser: initialAuthUser, children: [children, config.firebaseAuth.performance !== false && _jsx(AutoFirebasePerformanceEvents, {})] });
|
|
35
37
|
}
|
|
36
38
|
if (config.cookieConsent) {
|
|
37
|
-
providedChildren = _jsxs(CookieConsentProvider, { requiresConsent: requiresConsent, children: [providedChildren, analyticsConfig && _jsx(CookieConsentAnalytics, { config: analyticsConfig }), autoWireDialogs && _jsx(CookieConsentDialog, { ...dialogProps }), autoWireDialogs && _jsx(PrivacyPolicyUpdateDialog, { ...updateDialogProps })] });
|
|
39
|
+
providedChildren = _jsxs(CookieConsentProvider, { requiresConsent: requiresConsent, children: [providedChildren, analyticsConfig && _jsx(CookieConsentAnalytics, { config: analyticsConfig }), analyticsConfig && (analyticsConfig.googleAnalyticsId || analyticsConfig.googleAdsId) && _jsx(AutoAnalyticsEvents, { config: autoAnalyticsEventsConfig }), autoWireDialogs && _jsx(CookieConsentDialog, { ...dialogProps }), autoWireDialogs && _jsx(PrivacyPolicyUpdateDialog, { ...updateDialogProps })] });
|
|
38
40
|
}
|
|
39
41
|
const contextValue = useMemo(() => ({ language, messages }), [language, messages]);
|
|
40
42
|
return _jsx(LocaleContext.Provider, { value: contextValue, children: providedChildren });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { TranslationObject } from "../../types/types";
|
|
2
|
-
import type { CookieConsentAnalyticsConfig } from "../../types/types";
|
|
2
|
+
import type { CookieConsentAnalyticsConfig, AutoAnalyticsEventsConfig } from "../../types/types";
|
|
3
3
|
import type { CookieConsentDialogProps } from "../../cookie_consent/client/components/cookie_consent_dialog";
|
|
4
4
|
import type { PrivacyPolicyUpdateDialogProps } from "../../cookie_consent/client/components/privacy_policy_update_dialog";
|
|
5
5
|
interface LocaleContextType {
|
|
@@ -7,11 +7,13 @@ interface LocaleContextType {
|
|
|
7
7
|
messages: TranslationObject;
|
|
8
8
|
}
|
|
9
9
|
export declare const LocaleContext: import("react").Context<LocaleContextType | undefined>;
|
|
10
|
-
export default function LocationzationClientProvider({ language, messages, analyticsConfig, requiresConsent, autoWireDialogs, dialogProps, updateDialogProps, children }: {
|
|
10
|
+
export default function LocationzationClientProvider({ language, messages, analyticsConfig, autoAnalyticsEventsConfig, requiresConsent, autoWireDialogs, dialogProps, updateDialogProps, children }: {
|
|
11
11
|
language: string;
|
|
12
12
|
messages: TranslationObject;
|
|
13
13
|
/** Resolved server-side from `cookieConsent.analytics`/`getAnalytics` when `autoWireAnalytics` isn't `false`. */
|
|
14
14
|
analyticsConfig?: CookieConsentAnalyticsConfig;
|
|
15
|
+
/** From `cookieConsent.autoAnalyticsEvents` — forwarded as-is to the auto-wired `AutoAnalyticsEvents`. */
|
|
16
|
+
autoAnalyticsEventsConfig?: AutoAnalyticsEventsConfig;
|
|
15
17
|
/**
|
|
16
18
|
* Resolved server-side from `cookieConsent.getCountryCode`/`gdprCountries`.
|
|
17
19
|
* `false` means the visitor's country doesn't require the consent
|
|
@@ -17,16 +17,17 @@ export const LocaleContext = createContext(undefined);
|
|
|
17
17
|
// for the full explanation.
|
|
18
18
|
const CookieConsentProvider = dynamic(() => import("../../cookie_consent/client/cookie_consent_provider"));
|
|
19
19
|
const CookieConsentAnalytics = dynamic(() => import("../../cookie_consent/client/components/cookie_consent_analytics"));
|
|
20
|
+
const AutoAnalyticsEvents = dynamic(() => import("../../cookie_consent/client/components/auto_analytics_events"));
|
|
20
21
|
const CookieConsentDialog = dynamic(() => import("../../cookie_consent/client/components/cookie_consent_dialog"));
|
|
21
22
|
const PrivacyPolicyUpdateDialog = dynamic(() => import("../../cookie_consent/client/components/privacy_policy_update_dialog"));
|
|
22
|
-
export default function LocationzationClientProvider({ language, messages, analyticsConfig, requiresConsent = true, autoWireDialogs = true, dialogProps, updateDialogProps, children }) {
|
|
23
|
+
export default function LocationzationClientProvider({ language, messages, analyticsConfig, autoAnalyticsEventsConfig, requiresConsent = true, autoWireDialogs = true, dialogProps, updateDialogProps, children }) {
|
|
23
24
|
setLocaleCache(language);
|
|
24
25
|
setMessageForLocaleCache(language, messages);
|
|
25
26
|
installConsoleErrorOverride(config, true);
|
|
26
27
|
installGlobalErrorOverride(config);
|
|
27
28
|
let providedChildren = children;
|
|
28
29
|
if (config.cookieConsent) {
|
|
29
|
-
providedChildren = _jsxs(CookieConsentProvider, { requiresConsent: requiresConsent, children: [providedChildren, analyticsConfig && _jsx(CookieConsentAnalytics, { config: analyticsConfig }), autoWireDialogs && _jsx(CookieConsentDialog, { ...dialogProps }), autoWireDialogs && _jsx(PrivacyPolicyUpdateDialog, { ...updateDialogProps })] });
|
|
30
|
+
providedChildren = _jsxs(CookieConsentProvider, { requiresConsent: requiresConsent, children: [providedChildren, analyticsConfig && _jsx(CookieConsentAnalytics, { config: analyticsConfig }), analyticsConfig && (analyticsConfig.googleAnalyticsId || analyticsConfig.googleAdsId) && _jsx(AutoAnalyticsEvents, { config: autoAnalyticsEventsConfig }), autoWireDialogs && _jsx(CookieConsentDialog, { ...dialogProps }), autoWireDialogs && _jsx(PrivacyPolicyUpdateDialog, { ...updateDialogProps })] });
|
|
30
31
|
}
|
|
31
32
|
const contextValue = useMemo(() => ({ language, messages }), [language, messages]);
|
|
32
33
|
return _jsx(LocaleContext.Provider, { value: contextValue, children: providedChildren });
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AutoAnalyticsEventsConfig } from '../../../types/types';
|
|
2
|
+
/**
|
|
3
|
+
* Auto-rendered alongside `CookieConsentAnalytics` when Google Analytics/Ads
|
|
4
|
+
* is configured — sends `screen_view` on route change and one
|
|
5
|
+
* `gtag('event', ...)` per Web Vitals metric. Gated on consent the same way
|
|
6
|
+
* as the other analytics scripts (no-ops until `gtag` exists on `window`).
|
|
7
|
+
* See `CookieConsentRoutingConfig.autoAnalyticsEvents` to disable specific
|
|
8
|
+
* events or customize `screen_name`.
|
|
9
|
+
*/
|
|
10
|
+
export default function AutoAnalyticsEvents({ config }: {
|
|
11
|
+
config?: AutoAnalyticsEventsConfig;
|
|
12
|
+
}): React.ReactElement | null;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { usePathname } from 'next/navigation';
|
|
3
|
+
import { useReportWebVitals } from 'next/web-vitals';
|
|
4
|
+
import { useEffect } from 'react';
|
|
5
|
+
import useCookieConsent from '../use_cookie_consent';
|
|
6
|
+
const metricEventNames = {
|
|
7
|
+
CLS: 'web_cls',
|
|
8
|
+
FCP: 'web_fcp',
|
|
9
|
+
FID: 'web_fid',
|
|
10
|
+
LCP: 'web_lcp',
|
|
11
|
+
TTFB: 'web_ttfb',
|
|
12
|
+
INP: 'web_inp',
|
|
13
|
+
};
|
|
14
|
+
const formatMetricValue = (name, value) => Math.round(name === 'CLS' ? value * 1000 : value);
|
|
15
|
+
function gtagEvent(name, params) {
|
|
16
|
+
const w = window;
|
|
17
|
+
if (typeof w.gtag !== 'function')
|
|
18
|
+
return;
|
|
19
|
+
w.gtag('event', name, params);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Auto-rendered alongside `CookieConsentAnalytics` when Google Analytics/Ads
|
|
23
|
+
* is configured — sends `screen_view` on route change and one
|
|
24
|
+
* `gtag('event', ...)` per Web Vitals metric. Gated on consent the same way
|
|
25
|
+
* as the other analytics scripts (no-ops until `gtag` exists on `window`).
|
|
26
|
+
* See `CookieConsentRoutingConfig.autoAnalyticsEvents` to disable specific
|
|
27
|
+
* events or customize `screen_name`.
|
|
28
|
+
*/
|
|
29
|
+
export default function AutoAnalyticsEvents({ config }) {
|
|
30
|
+
const { consent, requiresConsent } = useCookieConsent();
|
|
31
|
+
const granted = consent === true || !requiresConsent;
|
|
32
|
+
const path = usePathname();
|
|
33
|
+
const screenName = config?.getScreenName ? config.getScreenName(path) : path;
|
|
34
|
+
const enabledEvents = config?.events;
|
|
35
|
+
const isEnabled = (event) => !enabledEvents || enabledEvents.includes(event);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (!granted)
|
|
38
|
+
return;
|
|
39
|
+
if (!isEnabled('screen_view'))
|
|
40
|
+
return;
|
|
41
|
+
gtagEvent('screen_view', { screen_name: screenName, page_path: path });
|
|
42
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
43
|
+
}, [path, screenName, granted]);
|
|
44
|
+
useReportWebVitals((metric) => {
|
|
45
|
+
if (!granted)
|
|
46
|
+
return;
|
|
47
|
+
const eventName = metricEventNames[metric.name];
|
|
48
|
+
if (!isEnabled(eventName))
|
|
49
|
+
return;
|
|
50
|
+
gtagEvent(eventName, {
|
|
51
|
+
value: formatMetricValue(metric.name, metric.value),
|
|
52
|
+
metric_rating: metric.rating,
|
|
53
|
+
screen_name: screenName,
|
|
54
|
+
page_path: path,
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AuthActionMessages, AuthFormState } from '../types';
|
|
1
|
+
import type { AuthActionCodeSettings, AuthActionMessages, AuthFormState } from '../types';
|
|
2
2
|
/**
|
|
3
3
|
* Builds a login server action for React's `useActionState` form hook.
|
|
4
4
|
* The returned function has the `(prevState, formData) => Promise<AuthFormState>`
|
|
@@ -37,10 +37,10 @@ export declare function createSignUpAction(locale: string, messages: AuthActionM
|
|
|
37
37
|
* `formData` must contain an `email` field.
|
|
38
38
|
*
|
|
39
39
|
* @param locale Used to localize the returned error message.
|
|
40
|
-
* @param
|
|
40
|
+
* @param actionCodeSettings Optional settings for action email (continue URL, etc.).
|
|
41
41
|
* @returns A form action: `{ success: true }` on success, `{ error }` on failure.
|
|
42
42
|
* @example
|
|
43
|
-
* const [state, action] = useActionState(createForgotPasswordAction(locale
|
|
43
|
+
* const [state, action] = useActionState(createForgotPasswordAction(locale), {});
|
|
44
44
|
* <form action={action}>...</form>
|
|
45
45
|
*/
|
|
46
|
-
export declare function createForgotPasswordAction(locale: string,
|
|
46
|
+
export declare function createForgotPasswordAction(locale: string, actionCodeSettings?: AuthActionCodeSettings): (_prevState: AuthFormState, formData: FormData) => Promise<AuthFormState>;
|
|
@@ -79,20 +79,20 @@ export function createSignUpAction(locale, messages) {
|
|
|
79
79
|
* `formData` must contain an `email` field.
|
|
80
80
|
*
|
|
81
81
|
* @param locale Used to localize the returned error message.
|
|
82
|
-
* @param
|
|
82
|
+
* @param actionCodeSettings Optional settings for action email (continue URL, etc.).
|
|
83
83
|
* @returns A form action: `{ success: true }` on success, `{ error }` on failure.
|
|
84
84
|
* @example
|
|
85
|
-
* const [state, action] = useActionState(createForgotPasswordAction(locale
|
|
85
|
+
* const [state, action] = useActionState(createForgotPasswordAction(locale), {});
|
|
86
86
|
* <form action={action}>...</form>
|
|
87
87
|
*/
|
|
88
|
-
export function createForgotPasswordAction(locale,
|
|
88
|
+
export function createForgotPasswordAction(locale, actionCodeSettings) {
|
|
89
89
|
return async function forgotPasswordAction(_prevState, formData) {
|
|
90
90
|
requireFirebaseAuthConfig(config.firebaseAuth);
|
|
91
91
|
const { auth } = await getFirebaseAuthClient();
|
|
92
92
|
const { sendPasswordResetEmail } = await getFirebaseAuthModule();
|
|
93
93
|
const email = (formData.get('email')?.toString() ?? '').trim();
|
|
94
94
|
try {
|
|
95
|
-
await sendPasswordResetEmail(auth, email);
|
|
95
|
+
await sendPasswordResetEmail(auth, email, actionCodeSettings);
|
|
96
96
|
return { success: true };
|
|
97
97
|
}
|
|
98
98
|
catch (e) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AuthUser, SerializedAuthUser } from '../types';
|
|
1
|
+
import type { AuthActionCodeSettings, AuthUser, SerializedAuthUser } from '../types';
|
|
2
2
|
export interface AuthUserContextType {
|
|
3
3
|
/** Current Firebase user, or `null` if signed out (or not yet resolved while `loading`). */
|
|
4
4
|
user: AuthUser | null;
|
|
@@ -7,7 +7,7 @@ export interface AuthUserContextType {
|
|
|
7
7
|
/** Force-refreshes the current user's ID token/claims and re-syncs the session cookie. */
|
|
8
8
|
reloadUser: () => Promise<void>;
|
|
9
9
|
/** Sends a verification email to the currently signed-in user. */
|
|
10
|
-
sendVerificationEmail: () => Promise<void>;
|
|
10
|
+
sendVerificationEmail: (actionCodeSettings?: AuthActionCodeSettings) => Promise<void>;
|
|
11
11
|
/** Signs out, clears the session cookie, and redirects to `firebaseAuth.redirectAuthPath`. */
|
|
12
12
|
logout: () => Promise<void>;
|
|
13
13
|
}
|
|
@@ -311,13 +311,13 @@ export default function AuthUserProvider({ initialUser = null, children }) {
|
|
|
311
311
|
console.error('AuthUserProvider: reloadUser failed', e);
|
|
312
312
|
}
|
|
313
313
|
}, []);
|
|
314
|
-
const sendVerificationEmail = useCallback(async () => {
|
|
314
|
+
const sendVerificationEmail = useCallback(async (actionCodeSettings) => {
|
|
315
315
|
const { auth } = await getFirebaseAuthClient();
|
|
316
316
|
const user = auth.currentUser;
|
|
317
317
|
if (!user)
|
|
318
318
|
return;
|
|
319
319
|
const { sendEmailVerification } = await getFirebaseAuthModule();
|
|
320
|
-
await sendEmailVerification(user);
|
|
320
|
+
await sendEmailVerification(user, actionCodeSettings);
|
|
321
321
|
}, []);
|
|
322
322
|
const logout = useCallback(async () => {
|
|
323
323
|
try {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-rendered alongside `FirebaseAuthClientProvider` when
|
|
3
|
+
* `firebaseAuth.performance` isn't `false` — records Firebase Performance
|
|
4
|
+
* custom traces for Web Vitals metrics (`web_cls`, `web_fcp`, `web_fid`,
|
|
5
|
+
* `web_lcp`, `web_ttfb`, `web_inp`) and SPA route changes (`route_change`),
|
|
6
|
+
* alongside Firebase Performance's own automatic page-load/network traces.
|
|
7
|
+
* No-ops if `firebaseAuth.performance` is disabled (`getFirebasePerformanceSync`
|
|
8
|
+
* never resolves an instance).
|
|
9
|
+
*/
|
|
10
|
+
export default function AutoFirebasePerformanceEvents(): null;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { usePathname } from 'next/navigation';
|
|
3
|
+
import { useReportWebVitals } from 'next/web-vitals';
|
|
4
|
+
import { useEffect, useRef } from 'react';
|
|
5
|
+
import reportError from '../../../error_handling/report_error';
|
|
6
|
+
import { getFirebaseAuthClient, getFirebasePerformanceSync } from '../firebase_client';
|
|
7
|
+
/**
|
|
8
|
+
* Records a Firebase Performance custom trace with a real duration, so
|
|
9
|
+
* Firebase's own duration histograms/percentiles are meaningful. No-ops if
|
|
10
|
+
* `firebaseAuth.performance` is disabled (`getFirebasePerformanceSync` never
|
|
11
|
+
* resolves an instance).
|
|
12
|
+
*/
|
|
13
|
+
async function recordFirebaseTrace(name, durationMs, attributes, metrics) {
|
|
14
|
+
try {
|
|
15
|
+
await getFirebaseAuthClient();
|
|
16
|
+
const performance = getFirebasePerformanceSync();
|
|
17
|
+
if (!performance)
|
|
18
|
+
return;
|
|
19
|
+
const { trace } = await import('firebase/performance');
|
|
20
|
+
const duration = Math.max(Math.round(durationMs), 1);
|
|
21
|
+
trace(performance, name).record(Date.now() - duration, duration, { attributes, metrics });
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
void reportError(undefined, {
|
|
25
|
+
error,
|
|
26
|
+
classOrMethodName: 'recordFirebaseTrace',
|
|
27
|
+
isClient: true,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const SLOW_RESOURCE_THRESHOLD_MS = 1000;
|
|
32
|
+
/**
|
|
33
|
+
* Auto-rendered alongside `FirebaseAuthClientProvider` when
|
|
34
|
+
* `firebaseAuth.performance` isn't `false` — records Firebase Performance
|
|
35
|
+
* custom traces for Web Vitals metrics (`web_cls`, `web_fcp`, `web_fid`,
|
|
36
|
+
* `web_lcp`, `web_ttfb`, `web_inp`) and SPA route changes (`route_change`),
|
|
37
|
+
* alongside Firebase Performance's own automatic page-load/network traces.
|
|
38
|
+
* No-ops if `firebaseAuth.performance` is disabled (`getFirebasePerformanceSync`
|
|
39
|
+
* never resolves an instance).
|
|
40
|
+
*/
|
|
41
|
+
export default function AutoFirebasePerformanceEvents() {
|
|
42
|
+
useReportWebVitals((metric) => {
|
|
43
|
+
const value = Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value);
|
|
44
|
+
void recordFirebaseTrace(`web_${metric.name.toLowerCase()}`, value, { rating: metric.rating });
|
|
45
|
+
});
|
|
46
|
+
const path = usePathname();
|
|
47
|
+
const isFirstRoute = useRef(true);
|
|
48
|
+
const lastRouteChangeRef = useRef(Date.now());
|
|
49
|
+
const longTaskStats = useRef({ count: 0, totalDuration: 0 });
|
|
50
|
+
// Mount-only: registers a PerformanceObserver for long tasks (main-thread
|
|
51
|
+
// blocks >= 50ms). Not all browsers support the 'longtask' entry type, so
|
|
52
|
+
// this is a best-effort signal. The final route's tail of accumulated
|
|
53
|
+
// long tasks between the last route change and unmount is intentionally
|
|
54
|
+
// dropped rather than flushed here — this is a monitoring signal, not a
|
|
55
|
+
// billing metric, and an occasional dropped tail sample is acceptable.
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (typeof PerformanceObserver === 'undefined' || !PerformanceObserver.supportedEntryTypes?.includes('longtask'))
|
|
58
|
+
return;
|
|
59
|
+
const observer = new PerformanceObserver((list) => {
|
|
60
|
+
for (const entry of list.getEntries()) {
|
|
61
|
+
longTaskStats.current.count += 1;
|
|
62
|
+
longTaskStats.current.totalDuration += entry.duration;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
observer.observe({ type: 'longtask', buffered: true });
|
|
66
|
+
return () => observer.disconnect();
|
|
67
|
+
}, []);
|
|
68
|
+
// Mount-only: registers a PerformanceObserver for slow non-fetch/XHR
|
|
69
|
+
// resources (scripts, images, stylesheets, fonts, etc). Firebase
|
|
70
|
+
// Performance's own automatic network monitoring already instruments
|
|
71
|
+
// fetch/XHR, so those are skipped here to avoid duplicate signals.
|
|
72
|
+
// `buffered: true` also catches resources that loaded before this
|
|
73
|
+
// component mounted (it only mounts after `AuthUserProvider`).
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (typeof PerformanceObserver === 'undefined' || !PerformanceObserver.supportedEntryTypes?.includes('resource'))
|
|
76
|
+
return;
|
|
77
|
+
const observer = new PerformanceObserver((list) => {
|
|
78
|
+
for (const entry of list.getEntries()) {
|
|
79
|
+
if (entry.initiatorType === 'fetch' || entry.initiatorType === 'xmlhttprequest')
|
|
80
|
+
continue;
|
|
81
|
+
if (entry.duration < SLOW_RESOURCE_THRESHOLD_MS)
|
|
82
|
+
continue;
|
|
83
|
+
void recordFirebaseTrace('slow_resource', entry.duration, {
|
|
84
|
+
initiator_type: entry.initiatorType,
|
|
85
|
+
resource: entry.name.slice(-100),
|
|
86
|
+
}, { transfer_size_bytes: Math.round(entry.transferSize ?? 0) });
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
observer.observe({ type: 'resource', buffered: true });
|
|
90
|
+
return () => observer.disconnect();
|
|
91
|
+
}, []);
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
const now = Date.now();
|
|
94
|
+
const duration = now - lastRouteChangeRef.current;
|
|
95
|
+
lastRouteChangeRef.current = now;
|
|
96
|
+
if (isFirstRoute.current) {
|
|
97
|
+
isFirstRoute.current = false;
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// Approximates navigation duration as time between path-change commits —
|
|
101
|
+
// App Router exposes no public "navigation start" event this package
|
|
102
|
+
// can hook into, so this is not a precise navigation timing.
|
|
103
|
+
void recordFirebaseTrace('route_change', duration, { path: path.slice(-100) });
|
|
104
|
+
const { count, totalDuration } = longTaskStats.current;
|
|
105
|
+
longTaskStats.current = { count: 0, totalDuration: 0 };
|
|
106
|
+
if (count > 0) {
|
|
107
|
+
void recordFirebaseTrace('route_long_tasks', totalDuration, { path: path.slice(-100) }, { long_task_count: count });
|
|
108
|
+
}
|
|
109
|
+
}, [path]);
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { FirebaseApp } from 'firebase/app';
|
|
2
2
|
import type { Auth } from 'firebase/auth';
|
|
3
|
+
import type { FirebasePerformance } from 'firebase/performance';
|
|
3
4
|
/**
|
|
4
5
|
* Current App Check token, or `undefined` if `appCheck` isn't configured or
|
|
5
6
|
* hasn't initialized yet. Forces a refresh only when the cached token is
|
|
@@ -24,5 +25,7 @@ export declare function getFirebaseAuthClientSync(): {
|
|
|
24
25
|
app: FirebaseApp;
|
|
25
26
|
auth: Auth;
|
|
26
27
|
} | undefined;
|
|
28
|
+
/** Synchronous read of the cached `FirebasePerformance` instance, or `undefined` if `performance` isn't enabled or hasn't initialized yet. */
|
|
29
|
+
export declare function getFirebasePerformanceSync(): FirebasePerformance | undefined;
|
|
27
30
|
/** Memoized `import('firebase/auth')` — see {@link getFirebaseAuthClient} for why this is worth caching. */
|
|
28
31
|
export declare function getFirebaseAuthModule(): Promise<typeof import('firebase/auth')>;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import config from '@intl-config';
|
|
3
3
|
import requireFirebaseAuthConfig from '../require_config';
|
|
4
4
|
let cachedAppCheck;
|
|
5
|
+
let cachedPerformance;
|
|
5
6
|
async function initializeFirebaseAppCheck(app, appCheckConfig) {
|
|
6
7
|
const { initializeAppCheck, ReCaptchaV3Provider, ReCaptchaEnterpriseProvider } = await import('firebase/app-check');
|
|
7
8
|
if (appCheckConfig.debugToken) {
|
|
@@ -45,7 +46,12 @@ export async function getFirebaseAuthClient() {
|
|
|
45
46
|
return cached;
|
|
46
47
|
if (!cachedPromise) {
|
|
47
48
|
const fa = config.firebaseAuth;
|
|
48
|
-
|
|
49
|
+
const isPerformanceEnabled = fa.performance !== false && typeof window !== 'undefined';
|
|
50
|
+
cachedPromise = Promise.all([
|
|
51
|
+
import('firebase/app'),
|
|
52
|
+
import('firebase/auth'),
|
|
53
|
+
isPerformanceEnabled ? import('firebase/performance') : Promise.resolve(null),
|
|
54
|
+
]).then(async ([{ getApp, getApps, initializeApp }, { getAuth }, perfModule]) => {
|
|
49
55
|
const firebaseConfig = {
|
|
50
56
|
apiKey: fa.apiKey,
|
|
51
57
|
authDomain: fa.authDomain,
|
|
@@ -59,6 +65,9 @@ export async function getFirebaseAuthClient() {
|
|
|
59
65
|
if (fa.appCheck) {
|
|
60
66
|
cachedAppCheck = await initializeFirebaseAppCheck(app, fa.appCheck);
|
|
61
67
|
}
|
|
68
|
+
if (perfModule) {
|
|
69
|
+
cachedPerformance = perfModule.getPerformance(app);
|
|
70
|
+
}
|
|
62
71
|
const auth = getAuth(app);
|
|
63
72
|
cached = { app, auth };
|
|
64
73
|
return cached;
|
|
@@ -70,6 +79,10 @@ export async function getFirebaseAuthClient() {
|
|
|
70
79
|
export function getFirebaseAuthClientSync() {
|
|
71
80
|
return cached;
|
|
72
81
|
}
|
|
82
|
+
/** Synchronous read of the cached `FirebasePerformance` instance, or `undefined` if `performance` isn't enabled or hasn't initialized yet. */
|
|
83
|
+
export function getFirebasePerformanceSync() {
|
|
84
|
+
return cachedPerformance;
|
|
85
|
+
}
|
|
73
86
|
let cachedAuthModule;
|
|
74
87
|
/** Memoized `import('firebase/auth')` — see {@link getFirebaseAuthClient} for why this is worth caching. */
|
|
75
88
|
export function getFirebaseAuthModule() {
|
|
@@ -5,6 +5,6 @@ export { default as useFirebaseAuthUserServer } from './server/use_auth_user_ser
|
|
|
5
5
|
export { createLoginAction, createSignUpAction, createForgotPasswordAction } from './client/auth_actions';
|
|
6
6
|
export { default as clearFirebaseAuthSession } from './server/clear_session_action';
|
|
7
7
|
export { default as updateFirebaseAuthSession, defaultSessionCookieName as firebaseAuthSessionCookieName } from './middleware/update_session';
|
|
8
|
-
export { getFirebaseAuthClient } from './client/firebase_client';
|
|
8
|
+
export { getFirebaseAuthClient, getFirebasePerformanceSync } from './client/firebase_client';
|
|
9
9
|
export type { SerializedAuthUser, AuthFormState, AuthActionMessages, AuthUser } from './types';
|
|
10
10
|
export type { FirebaseAuthRoutingConfig } from '../types/types';
|
|
@@ -11,4 +11,4 @@ export { default as useFirebaseAuthUserServer } from './server/use_auth_user_ser
|
|
|
11
11
|
export { createLoginAction, createSignUpAction, createForgotPasswordAction } from './client/auth_actions';
|
|
12
12
|
export { default as clearFirebaseAuthSession } from './server/clear_session_action';
|
|
13
13
|
export { default as updateFirebaseAuthSession, defaultSessionCookieName as firebaseAuthSessionCookieName } from './middleware/update_session';
|
|
14
|
-
export { getFirebaseAuthClient } from './client/firebase_client';
|
|
14
|
+
export { getFirebaseAuthClient, getFirebasePerformanceSync } from './client/firebase_client';
|
|
@@ -240,7 +240,37 @@ export default async function updateSession(request, baseResponse, locale, rebui
|
|
|
240
240
|
if (fa.actionLinkRedirectEnabled !== false && isEligibleActionPath) {
|
|
241
241
|
const mode = request.nextUrl.searchParams.get('mode');
|
|
242
242
|
if (mode) {
|
|
243
|
-
|
|
243
|
+
let target = resolveActionModePaths(fa)[mode];
|
|
244
|
+
if (fa.followSameOriginContinueUrl !== false) {
|
|
245
|
+
const continueUrl = request.nextUrl.searchParams.get('continueUrl');
|
|
246
|
+
if (continueUrl) {
|
|
247
|
+
try {
|
|
248
|
+
const parsed = new URL(continueUrl, request.url);
|
|
249
|
+
if (parsed.origin === request.nextUrl.origin) {
|
|
250
|
+
let continuePath = parsed.pathname;
|
|
251
|
+
if (locale !== config.defaultLocale) {
|
|
252
|
+
if (continuePath.startsWith(`/${locale}/`)) {
|
|
253
|
+
continuePath = continuePath.slice(locale.length + 1);
|
|
254
|
+
}
|
|
255
|
+
else if (continuePath === `/${locale}`) {
|
|
256
|
+
continuePath = '/';
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (continuePath !== '/') {
|
|
260
|
+
target = continuePath;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
// External continueUrl: redirect directly to the full external URL
|
|
265
|
+
parsed.search = request.nextUrl.search;
|
|
266
|
+
return buildRedirect(baseResponse, parsed);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
// Invalid continueUrl format — fall back to resolved mode target
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
244
274
|
// Skip when already on the destination — the forward sets the same
|
|
245
275
|
// `?mode=` it matched on, so redirecting again would loop forever.
|
|
246
276
|
if (target && target !== path) {
|
|
@@ -20,3 +20,18 @@ export interface AuthActionMessages {
|
|
|
20
20
|
mismatch?: string;
|
|
21
21
|
}
|
|
22
22
|
export type AuthUser = User | SerializedAuthUser;
|
|
23
|
+
/** Settings for configuring action code emails (password reset, email verification). */
|
|
24
|
+
export interface AuthActionCodeSettings {
|
|
25
|
+
url: string;
|
|
26
|
+
handleCodeInApp?: boolean;
|
|
27
|
+
iOS?: {
|
|
28
|
+
bundleId: string;
|
|
29
|
+
};
|
|
30
|
+
android?: {
|
|
31
|
+
packageName: string;
|
|
32
|
+
installApp?: boolean;
|
|
33
|
+
minimumVersion?: string;
|
|
34
|
+
};
|
|
35
|
+
dynamicLinkDomain?: string;
|
|
36
|
+
linkDomain?: string;
|
|
37
|
+
}
|
|
@@ -161,5 +161,5 @@ export default async function LocationzationProvider({ language, messages, stati
|
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
|
-
return _jsx(LocationzationClientProvider, { language: language, messages: messagesValue, initialAuthUser: initialAuthUser, skipAuthProvider: !autoWireClientProvider, analyticsConfig: analyticsConfig, requiresConsent: requiresConsent, autoWireDialogs: config.cookieConsent?.autoWireDialogs !== false, dialogProps: config.cookieConsent?.dialogProps, updateDialogProps: config.cookieConsent?.updateDialogProps, children: children });
|
|
164
|
+
return _jsx(LocationzationClientProvider, { language: language, messages: messagesValue, initialAuthUser: initialAuthUser, skipAuthProvider: !autoWireClientProvider, analyticsConfig: analyticsConfig, autoAnalyticsEventsConfig: config.cookieConsent?.autoAnalyticsEvents, requiresConsent: requiresConsent, autoWireDialogs: config.cookieConsent?.autoWireDialogs !== false, dialogProps: config.cookieConsent?.dialogProps, updateDialogProps: config.cookieConsent?.updateDialogProps, children: children });
|
|
165
165
|
}
|
|
@@ -104,5 +104,5 @@ export default async function LocationzationProvider({ language, messages, child
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
|
-
return _jsx(LocationzationClientProvider, { language: language, messages: messagesValue, analyticsConfig: analyticsConfig, requiresConsent: requiresConsent, autoWireDialogs: config.cookieConsent?.autoWireDialogs !== false, dialogProps: config.cookieConsent?.dialogProps, updateDialogProps: config.cookieConsent?.updateDialogProps, children: children });
|
|
107
|
+
return _jsx(LocationzationClientProvider, { language: language, messages: messagesValue, analyticsConfig: analyticsConfig, autoAnalyticsEventsConfig: config.cookieConsent?.autoAnalyticsEvents, requiresConsent: requiresConsent, autoWireDialogs: config.cookieConsent?.autoWireDialogs !== false, dialogProps: config.cookieConsent?.dialogProps, updateDialogProps: config.cookieConsent?.updateDialogProps, children: children });
|
|
108
108
|
}
|
|
@@ -308,6 +308,16 @@ export interface CookieConsentRoutingConfig {
|
|
|
308
308
|
* `undefined` in the returned object disables that provider's script.
|
|
309
309
|
*/
|
|
310
310
|
getAnalytics?: () => CookieConsentAnalyticsConfig | Promise<CookieConsentAnalyticsConfig>;
|
|
311
|
+
/**
|
|
312
|
+
* Configures automatic `screen_view` (on route change) and Web Vitals
|
|
313
|
+
* (`web_cls`/`web_fcp`/`web_fid`/`web_lcp`/`web_ttfb`/`web_inp`)
|
|
314
|
+
* `gtag('event', ...)` tracking. Auto-rendered alongside
|
|
315
|
+
* `CookieConsentAnalytics` whenever Google Analytics/Ads is configured
|
|
316
|
+
* and `autoWireAnalytics` isn't `false` — no manual wiring needed. Omit
|
|
317
|
+
* to keep all events enabled with default screen names (raw pathname);
|
|
318
|
+
* set `events` to a narrower array to disable specific ones.
|
|
319
|
+
*/
|
|
320
|
+
autoAnalyticsEvents?: AutoAnalyticsEventsConfig;
|
|
311
321
|
/**
|
|
312
322
|
* Resolves the visitor's country code directly (ISO 3166-1 alpha-2,
|
|
313
323
|
* e.g. `"DE"`) — the simplest option when you already have it from
|
|
@@ -389,6 +399,20 @@ export interface CookieConsentGetCloudflareContext {
|
|
|
389
399
|
async: false;
|
|
390
400
|
}): CookieConsentCloudflareContext | null;
|
|
391
401
|
}
|
|
402
|
+
/** Auto-tracked event names — `screen_view` on route change, plus one per Web Vitals metric. All enabled by default. */
|
|
403
|
+
export type AutoAnalyticsEventName = 'screen_view' | 'web_cls' | 'web_fcp' | 'web_fid' | 'web_lcp' | 'web_ttfb' | 'web_inp';
|
|
404
|
+
export interface AutoAnalyticsEventsConfig {
|
|
405
|
+
/**
|
|
406
|
+
* Which auto-tracked events to send via `gtag('event', ...)`. Defaults
|
|
407
|
+
* to all of {@link AutoAnalyticsEventName}. Pass a narrower array to
|
|
408
|
+
* disable specific events (e.g. omit `'web_cls'`) while keeping the
|
|
409
|
+
* rest. Only takes effect once `gtag` is available (i.e. Google
|
|
410
|
+
* Analytics/Ads is configured and consent is granted).
|
|
411
|
+
*/
|
|
412
|
+
events?: readonly AutoAnalyticsEventName[];
|
|
413
|
+
/** Maps a locale-stripped pathname to a human-readable screen name for `screen_view`'s `screen_name` param. Defaults to the raw pathname. */
|
|
414
|
+
getScreenName?: (path: string) => string;
|
|
415
|
+
}
|
|
392
416
|
export interface CookieConsentAnalyticsConfig {
|
|
393
417
|
/** Cloudflare Web Analytics beacon token, e.g. `'{"token": "..."}'` (the raw `data-cf-beacon` attribute value). */
|
|
394
418
|
cloudflareBeaconToken?: string;
|
|
@@ -440,6 +464,12 @@ export interface FirebaseAuthRoutingConfig {
|
|
|
440
464
|
appId: string;
|
|
441
465
|
/** Firebase Analytics measurement ID. */
|
|
442
466
|
measurementId?: string;
|
|
467
|
+
/**
|
|
468
|
+
* Whether Firebase Performance Monitoring is initialized on the client.
|
|
469
|
+
* Enabled (`true`) by default when `firebaseAuth` is configured.
|
|
470
|
+
* Set `false` to disable Performance Monitoring initialization.
|
|
471
|
+
*/
|
|
472
|
+
performance?: boolean;
|
|
443
473
|
/**
|
|
444
474
|
* Enables Firebase App Check on the client. Omit to leave App Check
|
|
445
475
|
* uninitialized — required if App Check enforcement is turned on for
|
|
@@ -485,6 +515,12 @@ export interface FirebaseAuthRoutingConfig {
|
|
|
485
515
|
* Defaults to `true`.
|
|
486
516
|
*/
|
|
487
517
|
actionLinkRedirectEnabled?: boolean;
|
|
518
|
+
/**
|
|
519
|
+
* When `true` (the default), if an emailed-action-link carries a `continueUrl` query parameter
|
|
520
|
+
* matching the request's origin (same domain/app), the middleware will redirect to `continueUrl`'s
|
|
521
|
+
* path instead of the static mode path (e.g. `resetPasswordPath`). Set `false` to disable.
|
|
522
|
+
*/
|
|
523
|
+
followSameOriginContinueUrl?: boolean;
|
|
488
524
|
/**
|
|
489
525
|
* Restricts the emailed-action-link forward (see {@link resetPasswordPath})
|
|
490
526
|
* to this exact static path — set this to whatever path your Firebase
|
package/package.json
CHANGED