cloudflare-next-intl 0.10.16 → 0.10.17

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
@@ -512,10 +512,12 @@ 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). That script is injected on first actual need (the first `getAppCheckToken()`/token mint) rather than added to `<head>` up front, so a page that never mints a token never pays its ~345KB cost. Supports server-side token minting via service accounts.
520
+ - `lazyInit`: defers App Check's own initialization (not just the script above) to that same first-need moment instead of running it as part of every `getFirebaseAuthClient()` call. Off by default — App Check reads the App Check provider off the app per-request and silently omits the `X-Firebase-AppCheck` header if it isn't registered yet, so deferring is only safe when App Check enforcement is off for every product this app calls (Auth, Firestore, Functions, etc).
519
521
  - `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
522
  - `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
523
 
@@ -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, {}) }))] }) });
@@ -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,34 @@ async function initializeFirebaseAppCheck(app, appCheckConfig) {
108
121
  });
109
122
  }
110
123
  const APP_CHECK_TOKEN_TIMEOUT_MS = 10000;
124
+ let appCheckInitPromise;
125
+ function initAppCheckOnce(app) {
126
+ if (appCheckInitPromise)
127
+ return appCheckInitPromise;
128
+ const appCheckConfig = config.firebaseAuth?.appCheck;
129
+ if (!appCheckConfig || typeof window === 'undefined')
130
+ return Promise.resolve();
131
+ appCheckInitPromise = (async () => {
132
+ try {
133
+ cachedAppCheck = await initializeFirebaseAppCheck(app, appCheckConfig);
134
+ }
135
+ catch (error) {
136
+ console.warn('App Check initialization failed, continuing without it', error);
137
+ }
138
+ })();
139
+ return appCheckInitPromise;
140
+ }
141
+ async function ensureAppCheck() {
142
+ if (appCheckInitPromise)
143
+ return appCheckInitPromise;
144
+ const appCheckConfig = config.firebaseAuth?.appCheck;
145
+ if (!appCheckConfig || typeof window === 'undefined')
146
+ return;
147
+ const { app } = await getFirebaseAuthClient();
148
+ return initAppCheckOnce(app);
149
+ }
111
150
  export async function getAppCheckToken() {
151
+ await ensureAppCheck().catch(() => undefined);
112
152
  if (!cachedAppCheck)
113
153
  return undefined;
114
154
  const { getToken } = await import('@firebase/app-check');
@@ -137,7 +177,8 @@ export async function getFirebaseAuthClient() {
137
177
  import('@firebase/app'),
138
178
  import('@firebase/auth'),
139
179
  isPerformanceEnabled ? import('@firebase/performance') : Promise.resolve(null),
140
- ]).then(async ([{ getApp, getApps, initializeApp }, { getAuth }, perfModule]) => {
180
+ ]).then(async ([{ getApp, getApps, initializeApp }, authModule, perfModule]) => {
181
+ const { getAuth, initializeAuth, indexedDBLocalPersistence, browserLocalPersistence, browserSessionPersistence } = authModule;
141
182
  const firebaseConfig = {
142
183
  apiKey: fa.apiKey,
143
184
  authDomain: fa.authDomain,
@@ -148,18 +189,23 @@ export async function getFirebaseAuthClient() {
148
189
  measurementId: fa.measurementId,
149
190
  };
150
191
  const app = getApps().length ? getApp() : initializeApp(firebaseConfig);
151
- if (fa.appCheck && typeof window !== 'undefined') {
192
+ if (!fa.appCheck?.lazyInit)
193
+ await initAppCheckOnce(app);
194
+ if (perfModule) {
195
+ cachedPerformance = perfModule.initializePerformance(app, { instrumentationEnabled: false });
196
+ }
197
+ let auth;
198
+ if (fa.skipPopupRedirectResolver) {
152
199
  try {
153
- cachedAppCheck = await initializeFirebaseAppCheck(app, fa.appCheck);
200
+ auth = initializeAuth(app, { persistence: [indexedDBLocalPersistence, browserLocalPersistence, browserSessionPersistence] });
154
201
  }
155
- catch (error) {
156
- console.warn('App Check initialization failed, continuing without it', error);
202
+ catch {
203
+ auth = getAuth(app);
157
204
  }
158
205
  }
159
- if (perfModule) {
160
- cachedPerformance = perfModule.initializePerformance(app, { instrumentationEnabled: false });
206
+ else {
207
+ auth = getAuth(app);
161
208
  }
162
- const auth = getAuth(app);
163
209
  cached = { app, auth };
164
210
  return cached;
165
211
  });
@@ -5,13 +5,10 @@ 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
9
  export default function HelperScript() {
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
+ return _jsxs(_Fragment, { children: [!isDev &&
15
12
  _jsx("script", { id: "stale-deploy-early-catch", dangerouslySetInnerHTML: {
16
13
  __html: `(function() {
17
14
  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;
@@ -164,6 +165,7 @@ export interface FirebaseAppCheckConfig {
164
165
  recaptchaV3SiteKey?: string;
165
166
  recaptchaEnterpriseSiteKey?: string;
166
167
  useExplicitRecaptchaScript?: boolean;
168
+ lazyInit?: boolean;
167
169
  debugToken?: boolean | string;
168
170
  isTokenAutoRefreshEnabled?: boolean;
169
171
  clientEmail: 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.17",
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",