cloudflare-next-intl 0.10.16 → 0.10.18

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 CHANGED
@@ -409,7 +409,7 @@ export default async function RootLayout({ children, params }) {
409
409
  return (
410
410
  <html lang={locale}>
411
411
  <head>
412
- <IntlHelperScript />
412
+ <IntlHelperScript locale={locale} />
413
413
  </head>
414
414
  <body>
415
415
  <IntlProvider language={locale}>{children}</IntlProvider>
@@ -512,10 +512,11 @@ Features include:
512
512
  - `whiteListPaths`: Array of paths exempt from auth redirects (matches exact path or path-segment prefix, e.g. `/bonds` matches `/bonds/some-slug`).
513
513
  - `protectedPaths`: `(path) => boolean` gate that inverts `whiteListPaths` — when set, every path passes through unless `protectedPaths` (or `isAuthPath`) returns true, so unrecognised URLs reach the app's own routing instead of being redirected to `redirectAuthPath`. Preferred over `whiteListPaths` for apps where most pages are public: both the middleware and `AuthUserProvider` (client) derive their whitelist from it directly, so you don't maintain two separate path lists that can drift out of sync. `whiteListPaths` is only consulted when `protectedPaths` isn't set.
514
514
  - `actionLinkPath`: Pinned route for handling Firebase action links (e.g. `/auth/action`), prioritized during cross-origin action link redirects.
515
+ - `skipPopupRedirectResolver`: Skips the Auth SDK's default `browserPopupRedirectResolver`, which proactively opens the `<authDomain>/__/auth/iframe.js` relay iframe on every page load on mobile/Safari/iOS user agents. Safe even with Google/Apple/social sign-in — pass `browserPopupRedirectResolver` as the last argument to `signInWithPopup`/`signInWithRedirect`/`getRedirectResult`/`linkWithPopup`/`reauthenticateWithPopup` at the call site instead, so the iframe only loads on the click. Also skips the Auth emulator auto-connect and the experimental `authTokenSyncURL` cookie exchange that `getAuth` does — see the JSDoc on this option before enabling it against an emulator.
515
516
  - `createForgotPasswordAction(locale, actionCodeSettings?)`: Accepts optional Firebase `AuthActionCodeSettings` (e.g. `url` redirect link).
516
517
  - `sendVerificationEmail(actionCodeSettings?)` on `useAuthUser()`: Custom action email settings when resending email verification.
517
518
  - `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). If `continueUrl` points to home root (`/`), it resolves to `actionLinkPath` (if set) or the mode target path (e.g. `/reset-password`).
518
- - `appCheck`: Firebase App Check integration supporting reCAPTCHA Enterprise (`recaptchaEnterpriseSiteKey`) and reCAPTCHA v3 (`recaptchaV3SiteKey`). For reCAPTCHA v3, defaults to a `CustomProvider` using `IntlHelperScript`'s explicit script tag to avoid iframe/webworker CDN integrity issues in private windows (`useExplicitRecaptchaScript: false` to opt out). Supports server-side token minting via service accounts.
519
+ - `appCheck`: Firebase App Check integration supporting reCAPTCHA Enterprise (`recaptchaEnterpriseSiteKey`) and reCAPTCHA v3 (`recaptchaV3SiteKey`). For reCAPTCHA v3, defaults to a `CustomProvider` using an explicit `render=explicit` script tag to avoid iframe/webworker CDN integrity issues in private windows (`useExplicitRecaptchaScript: false` to opt out). Both the script and App Check's own initialization are always deferred to first actual need (the first `getAppCheckToken()`/token mint) rather than run as part of every `getFirebaseAuthClient()` call, so a page that never mints a token never pays reCAPTCHA's ~345KB cost. **Note:** because of that deferral, a request made through `getFirebaseAuthClient()`'s `auth` before anything has called `getAppCheckToken()` (e.g. a sign-in call) goes out without an `X-Firebase-AppCheck` header — only a concern if App Check enforcement is on for Auth/Firestore/Functions. Supports server-side token minting via service accounts.
519
520
  - `resolveOptionalAuthUser()` (`cloudflare-next-intl/resolveOptionalAuthUser`): best-effort variant of `getAuthUser()` for callers that want to *attach* the current user when one happens to be known — error/telemetry reporting, analytics, logging — without failing or changing behavior when no request/session context is available. Swallows every failure and resolves `{ user: null }` instead of throwing. Use `getAuthUser()` (not this) whenever your own output actually depends on who's signed in — `checkDynamicPages` (below) treats `getAuthUser()`/`useAuthUser()`/`withUserDb()` as page-blocking dynamic-API signals but does not follow into this helper, so it's also the way to read "whoever's signed in, if anyone" from a page or a module it imports without forcing that page dynamic.
520
521
  - `resolveErrorReportingUser(useAuthUser?)` (same subpath): gated form of `resolveOptionalAuthUser()` for an `onError` sink — pass `ErrorHandlingParams.useAuthUser` straight through. Defaults to `false`: when not `true`, resolves `{ user: null }` immediately without calling `getAuthUser()` at all, so a page reached only through your `onError` sink's default path stays static-eligible. A specific `reportError({ ..., useAuthUser: true })` call site opts in per-call when it actually wants the user on that report.
521
522
 
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { setLocaleCache, setMessageForLocaleCache } from "../../general/cache_variables.js";
4
- import { createContext, useMemo, Suspense } from "react";
4
+ import { createContext, useEffect, useMemo, Suspense } from "react";
5
5
  import dynamic from "next/dynamic.js";
6
6
  import useLazyWrappingProvider from "./use_lazy_wrapping_provider.js";
7
7
  import config from "@intl-config";
@@ -14,7 +14,8 @@ const AutoFirebasePerformanceEvents = dynamic(() => import("../../firebase_auth/
14
14
  const loadCookieConsentProvider = () => import("../../cookie_consent/client/cookie_consent_provider.js");
15
15
  const CookieConsentAnalytics = dynamic(() => import("../../cookie_consent/client/components/cookie_consent_analytics.js"));
16
16
  const AutoAnalyticsEvents = dynamic(() => import("../../cookie_consent/client/components/auto_analytics_events.js"));
17
- const CookieConsentDialog = dynamic(() => import("../../cookie_consent/client/components/cookie_consent_dialog.js"));
17
+ const loadCookieConsentDialog = () => import("../../cookie_consent/client/components/cookie_consent_dialog.js");
18
+ const CookieConsentDialog = dynamic(loadCookieConsentDialog);
18
19
  const PrivacyPolicyUpdateDialog = dynamic(() => import("../../cookie_consent/client/components/privacy_policy_update_dialog.js"));
19
20
  export default function LocationzationClientProvider({ language, messages, initialAuthUser = null, skipAuthProvider = false, analyticsConfig, autoAnalyticsEventsConfig, requiresConsent = true, autoWireDialogs = true, dialogProps, updateDialogProps, children }) {
20
21
  setLocaleCache(language);
@@ -23,6 +24,11 @@ export default function LocationzationClientProvider({ language, messages, initi
23
24
  installGlobalErrorOverride(config);
24
25
  const { Provider: AuthUserProvider } = useLazyWrappingProvider(loadAuthUserProvider);
25
26
  const { Provider: CookieConsentProvider, isReady: cookieConsentReady } = useLazyWrappingProvider(loadCookieConsentProvider);
27
+ useEffect(() => {
28
+ if (!config.cookieConsent || !autoWireDialogs)
29
+ return;
30
+ loadCookieConsentDialog().catch(() => undefined);
31
+ }, [autoWireDialogs]);
26
32
  let providedChildren = children;
27
33
  if (config.firebaseAuth && !skipAuthProvider) {
28
34
  providedChildren = _jsx(AuthUserPendingProvider, { initialUser: initialAuthUser, children: _jsxs(AuthUserProvider, { initialUser: initialAuthUser, children: [children, config.firebaseAuth.performance !== false && (_jsx(Suspense, { fallback: null, children: _jsx(AutoFirebasePerformanceEvents, {}) }))] }) });
@@ -10,16 +10,34 @@ function getClarityModule() {
10
10
  }
11
11
  export default function ClarityScript({ projectId }) {
12
12
  useEffect(() => {
13
- getClarityModule()
14
- .then(({ default: Clarity }) => {
15
- Clarity.init(projectId);
16
- Clarity.consent();
17
- })
18
- .catch((error) => void reportError(undefined, {
19
- error,
20
- classOrMethodName: 'ClarityScript',
21
- isClient: true,
22
- }));
13
+ let handle;
14
+ let timeout;
15
+ const init = () => {
16
+ getClarityModule()
17
+ .then(({ default: Clarity }) => {
18
+ Clarity.init(projectId);
19
+ Clarity.consent();
20
+ })
21
+ .catch((error) => void reportError(undefined, {
22
+ error,
23
+ classOrMethodName: 'ClarityScript',
24
+ isClient: true,
25
+ }));
26
+ };
27
+ if (typeof window !== 'undefined' && 'requestIdleCallback' in window) {
28
+ handle = window.requestIdleCallback(init, { timeout: 3000 });
29
+ }
30
+ else {
31
+ timeout = setTimeout(init, 1500);
32
+ }
33
+ return () => {
34
+ if (handle !== undefined && typeof window !== 'undefined' && 'cancelIdleCallback' in window) {
35
+ window.cancelIdleCallback(handle);
36
+ }
37
+ if (timeout !== undefined) {
38
+ clearTimeout(timeout);
39
+ }
40
+ };
23
41
  }, [projectId]);
24
42
  return null;
25
43
  }
@@ -15,6 +15,8 @@ import setCookie from '../../client/functions/set_cookie.js';
15
15
  import getCookie from '../../client/functions/get_cookie.js';
16
16
  import clearSessionAction from '../server/clear_session_action.js';
17
17
  import { AuthUserContext } from './auth_user_context.js';
18
+ const AUTH_SUBSCRIBE_IDLE_TIMEOUT_MS = 3000;
19
+ const AUTH_SUBSCRIBE_FALLBACK_DELAY_MS = 1500;
18
20
  export { AuthUserContext };
19
21
  function writeSessionCookie(sessionCookieName, idToken, maxAge) {
20
22
  setCookie({ name: sessionCookieName, value: idToken, maxAge });
@@ -96,6 +98,7 @@ export default function AuthUserProvider({ initialUser = null, children }) {
96
98
  const signInCallbackFired = useRef(initialUser !== null);
97
99
  const signOutCallbackFired = useRef(initialUser === null);
98
100
  const emailVerifiedRef = useRef(initialUser?.emailVerified ?? false);
101
+ const deferAuthSubscribe = initialUser === null && isWhiteListed && !isAuthPage && typeof window !== 'undefined';
99
102
  useEffect(() => {
100
103
  const { user, loading } = state;
101
104
  if (loading || isWhiteListed)
@@ -114,7 +117,9 @@ export default function AuthUserProvider({ initialUser = null, children }) {
114
117
  useEffect(() => {
115
118
  let unsubscribe;
116
119
  let cancelled = false;
117
- getFirebaseAuthClient().then(async ({ auth }) => {
120
+ let idleHandle;
121
+ let timeoutHandle;
122
+ const subscribe = () => getFirebaseAuthClient().then(async ({ auth }) => {
118
123
  if (cancelled)
119
124
  return;
120
125
  const { onIdTokenChanged } = await getFirebaseAuthModule();
@@ -185,11 +190,28 @@ export default function AuthUserProvider({ initialUser = null, children }) {
185
190
  }
186
191
  });
187
192
  });
193
+ if (deferAuthSubscribe) {
194
+ setState(current => (current.loading ? { ...current, loading: false } : current));
195
+ const idle = window.requestIdleCallback;
196
+ if (idle)
197
+ idleHandle = idle(() => { if (!cancelled)
198
+ subscribe(); }, { timeout: AUTH_SUBSCRIBE_IDLE_TIMEOUT_MS });
199
+ else
200
+ timeoutHandle = setTimeout(() => { if (!cancelled)
201
+ subscribe(); }, AUTH_SUBSCRIBE_FALLBACK_DELAY_MS);
202
+ }
203
+ else {
204
+ subscribe();
205
+ }
188
206
  return () => {
189
207
  cancelled = true;
208
+ if (idleHandle !== undefined)
209
+ window.cancelIdleCallback?.(idleHandle);
210
+ if (timeoutHandle !== undefined)
211
+ clearTimeout(timeoutHandle);
190
212
  unsubscribe?.();
191
213
  };
192
- }, [router, isAuthPage, isWhiteListed, maxAge, sessionCookieName, refreshTokenMaxAge, refreshTokenCookieName, emailVerifiedHintCookieName, appCheckTokenCookieName, appCheckTokenMaxAge, fa]);
214
+ }, [deferAuthSubscribe, router, isAuthPage, isWhiteListed, maxAge, sessionCookieName, refreshTokenMaxAge, refreshTokenCookieName, emailVerifiedHintCookieName, appCheckTokenCookieName, appCheckTokenMaxAge, fa]);
193
215
  const reloadUser = useCallback(async () => {
194
216
  const { auth } = await getFirebaseAuthClient();
195
217
  const user = auth.currentUser;
@@ -5,9 +5,22 @@ let cachedAppCheck;
5
5
  let cachedPerformance;
6
6
  const GRECAPTCHA_LOAD_TIMEOUT_MS = 15000;
7
7
  const GRECAPTCHA_POLL_INTERVAL_MS = 50;
8
+ const RECAPTCHA_SCRIPT_SRC = 'https://www.google.com/recaptcha/api.js?render=explicit';
9
+ let recaptchaScriptFailed = false;
10
+ function ensureRecaptchaScript() {
11
+ if (document.querySelector(`script[src="${RECAPTCHA_SCRIPT_SRC}"]`))
12
+ return;
13
+ const script = document.createElement('script');
14
+ script.src = RECAPTCHA_SCRIPT_SRC;
15
+ script.async = true;
16
+ script.defer = true;
17
+ script.onerror = () => { recaptchaScriptFailed = true; };
18
+ document.head.appendChild(script);
19
+ }
8
20
  function waitForGrecaptcha() {
9
21
  if (window.grecaptcha)
10
22
  return Promise.resolve(window.grecaptcha);
23
+ ensureRecaptchaScript();
11
24
  return new Promise((resolve, reject) => {
12
25
  const startedAt = Date.now();
13
26
  const timer = setInterval(() => {
@@ -15,9 +28,9 @@ function waitForGrecaptcha() {
15
28
  clearInterval(timer);
16
29
  resolve(window.grecaptcha);
17
30
  }
18
- else if (Date.now() - startedAt >= GRECAPTCHA_LOAD_TIMEOUT_MS) {
31
+ else if (recaptchaScriptFailed || Date.now() - startedAt >= GRECAPTCHA_LOAD_TIMEOUT_MS) {
19
32
  clearInterval(timer);
20
- reject(new Error('window.grecaptcha never loaded; ensure the reCAPTCHA <script src="https://www.google.com/recaptcha/api.js?render=explicit"> tag is present'));
33
+ reject(new Error(`window.grecaptcha never loaded; ${RECAPTCHA_SCRIPT_SRC} failed to load or was blocked`));
21
34
  }
22
35
  }, GRECAPTCHA_POLL_INTERVAL_MS);
23
36
  });
@@ -108,7 +121,28 @@ async function initializeFirebaseAppCheck(app, appCheckConfig) {
108
121
  });
109
122
  }
110
123
  const APP_CHECK_TOKEN_TIMEOUT_MS = 10000;
124
+ let appCheckInitPromise;
125
+ async function ensureAppCheck() {
126
+ if (appCheckInitPromise)
127
+ return appCheckInitPromise;
128
+ const appCheckConfig = config.firebaseAuth?.appCheck;
129
+ if (!appCheckConfig || typeof window === 'undefined')
130
+ return;
131
+ const { app } = await getFirebaseAuthClient();
132
+ if (appCheckInitPromise)
133
+ return appCheckInitPromise;
134
+ appCheckInitPromise = (async () => {
135
+ try {
136
+ cachedAppCheck = await initializeFirebaseAppCheck(app, appCheckConfig);
137
+ }
138
+ catch (error) {
139
+ console.warn('App Check initialization failed, continuing without it', error);
140
+ }
141
+ })();
142
+ return appCheckInitPromise;
143
+ }
111
144
  export async function getAppCheckToken() {
145
+ await ensureAppCheck().catch(() => undefined);
112
146
  if (!cachedAppCheck)
113
147
  return undefined;
114
148
  const { getToken } = await import('@firebase/app-check');
@@ -137,7 +171,8 @@ export async function getFirebaseAuthClient() {
137
171
  import('@firebase/app'),
138
172
  import('@firebase/auth'),
139
173
  isPerformanceEnabled ? import('@firebase/performance') : Promise.resolve(null),
140
- ]).then(async ([{ getApp, getApps, initializeApp }, { getAuth }, perfModule]) => {
174
+ ]).then(async ([{ getApp, getApps, initializeApp }, authModule, perfModule]) => {
175
+ const { getAuth, initializeAuth, indexedDBLocalPersistence, browserLocalPersistence, browserSessionPersistence } = authModule;
141
176
  const firebaseConfig = {
142
177
  apiKey: fa.apiKey,
143
178
  authDomain: fa.authDomain,
@@ -148,18 +183,21 @@ export async function getFirebaseAuthClient() {
148
183
  measurementId: fa.measurementId,
149
184
  };
150
185
  const app = getApps().length ? getApp() : initializeApp(firebaseConfig);
151
- if (fa.appCheck && typeof window !== 'undefined') {
186
+ if (perfModule) {
187
+ cachedPerformance = perfModule.initializePerformance(app, { instrumentationEnabled: false });
188
+ }
189
+ let auth;
190
+ if (fa.skipPopupRedirectResolver) {
152
191
  try {
153
- cachedAppCheck = await initializeFirebaseAppCheck(app, fa.appCheck);
192
+ auth = initializeAuth(app, { persistence: [indexedDBLocalPersistence, browserLocalPersistence, browserSessionPersistence] });
154
193
  }
155
- catch (error) {
156
- console.warn('App Check initialization failed, continuing without it', error);
194
+ catch {
195
+ auth = getAuth(app);
157
196
  }
158
197
  }
159
- if (perfModule) {
160
- cachedPerformance = perfModule.initializePerformance(app, { instrumentationEnabled: false });
198
+ else {
199
+ auth = getAuth(app);
161
200
  }
162
- const auth = getAuth(app);
163
201
  cached = { app, auth };
164
202
  return cached;
165
203
  });
@@ -1,2 +1,4 @@
1
1
  export declare const defaultReloadHtml = "<div style=\"position:fixed;inset:0;background:#ffffff;display:flex;align-items:center;justify-content:center;z-index:9999999;\"><div style=\"width:36px;height:36px;border:3px solid #e5e7eb;border-top-color:#17181b;border-radius:50%;animation:cfni-spin 0.8s linear infinite;\"></div><style>@keyframes cfni-spin{to{transform:rotate(360deg)}}</style></div>";
2
- export default function HelperScript(): Component | null;
2
+ export default function HelperScript({ locale }: {
3
+ locale?: string;
4
+ }): Component | null;
@@ -5,13 +5,14 @@ import ClientHelperScript from "../../client/components/client_helper_script.js"
5
5
  import { defaultStaleDeployPatterns } from "../../error_handling/is_stale_deploy_error.js";
6
6
  const isDev = process.env.NODE_ENV === 'development';
7
7
  export const defaultReloadHtml = '<div style="position:fixed;inset:0;background:#ffffff;display:flex;align-items:center;justify-content:center;z-index:9999999;"><div style="width:36px;height:36px;border:3px solid #e5e7eb;border-top-color:#17181b;border-radius:50%;animation:cfni-spin 0.8s linear infinite;"></div><style>@keyframes cfni-spin{to{transform:rotate(360deg)}}</style></div>';
8
- const appCheck = config.firebaseAuth?.appCheck;
9
- const shouldLoadExplicitRecaptchaScript = !!appCheck?.recaptchaV3SiteKey && appCheck.useExplicitRecaptchaScript !== false;
10
8
  const secureCookieAttribute = isDev ? '+ " Secure;"' : '';
11
- export default function HelperScript() {
9
+ export default function HelperScript({ locale }) {
12
10
  const reloadHtml = config.errorHandling?.staleDeployReloadHtml ?? defaultReloadHtml;
13
- return _jsxs(_Fragment, { children: [shouldLoadExplicitRecaptchaScript &&
14
- _jsx("script", { src: "https://www.google.com/recaptcha/api.js?render=explicit", async: true, defer: true }), !isDev &&
11
+ const analytics = config.cookieConsent?.analytics;
12
+ const hasGoogle = !!(analytics?.googleAnalyticsId || analytics?.googleAdsId || analytics?.googleAdSenseId);
13
+ const hasClarity = !!analytics?.clarityProjectId;
14
+ const hasFirebase = !!config.firebaseAuth;
15
+ return _jsxs(_Fragment, { children: [locale && _jsx("meta", { httpEquiv: "Content-Language", content: locale }), hasGoogle && _jsx("link", { rel: "dns-prefetch", href: "https://www.googletagmanager.com" }), hasClarity && _jsx("link", { rel: "dns-prefetch", href: "https://www.clarity.ms" }), hasClarity && _jsx("link", { rel: "dns-prefetch", href: "https://scripts.clarity.ms" }), hasFirebase && _jsx("link", { rel: "dns-prefetch", href: "https://firebaseinstallations.googleapis.com" }), !isDev &&
15
16
  _jsx("script", { id: "stale-deploy-early-catch", dangerouslySetInnerHTML: {
16
17
  __html: `(function() {
17
18
  try {
@@ -134,6 +134,7 @@ export interface FirebaseAuthRoutingConfig {
134
134
  measurementId?: string;
135
135
  performance?: boolean;
136
136
  appCheck?: FirebaseAppCheckConfig;
137
+ skipPopupRedirectResolver?: boolean;
137
138
  redirectAuthPath: string;
138
139
  homePath: string;
139
140
  verifyEmailPath?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudflare-next-intl",
3
- "version": "0.10.16",
3
+ "version": "0.10.18",
4
4
  "description": "Optimized Next Intl Package Special for App Router and Cloudflare",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",