cloudflare-next-intl 0.6.13 → 0.6.14

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.
@@ -2,9 +2,14 @@ import type { LocalePrefixMode, Locales, RoutingConfig } from '../types/types';
2
2
  /**
3
3
  * Defines and type-checks your app's i18n routing config.
4
4
  *
5
- * Identity function at runtime — it exists purely so TypeScript infers
6
- * `AppLocales`/`AppLocalePrefixMode` from the literal config object you pass
7
- * in, giving you autocomplete/type errors on `locale` params elsewhere.
5
+ * Mostly an identity function at runtime — it exists primarily so
6
+ * TypeScript infers `AppLocales`/`AppLocalePrefixMode` from the literal
7
+ * config object you pass in, giving you autocomplete/type errors on
8
+ * `locale` params elsewhere. The one runtime behavior: if `firebaseAuth` is
9
+ * set, `redirectAuthPath`/`homePath`/`verifyEmailPath` are auto-corrected to
10
+ * start with `/` (with a console warning) if you forgot it — see
11
+ * `normalizeFirebaseAuthPaths` above for why that specific typo is worth
12
+ * guarding against.
8
13
  *
9
14
  * Export the result from the file referenced by `@intl-config` (see your
10
15
  * `next.config`), e.g.:
@@ -1,9 +1,45 @@
1
+ // Every path this package compares against `request.nextUrl.pathname`
2
+ // (always `/`-prefixed) must itself start with `/` — a missing leading
3
+ // slash means `path === fa.verifyEmailPath` (and the same check for
4
+ // `redirectAuthPath`/`homePath`) never matches, silently disabling that
5
+ // redirect/exemption entirely (e.g. an infinite redirect loop on
6
+ // `verifyEmailPath` because the page is never recognized as itself).
7
+ // Auto-prepending `/` here fixes the common typo (`'login'` instead of
8
+ // `'/login'`) at the source, for every consumer, instead of requiring each
9
+ // one to notice and fix it themselves.
10
+ const FIREBASE_AUTH_PATH_FIELDS = ['redirectAuthPath', 'homePath', 'verifyEmailPath'];
11
+ function normalizeFirebaseAuthPaths(config) {
12
+ const fa = config.firebaseAuth;
13
+ if (!fa)
14
+ return config;
15
+ let changed = false;
16
+ const normalizedFa = { ...fa };
17
+ for (const field of FIREBASE_AUTH_PATH_FIELDS) {
18
+ const value = normalizedFa[field];
19
+ if (typeof value === 'string' && value !== '' && !value.startsWith('/')) {
20
+ console.warn(`[cloudflare-next-intl] firebaseAuth.${field} ("${value}") is missing its leading "/" — ` +
21
+ `auto-corrected to "/${value}". Paths are compared against the URL pathname (always ` +
22
+ `"/"-prefixed), so without this fix the check would never match and silently disable the ` +
23
+ `redirect/exemption for this path. Fix your config to avoid this warning.`);
24
+ normalizedFa[field] = `/${value}`;
25
+ changed = true;
26
+ }
27
+ }
28
+ if (!changed)
29
+ return config;
30
+ return { ...config, firebaseAuth: normalizedFa };
31
+ }
1
32
  /**
2
33
  * Defines and type-checks your app's i18n routing config.
3
34
  *
4
- * Identity function at runtime — it exists purely so TypeScript infers
5
- * `AppLocales`/`AppLocalePrefixMode` from the literal config object you pass
6
- * in, giving you autocomplete/type errors on `locale` params elsewhere.
35
+ * Mostly an identity function at runtime — it exists primarily so
36
+ * TypeScript infers `AppLocales`/`AppLocalePrefixMode` from the literal
37
+ * config object you pass in, giving you autocomplete/type errors on
38
+ * `locale` params elsewhere. The one runtime behavior: if `firebaseAuth` is
39
+ * set, `redirectAuthPath`/`homePath`/`verifyEmailPath` are auto-corrected to
40
+ * start with `/` (with a console warning) if you forgot it — see
41
+ * `normalizeFirebaseAuthPaths` above for why that specific typo is worth
42
+ * guarding against.
7
43
  *
8
44
  * Export the result from the file referenced by `@intl-config` (see your
9
45
  * `next.config`), e.g.:
@@ -15,5 +51,5 @@
15
51
  * ```
16
52
  */
17
53
  export function setIntlConfig(config) {
18
- return config;
54
+ return normalizeFirebaseAuthPaths(config);
19
55
  }
@@ -108,6 +108,19 @@ export default function AuthUserProvider({ initialUser = null, children }) {
108
108
  // login-then-bounce-home flash whenever the two disagreed.
109
109
  const consecutiveNulls = useRef(0);
110
110
  const [confirmedSignedOut, setConfirmedSignedOut] = useState(initialUser === null);
111
+ // Whether `onSignIn`/`onSignOut` have already fired for the CURRENT
112
+ // signed-in/signed-out state — both seeded from `initialUser` so a
113
+ // callback observing the SAME state `initialUser` already established
114
+ // (server-resolved, not a fresh client-side transition) does not refire
115
+ // it. Reset on the opposite transition so the next real occurrence of
116
+ // this state fires again.
117
+ const signInCallbackFired = useRef(initialUser !== null);
118
+ const signOutCallbackFired = useRef(initialUser === null);
119
+ // Tracks the last-observed `emailVerified` value so `onEmailVerified`
120
+ // fires exactly once on the false→true edge, not on every later
121
+ // observation of an already-verified user (both `onIdTokenChanged` and
122
+ // `reloadUser` can be the first to observe the transition).
123
+ const emailVerifiedRef = useRef(initialUser?.emailVerified ?? false);
111
124
  useEffect(() => {
112
125
  const { user, loading } = state;
113
126
  if (loading || isWhiteListed)
@@ -168,11 +181,42 @@ export default function AuthUserProvider({ initialUser = null, children }) {
168
181
  if (user) {
169
182
  consecutiveNulls.current = 0;
170
183
  setConfirmedSignedOut(false);
184
+ signOutCallbackFired.current = false;
185
+ if (!signInCallbackFired.current) {
186
+ signInCallbackFired.current = true;
187
+ try {
188
+ await fa.onSignIn?.(user);
189
+ }
190
+ catch (e) {
191
+ console.error('AuthUserProvider: onSignIn callback failed', e);
192
+ }
193
+ }
194
+ if (!emailVerifiedRef.current && user.emailVerified) {
195
+ emailVerifiedRef.current = true;
196
+ try {
197
+ await fa.onEmailVerified?.(user);
198
+ }
199
+ catch (e) {
200
+ console.error('AuthUserProvider: onEmailVerified callback failed', e);
201
+ }
202
+ }
203
+ else {
204
+ emailVerifiedRef.current = user.emailVerified;
205
+ }
171
206
  }
172
207
  else {
173
208
  consecutiveNulls.current += 1;
174
- if (consecutiveNulls.current >= 2)
209
+ signInCallbackFired.current = false;
210
+ if (consecutiveNulls.current >= 2 && !signOutCallbackFired.current) {
175
211
  setConfirmedSignedOut(true);
212
+ signOutCallbackFired.current = true;
213
+ try {
214
+ await fa.onSignOut?.();
215
+ }
216
+ catch (e) {
217
+ console.error('AuthUserProvider: onSignOut callback failed', e);
218
+ }
219
+ }
176
220
  }
177
221
  const flipped = previous !== undefined && previous !== isSignedIn;
178
222
  const contradictsPage = previous === undefined && isSignedIn === isAuthPage;
@@ -221,6 +265,18 @@ export default function AuthUserProvider({ initialUser = null, children }) {
221
265
  }
222
266
  }
223
267
  await writeSession(user, sessionCookieName, maxAge, refreshTokenCookieName, refreshTokenMaxAge, emailVerifiedHintCookieName, confirmedToken);
268
+ if (!emailVerifiedRef.current && user.emailVerified) {
269
+ emailVerifiedRef.current = true;
270
+ try {
271
+ await fa.onEmailVerified?.(user);
272
+ }
273
+ catch (e) {
274
+ console.error('AuthUserProvider: onEmailVerified callback failed', e);
275
+ }
276
+ }
277
+ else {
278
+ emailVerifiedRef.current = user.emailVerified;
279
+ }
224
280
  setAuthUserCache(user);
225
281
  setState({ user, loading: false });
226
282
  }
@@ -4,6 +4,7 @@ import type { Videos } from 'next/dist/lib/metadata/types/metadata-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
  import type { ConsentValue } from '../cookie_consent/types';
7
+ import type { User } from 'firebase/auth';
7
8
  /**
8
9
  * Custom middleware hook, run by `intlMiddleware` for your own logic
9
10
  * (e.g. auth, feature flags, A/B tests) — on top of the library's own
@@ -439,11 +440,11 @@ export interface FirebaseAuthRoutingConfig {
439
440
  appId: string;
440
441
  /** Firebase Analytics measurement ID. */
441
442
  measurementId?: string;
442
- /** Path to redirect signed-out users to, e.g. "/login". */
443
+ /** Path to redirect signed-out users to, e.g. "/login". Must start with "/" — `setIntlConfig` auto-corrects a missing leading slash with a warning. */
443
444
  redirectAuthPath: string;
444
- /** Path to redirect signed-in users away from auth pages to, e.g. "/". */
445
+ /** Path to redirect signed-in users away from auth pages to, e.g. "/". Must start with "/" — `setIntlConfig` auto-corrects a missing leading slash with a warning. */
445
446
  homePath: string;
446
- /** Path to redirect unverified-email users to. Omit to skip email-verification redirects. */
447
+ /** Path to redirect unverified-email users to. Omit to skip email-verification redirects. Must start with "/" — `setIntlConfig` auto-corrects a missing leading slash with a warning. */
447
448
  verifyEmailPath?: string;
448
449
  /** Returns true if the given (locale-stripped) path is an auth page (login/signup/etc). */
449
450
  isAuthPath: (path: string) => boolean;
@@ -459,6 +460,34 @@ export interface FirebaseAuthRoutingConfig {
459
460
  refreshTokenCookieName?: string;
460
461
  /** Email-verified hint cookie name. Defaults to `'__fa_email_verified_hint__'`. Client-written, non-httpOnly; lets the middleware avoid an unnecessary token refresh when its view already matches the client's. */
461
462
  emailVerifiedHintCookieName?: string;
463
+ /**
464
+ * Called once, the moment `AuthUserProvider` observes a real sign-in
465
+ * (a `null → user` transition) — never on a plain token refresh of an
466
+ * already-signed-in user. Runs after the session/refresh-token/
467
+ * email-verified-hint cookies have already been written for this
468
+ * user, so cookie state is in sync when this fires. A throw/rejection
469
+ * is caught and logged via `console.error`; it never blocks cookie
470
+ * sync or navigation.
471
+ */
472
+ onSignIn?: (user: User) => void | Promise<void>;
473
+ /**
474
+ * Called once, on the `false → true` transition of `user.emailVerified`
475
+ * — never on a later observation of an already-verified user. Checked
476
+ * from both `AuthUserProvider`'s `onIdTokenChanged` listener and its
477
+ * `reloadUser()`, since either can be the first to observe the
478
+ * transition. A throw/rejection is caught and logged via
479
+ * `console.error`.
480
+ */
481
+ onEmailVerified?: (user: User) => void | Promise<void>;
482
+ /**
483
+ * Called once, when sign-out is confirmed — after `AuthUserProvider`'s
484
+ * existing debounce for transient SDK null-callbacks (two consecutive
485
+ * `onIdTokenChanged(null)` calls), not on the first, possibly
486
+ * transient, null. Runs after the session/refresh-token cookies have
487
+ * already been cleared. A throw/rejection is caught and logged via
488
+ * `console.error`.
489
+ */
490
+ onSignOut?: () => void | Promise<void>;
462
491
  }
463
492
  export interface CookieAttributes {
464
493
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudflare-next-intl",
3
- "version": "0.6.13",
3
+ "version": "0.6.14",
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",