@imtbl/auth-nextjs 2.12.4-alpha.6 → 2.12.4-alpha.8

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
@@ -61,10 +61,12 @@ const config = {
61
61
  };
62
62
 
63
63
  export default function Callback() {
64
- return <CallbackPage config={config} />;
64
+ return <CallbackPage config={config} redirectTo="/dashboard" />;
65
65
  }
66
66
  ```
67
67
 
68
+ See [CallbackPage Props](#callbackpage-props) for all available options.
69
+
68
70
  ### 4. Add Provider to Layout
69
71
 
70
72
  ```typescript
@@ -166,14 +168,15 @@ export const config = {
166
168
 
167
169
  The `ImmutableAuthConfig` object accepts the following properties:
168
170
 
169
- | Property | Type | Required | Default | Description |
170
- | ---------------------- | -------- | -------- | ------------------------------------------------ | ------------------------------ |
171
- | `clientId` | `string` | Yes | - | Immutable OAuth client ID |
172
- | `redirectUri` | `string` | Yes | - | OAuth callback redirect URI |
173
- | `logoutRedirectUri` | `string` | No | - | Where to redirect after logout |
174
- | `audience` | `string` | No | `"platform_api"` | OAuth audience |
175
- | `scope` | `string` | No | `"openid profile email offline_access transact"` | OAuth scopes |
176
- | `authenticationDomain` | `string` | No | `"https://auth.immutable.com"` | Authentication domain |
171
+ | Property | Type | Required | Default | Description |
172
+ | ---------------------- | -------- | -------- | ------------------------------------------------ | ----------------------------------------------- |
173
+ | `clientId` | `string` | Yes | - | Immutable OAuth client ID |
174
+ | `redirectUri` | `string` | Yes | - | OAuth callback redirect URI (for redirect flow) |
175
+ | `popupRedirectUri` | `string` | No | `redirectUri` | OAuth callback redirect URI for popup flow |
176
+ | `logoutRedirectUri` | `string` | No | - | Where to redirect after logout |
177
+ | `audience` | `string` | No | `"platform_api"` | OAuth audience |
178
+ | `scope` | `string` | No | `"openid profile email offline_access transact"` | OAuth scopes |
179
+ | `authenticationDomain` | `string` | No | `"https://auth.immutable.com"` | Authentication domain |
177
180
 
178
181
  ## Environment Variables
179
182
 
@@ -223,6 +226,69 @@ openssl rand -base64 32
223
226
  | `useAccessToken()` | Hook returning `getAccessToken` function |
224
227
  | `CallbackPage` | Pre-built callback page component for OAuth redirects |
225
228
 
229
+ #### CallbackPage Props
230
+
231
+ | Prop | Type | Default | Description |
232
+ | ------------------ | ----------------------------------------------------- | ------- | ------------------------------------------------------------------ |
233
+ | `config` | `ImmutableAuthConfig` | - | Required. Immutable auth configuration |
234
+ | `redirectTo` | `string \| ((user: ImmutableUser) => string \| void)` | `"/"` | Where to redirect after successful auth (supports dynamic routing) |
235
+ | `loadingComponent` | `React.ReactElement \| null` | `null` | Custom loading UI while processing authentication |
236
+ | `errorComponent` | `(error: string) => React.ReactElement \| null` | - | Custom error UI component |
237
+ | `onSuccess` | `(user: ImmutableUser) => void \| Promise<void>` | - | Callback fired after successful authentication |
238
+ | `onError` | `(error: string) => void` | - | Callback fired when authentication fails |
239
+
240
+ **Example with all props:**
241
+
242
+ ```tsx
243
+ // app/callback/page.tsx
244
+ "use client";
245
+
246
+ import { CallbackPage } from "@imtbl/auth-nextjs/client";
247
+ import { Spinner } from "@/components/ui/spinner";
248
+
249
+ const config = {
250
+ clientId: process.env.NEXT_PUBLIC_IMMUTABLE_CLIENT_ID!,
251
+ redirectUri: `${process.env.NEXT_PUBLIC_BASE_URL}/callback`,
252
+ };
253
+
254
+ export default function Callback() {
255
+ return (
256
+ <CallbackPage
257
+ config={config}
258
+ // Dynamic redirect based on user
259
+ redirectTo={(user) => {
260
+ if (user.email?.endsWith("@admin.com")) return "/admin";
261
+ return "/dashboard";
262
+ }}
263
+ // Custom loading UI
264
+ loadingComponent={
265
+ <div className="flex items-center justify-center min-h-screen">
266
+ <Spinner />
267
+ <span>Completing authentication...</span>
268
+ </div>
269
+ }
270
+ // Custom error UI
271
+ errorComponent={(error) => (
272
+ <div className="text-center p-8">
273
+ <h2 className="text-red-500">Authentication Error</h2>
274
+ <p>{error}</p>
275
+ <a href="/">Return Home</a>
276
+ </div>
277
+ )}
278
+ // Success callback for analytics
279
+ onSuccess={async (user) => {
280
+ await analytics.track("login_success", { userId: user.sub });
281
+ }}
282
+ // Error callback for logging
283
+ onError={(error) => {
284
+ console.error("Auth failed:", error);
285
+ Sentry.captureMessage(error);
286
+ }}
287
+ />
288
+ );
289
+ }
290
+ ```
291
+
226
292
  **`useImmutableAuth()` Return Value:**
227
293
 
228
294
  | Property | Type | Description |
@@ -247,6 +247,9 @@ function createImmutableAuth(config, overrides) {
247
247
  if (overrides.callbacks.redirect) {
248
248
  composedCallbacks.redirect = overrides.callbacks.redirect;
249
249
  }
250
+ if (overrides.callbacks.authorized) {
251
+ composedCallbacks.authorized = overrides.callbacks.authorized;
252
+ }
250
253
  }
251
254
  const mergedConfig = {
252
255
  ...authConfig,
@@ -76,22 +76,24 @@ function ImmutableAuthInner({
76
76
  const [isAuthReady, setIsAuthReady] = (0, import_react.useState)(false);
77
77
  const { data: session, update: updateSession } = (0, import_react2.useSession)();
78
78
  (0, import_react.useEffect)(() => {
79
- if (typeof window === "undefined") return;
79
+ if (typeof window === "undefined") return void 0;
80
80
  const configKey = [
81
81
  config.clientId,
82
82
  config.redirectUri,
83
+ config.popupRedirectUri || "",
83
84
  config.logoutRedirectUri || "",
84
85
  config.audience || DEFAULT_AUDIENCE,
85
86
  config.scope || DEFAULT_SCOPE,
86
87
  config.authenticationDomain || DEFAULT_AUTH_DOMAIN
87
88
  ].join(":");
88
89
  if (prevConfigRef.current === configKey) {
89
- return;
90
+ return void 0;
90
91
  }
91
92
  prevConfigRef.current = configKey;
92
93
  const newAuth = new import_auth2.Auth({
93
94
  clientId: config.clientId,
94
95
  redirectUri: config.redirectUri,
96
+ popupRedirectUri: config.popupRedirectUri,
95
97
  logoutRedirectUri: config.logoutRedirectUri,
96
98
  audience: config.audience || DEFAULT_AUDIENCE,
97
99
  scope: config.scope || DEFAULT_SCOPE,
@@ -99,6 +101,11 @@ function ImmutableAuthInner({
99
101
  });
100
102
  setAuth(newAuth);
101
103
  setIsAuthReady(true);
104
+ return () => {
105
+ setAuth(null);
106
+ setIsAuthReady(false);
107
+ prevConfigRef.current = null;
108
+ };
102
109
  }, [config]);
103
110
  (0, import_react.useEffect)(() => {
104
111
  if (!auth || !isAuthReady) return;
@@ -262,7 +269,9 @@ function CallbackPage({
262
269
  config,
263
270
  redirectTo = "/",
264
271
  loadingComponent = null,
265
- errorComponent
272
+ errorComponent,
273
+ onSuccess,
274
+ onError
266
275
  }) {
267
276
  const router = (0, import_navigation.useRouter)();
268
277
  const searchParams = (0, import_navigation.useSearchParams)();
@@ -284,6 +293,14 @@ function CallbackPage({
284
293
  if (!authUser) {
285
294
  throw new Error("Authentication failed: no user data received from login callback");
286
295
  }
296
+ const user = {
297
+ sub: authUser.profile.sub,
298
+ email: authUser.profile.email,
299
+ nickname: authUser.profile.nickname
300
+ };
301
+ if (onSuccess) {
302
+ await onSuccess(user);
303
+ }
287
304
  window.close();
288
305
  } else if (authUser) {
289
306
  const tokenData = {
@@ -308,29 +325,58 @@ function CallbackPage({
308
325
  if (!result?.ok) {
309
326
  throw new Error("NextAuth sign-in failed: unknown error");
310
327
  }
311
- router.replace(redirectTo);
328
+ const user = {
329
+ sub: authUser.profile.sub,
330
+ email: authUser.profile.email,
331
+ nickname: authUser.profile.nickname
332
+ };
333
+ if (onSuccess) {
334
+ await onSuccess(user);
335
+ }
336
+ const resolvedRedirectTo = typeof redirectTo === "function" ? redirectTo(user) || "/" : redirectTo;
337
+ router.replace(resolvedRedirectTo);
312
338
  } else {
313
339
  throw new Error("Authentication failed: no user data received from login callback");
314
340
  }
315
341
  } catch (err) {
316
- setError(err instanceof Error ? err.message : "Authentication failed");
342
+ const errorMessage2 = err instanceof Error ? err.message : "Authentication failed";
343
+ if (onError) {
344
+ onError(errorMessage2);
345
+ }
346
+ setError(errorMessage2);
317
347
  }
318
348
  };
319
349
  const handleOAuthError = () => {
320
350
  const errorCode = searchParams.get("error");
321
351
  const errorDescription = searchParams.get("error_description");
322
- const errorMessage = errorDescription || errorCode || "Authentication failed";
323
- setError(errorMessage);
352
+ const errorMessage2 = errorDescription || errorCode || "Authentication failed";
353
+ if (onError) {
354
+ onError(errorMessage2);
355
+ }
356
+ setError(errorMessage2);
324
357
  };
325
- if (searchParams.get("error")) {
358
+ if (callbackProcessedRef.current) {
359
+ return;
360
+ }
361
+ const hasError = searchParams.get("error");
362
+ const hasCode = searchParams.get("code");
363
+ if (hasError) {
364
+ callbackProcessedRef.current = true;
326
365
  handleOAuthError();
327
366
  return;
328
367
  }
329
- if (searchParams.get("code") && !callbackProcessedRef.current) {
368
+ if (hasCode) {
330
369
  callbackProcessedRef.current = true;
331
370
  handleCallback();
371
+ return;
372
+ }
373
+ callbackProcessedRef.current = true;
374
+ const errorMessage = "Invalid callback: missing OAuth parameters. Please try logging in again.";
375
+ if (onError) {
376
+ onError(errorMessage);
332
377
  }
333
- }, [searchParams, router, config, redirectTo]);
378
+ setError(errorMessage);
379
+ }, [searchParams, router, config, redirectTo, onSuccess, onError]);
334
380
  if (error) {
335
381
  if (errorComponent) {
336
382
  return errorComponent(error);
@@ -62,22 +62,24 @@ function ImmutableAuthInner({
62
62
  const [isAuthReady, setIsAuthReady] = useState(false);
63
63
  const { data: session, update: updateSession } = useSession();
64
64
  useEffect(() => {
65
- if (typeof window === "undefined") return;
65
+ if (typeof window === "undefined") return void 0;
66
66
  const configKey = [
67
67
  config.clientId,
68
68
  config.redirectUri,
69
+ config.popupRedirectUri || "",
69
70
  config.logoutRedirectUri || "",
70
71
  config.audience || DEFAULT_AUDIENCE,
71
72
  config.scope || DEFAULT_SCOPE,
72
73
  config.authenticationDomain || DEFAULT_AUTH_DOMAIN
73
74
  ].join(":");
74
75
  if (prevConfigRef.current === configKey) {
75
- return;
76
+ return void 0;
76
77
  }
77
78
  prevConfigRef.current = configKey;
78
79
  const newAuth = new Auth({
79
80
  clientId: config.clientId,
80
81
  redirectUri: config.redirectUri,
82
+ popupRedirectUri: config.popupRedirectUri,
81
83
  logoutRedirectUri: config.logoutRedirectUri,
82
84
  audience: config.audience || DEFAULT_AUDIENCE,
83
85
  scope: config.scope || DEFAULT_SCOPE,
@@ -85,6 +87,11 @@ function ImmutableAuthInner({
85
87
  });
86
88
  setAuth(newAuth);
87
89
  setIsAuthReady(true);
90
+ return () => {
91
+ setAuth(null);
92
+ setIsAuthReady(false);
93
+ prevConfigRef.current = null;
94
+ };
88
95
  }, [config]);
89
96
  useEffect(() => {
90
97
  if (!auth || !isAuthReady) return;
@@ -248,7 +255,9 @@ function CallbackPage({
248
255
  config,
249
256
  redirectTo = "/",
250
257
  loadingComponent = null,
251
- errorComponent
258
+ errorComponent,
259
+ onSuccess,
260
+ onError
252
261
  }) {
253
262
  const router = useRouter();
254
263
  const searchParams = useSearchParams();
@@ -270,6 +279,14 @@ function CallbackPage({
270
279
  if (!authUser) {
271
280
  throw new Error("Authentication failed: no user data received from login callback");
272
281
  }
282
+ const user = {
283
+ sub: authUser.profile.sub,
284
+ email: authUser.profile.email,
285
+ nickname: authUser.profile.nickname
286
+ };
287
+ if (onSuccess) {
288
+ await onSuccess(user);
289
+ }
273
290
  window.close();
274
291
  } else if (authUser) {
275
292
  const tokenData = {
@@ -294,29 +311,58 @@ function CallbackPage({
294
311
  if (!result?.ok) {
295
312
  throw new Error("NextAuth sign-in failed: unknown error");
296
313
  }
297
- router.replace(redirectTo);
314
+ const user = {
315
+ sub: authUser.profile.sub,
316
+ email: authUser.profile.email,
317
+ nickname: authUser.profile.nickname
318
+ };
319
+ if (onSuccess) {
320
+ await onSuccess(user);
321
+ }
322
+ const resolvedRedirectTo = typeof redirectTo === "function" ? redirectTo(user) || "/" : redirectTo;
323
+ router.replace(resolvedRedirectTo);
298
324
  } else {
299
325
  throw new Error("Authentication failed: no user data received from login callback");
300
326
  }
301
327
  } catch (err) {
302
- setError(err instanceof Error ? err.message : "Authentication failed");
328
+ const errorMessage2 = err instanceof Error ? err.message : "Authentication failed";
329
+ if (onError) {
330
+ onError(errorMessage2);
331
+ }
332
+ setError(errorMessage2);
303
333
  }
304
334
  };
305
335
  const handleOAuthError = () => {
306
336
  const errorCode = searchParams.get("error");
307
337
  const errorDescription = searchParams.get("error_description");
308
- const errorMessage = errorDescription || errorCode || "Authentication failed";
309
- setError(errorMessage);
338
+ const errorMessage2 = errorDescription || errorCode || "Authentication failed";
339
+ if (onError) {
340
+ onError(errorMessage2);
341
+ }
342
+ setError(errorMessage2);
310
343
  };
311
- if (searchParams.get("error")) {
344
+ if (callbackProcessedRef.current) {
345
+ return;
346
+ }
347
+ const hasError = searchParams.get("error");
348
+ const hasCode = searchParams.get("code");
349
+ if (hasError) {
350
+ callbackProcessedRef.current = true;
312
351
  handleOAuthError();
313
352
  return;
314
353
  }
315
- if (searchParams.get("code") && !callbackProcessedRef.current) {
354
+ if (hasCode) {
316
355
  callbackProcessedRef.current = true;
317
356
  handleCallback();
357
+ return;
358
+ }
359
+ callbackProcessedRef.current = true;
360
+ const errorMessage = "Invalid callback: missing OAuth parameters. Please try logging in again.";
361
+ if (onError) {
362
+ onError(errorMessage);
318
363
  }
319
- }, [searchParams, router, config, redirectTo]);
364
+ setError(errorMessage);
365
+ }, [searchParams, router, config, redirectTo, onSuccess, onError]);
320
366
  if (error) {
321
367
  if (errorComponent) {
322
368
  return errorComponent(error);
@@ -290,6 +290,9 @@ function createImmutableAuth(config, overrides) {
290
290
  if (overrides.callbacks.redirect) {
291
291
  composedCallbacks.redirect = overrides.callbacks.redirect;
292
292
  }
293
+ if (overrides.callbacks.authorized) {
294
+ composedCallbacks.authorized = overrides.callbacks.authorized;
295
+ }
293
296
  }
294
297
  const mergedConfig = {
295
298
  ...authConfig,
@@ -9,7 +9,7 @@ import {
9
9
  createImmutableAuth,
10
10
  isTokenExpired,
11
11
  refreshAccessToken
12
- } from "./chunk-BRDI4KXS.js";
12
+ } from "./chunk-BLU3AV4X.js";
13
13
  export {
14
14
  DEFAULT_AUDIENCE,
15
15
  DEFAULT_AUTH_DOMAIN,
@@ -284,6 +284,9 @@ function createImmutableAuth(config, overrides) {
284
284
  if (overrides.callbacks.redirect) {
285
285
  composedCallbacks.redirect = overrides.callbacks.redirect;
286
286
  }
287
+ if (overrides.callbacks.authorized) {
288
+ composedCallbacks.authorized = overrides.callbacks.authorized;
289
+ }
287
290
  }
288
291
  const mergedConfig = {
289
292
  ...authConfig,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createAuthConfig,
3
3
  createImmutableAuth
4
- } from "../chunk-BRDI4KXS.js";
4
+ } from "../chunk-BLU3AV4X.js";
5
5
 
6
6
  // src/server/index.ts
7
7
  import { NextResponse } from "next/server";
@@ -1,14 +1,30 @@
1
1
  import React from 'react';
2
- import type { ImmutableAuthConfig } from '../types';
2
+ import type { ImmutableAuthConfig, ImmutableUser } from '../types';
3
3
  export interface CallbackPageProps {
4
4
  /**
5
5
  * Immutable auth configuration
6
6
  */
7
7
  config: ImmutableAuthConfig;
8
8
  /**
9
- * URL to redirect to after successful authentication (when not in popup)
9
+ * URL to redirect to after successful authentication (when not in popup).
10
+ * Can be a string or a function that receives the authenticated user.
11
+ * If a function returns void/undefined, defaults to "/".
12
+ * @default "/"
13
+ *
14
+ * @example Static redirect
15
+ * ```tsx
16
+ * <CallbackPage config={config} redirectTo="/dashboard" />
17
+ * ```
18
+ *
19
+ * @example Dynamic redirect based on user
20
+ * ```tsx
21
+ * <CallbackPage
22
+ * config={config}
23
+ * redirectTo={(user) => user.email?.endsWith('@admin.com') ? '/admin' : '/dashboard'}
24
+ * />
25
+ * ```
10
26
  */
11
- redirectTo?: string;
27
+ redirectTo?: string | ((user: ImmutableUser) => string | void);
12
28
  /**
13
29
  * Custom loading component
14
30
  */
@@ -17,6 +33,19 @@ export interface CallbackPageProps {
17
33
  * Custom error component
18
34
  */
19
35
  errorComponent?: (error: string) => React.ReactElement | null;
36
+ /**
37
+ * Callback fired after successful authentication.
38
+ * Receives the authenticated user as a parameter.
39
+ * Called before redirect (non-popup) or before window.close (popup).
40
+ * If this callback returns a Promise, it will be awaited before proceeding.
41
+ */
42
+ onSuccess?: (user: ImmutableUser) => void | Promise<void>;
43
+ /**
44
+ * Callback fired when authentication fails.
45
+ * Receives the error message as a parameter.
46
+ * Called before the error UI is displayed.
47
+ */
48
+ onError?: (error: string) => void;
20
49
  }
21
50
  /**
22
51
  * Callback page component for handling OAuth redirects (App Router version).
@@ -39,4 +68,4 @@ export interface CallbackPageProps {
39
68
  * }
40
69
  * ```
41
70
  */
42
- export declare function CallbackPage({ config, redirectTo, loadingComponent, errorComponent, }: CallbackPageProps): import("react/jsx-runtime").JSX.Element | null;
71
+ export declare function CallbackPage({ config, redirectTo, loadingComponent, errorComponent, onSuccess, onError, }: CallbackPageProps): import("react/jsx-runtime").JSX.Element | null;
@@ -9,9 +9,14 @@ export interface ImmutableAuthConfig {
9
9
  */
10
10
  clientId: string;
11
11
  /**
12
- * OAuth callback redirect URI
12
+ * OAuth callback redirect URI (used for redirect flow)
13
13
  */
14
14
  redirectUri: string;
15
+ /**
16
+ * OAuth callback redirect URI for popup flow
17
+ * If not provided, falls back to redirectUri
18
+ */
19
+ popupRedirectUri?: string;
15
20
  /**
16
21
  * Where to redirect after logout
17
22
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imtbl/auth-nextjs",
3
- "version": "2.12.4-alpha.6",
3
+ "version": "2.12.4-alpha.8",
4
4
  "description": "Next.js App Router authentication integration for Immutable SDK using Auth.js v5",
5
5
  "author": "Immutable",
6
6
  "bugs": "https://github.com/immutable/ts-immutable-sdk/issues",
@@ -51,7 +51,7 @@
51
51
  "dist"
52
52
  ],
53
53
  "dependencies": {
54
- "@imtbl/auth": "2.12.4-alpha.6"
54
+ "@imtbl/auth": "2.12.4-alpha.8"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "next": "14.2.25",