@imtbl/auth-next-client 2.12.5-alpha.13 → 2.12.5-alpha.15

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 ADDED
@@ -0,0 +1,795 @@
1
+ # @imtbl/auth-next-client
2
+
3
+ Client-side React components and hooks for Immutable authentication with Auth.js v5 (NextAuth) in Next.js applications.
4
+
5
+ ## Overview
6
+
7
+ This package provides React components and hooks for client-side authentication in Next.js applications using the App Router. It works in conjunction with `@imtbl/auth-next-server` to provide a complete authentication solution.
8
+
9
+ **Key features:**
10
+ - `ImmutableAuthProvider` - Authentication context provider
11
+ - `useImmutableAuth` - Hook for auth state and methods
12
+ - `CallbackPage` - OAuth callback handler component
13
+ - `useHydratedData` - SSR data hydration with client-side fallback
14
+ - Automatic token refresh and session synchronization
15
+
16
+ For server-side utilities, use [`@imtbl/auth-next-server`](../auth-next-server).
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ npm install @imtbl/auth-next-client @imtbl/auth-next-server next-auth@5
22
+ # or
23
+ pnpm add @imtbl/auth-next-client @imtbl/auth-next-server next-auth@5
24
+ # or
25
+ yarn add @imtbl/auth-next-client @imtbl/auth-next-server next-auth@5
26
+ ```
27
+
28
+ ### Peer Dependencies
29
+
30
+ - `react` >= 18.0.0
31
+ - `next` >= 14.0.0
32
+ - `next-auth` >= 5.0.0-beta.25
33
+
34
+ ## Quick Start
35
+
36
+ ### 1. Set Up Server-Side Auth
37
+
38
+ First, set up the server-side authentication following the [`@imtbl/auth-next-server` documentation](../auth-next-server).
39
+
40
+ ### 2. Create Providers Component
41
+
42
+ ```tsx
43
+ // app/providers.tsx
44
+ "use client";
45
+
46
+ import { ImmutableAuthProvider } from "@imtbl/auth-next-client";
47
+
48
+ const config = {
49
+ clientId: process.env.NEXT_PUBLIC_IMMUTABLE_CLIENT_ID!,
50
+ redirectUri: `${process.env.NEXT_PUBLIC_BASE_URL}/callback`,
51
+ };
52
+
53
+ export function Providers({ children }: { children: React.ReactNode }) {
54
+ return (
55
+ <ImmutableAuthProvider config={config}>
56
+ {children}
57
+ </ImmutableAuthProvider>
58
+ );
59
+ }
60
+ ```
61
+
62
+ ### 3. Wrap Your App
63
+
64
+ ```tsx
65
+ // app/layout.tsx
66
+ import { Providers } from "./providers";
67
+
68
+ export default function RootLayout({
69
+ children,
70
+ }: {
71
+ children: React.ReactNode;
72
+ }) {
73
+ return (
74
+ <html lang="en">
75
+ <body>
76
+ <Providers>{children}</Providers>
77
+ </body>
78
+ </html>
79
+ );
80
+ }
81
+ ```
82
+
83
+ ### 4. Create Callback Page
84
+
85
+ ```tsx
86
+ // app/callback/page.tsx
87
+ "use client";
88
+
89
+ import { CallbackPage } from "@imtbl/auth-next-client";
90
+
91
+ const config = {
92
+ clientId: process.env.NEXT_PUBLIC_IMMUTABLE_CLIENT_ID!,
93
+ redirectUri: `${process.env.NEXT_PUBLIC_BASE_URL}/callback`,
94
+ };
95
+
96
+ export default function Callback() {
97
+ return <CallbackPage config={config} redirectTo="/dashboard" />;
98
+ }
99
+ ```
100
+
101
+ ### 5. Use Authentication in Components
102
+
103
+ ```tsx
104
+ // components/AuthButton.tsx
105
+ "use client";
106
+
107
+ import { useImmutableAuth } from "@imtbl/auth-next-client";
108
+
109
+ export function AuthButton() {
110
+ const { user, isLoading, isLoggingIn, isAuthenticated, signIn, signOut } = useImmutableAuth();
111
+
112
+ if (isLoading) {
113
+ return <div>Loading...</div>;
114
+ }
115
+
116
+ if (isAuthenticated) {
117
+ return (
118
+ <div>
119
+ <span>Welcome, {user?.email || user?.sub}</span>
120
+ <button onClick={() => signOut()}>Sign Out</button>
121
+ </div>
122
+ );
123
+ }
124
+
125
+ return (
126
+ <button onClick={() => signIn()} disabled={isLoggingIn}>
127
+ {isLoggingIn ? "Signing in..." : "Sign In"}
128
+ </button>
129
+ );
130
+ }
131
+ ```
132
+
133
+ ## Components
134
+
135
+ ### `ImmutableAuthProvider`
136
+
137
+ **Use case:** Wraps your application to provide authentication context. Required for all `useImmutableAuth` and related hooks to work.
138
+
139
+ **When to use:**
140
+ - Required: Must wrap your app at the root level (typically in `app/layout.tsx` or a providers file)
141
+ - Provides auth state to all child components via React Context
142
+
143
+ ```tsx
144
+ // app/providers.tsx
145
+ // Use case: Basic provider setup
146
+ "use client";
147
+
148
+ import { ImmutableAuthProvider } from "@imtbl/auth-next-client";
149
+
150
+ const config = {
151
+ clientId: process.env.NEXT_PUBLIC_IMMUTABLE_CLIENT_ID!,
152
+ redirectUri: `${process.env.NEXT_PUBLIC_BASE_URL}/callback`,
153
+ };
154
+
155
+ export function Providers({ children }: { children: React.ReactNode }) {
156
+ return (
157
+ <ImmutableAuthProvider config={config}>
158
+ {children}
159
+ </ImmutableAuthProvider>
160
+ );
161
+ }
162
+ ```
163
+
164
+ #### With SSR Session Hydration
165
+
166
+ Pass the server-side session to avoid a flash of unauthenticated state on page load:
167
+
168
+ ```tsx
169
+ // app/providers.tsx
170
+ // Use case: SSR hydration to prevent auth state flash
171
+ "use client";
172
+
173
+ import { ImmutableAuthProvider } from "@imtbl/auth-next-client";
174
+ import type { Session } from "next-auth";
175
+
176
+ export function Providers({
177
+ children,
178
+ session // Passed from Server Component
179
+ }: {
180
+ children: React.ReactNode;
181
+ session: Session | null;
182
+ }) {
183
+ return (
184
+ <ImmutableAuthProvider config={config} session={session}>
185
+ {children}
186
+ </ImmutableAuthProvider>
187
+ );
188
+ }
189
+ ```
190
+
191
+ ```tsx
192
+ // app/layout.tsx
193
+ // Use case: Get session server-side and pass to providers
194
+ import { auth } from "@/lib/auth";
195
+ import { Providers } from "./providers";
196
+
197
+ export default async function RootLayout({ children }) {
198
+ const session = await auth();
199
+ return (
200
+ <html>
201
+ <body>
202
+ <Providers session={session}>{children}</Providers>
203
+ </body>
204
+ </html>
205
+ );
206
+ }
207
+ ```
208
+
209
+ #### With Custom Base Path
210
+
211
+ Use when you have a non-standard Auth.js API route path:
212
+
213
+ ```tsx
214
+ // Use case: Custom API route path (e.g., per-environment routes)
215
+ <ImmutableAuthProvider
216
+ config={config}
217
+ basePath="/api/auth/sandbox" // Instead of default "/api/auth"
218
+ >
219
+ {children}
220
+ </ImmutableAuthProvider>
221
+ ```
222
+
223
+ #### Props
224
+
225
+ | Prop | Type | Required | Description |
226
+ |------|------|----------|-------------|
227
+ | `config` | `object` | Yes | Authentication configuration |
228
+ | `config.clientId` | `string` | Yes | Immutable application client ID |
229
+ | `config.redirectUri` | `string` | Yes | OAuth redirect URI (must match Immutable Hub config) |
230
+ | `config.popupRedirectUri` | `string` | No | Separate redirect URI for popup login flows |
231
+ | `config.logoutRedirectUri` | `string` | No | Where to redirect after logout |
232
+ | `config.audience` | `string` | No | OAuth audience (default: `"platform_api"`) |
233
+ | `config.scope` | `string` | No | OAuth scopes (default includes `transact` for blockchain) |
234
+ | `config.authenticationDomain` | `string` | No | Immutable auth domain URL |
235
+ | `config.passportDomain` | `string` | No | Immutable Passport domain URL |
236
+ | `session` | `Session` | No | Server-side session for SSR hydration (prevents auth flash) |
237
+ | `basePath` | `string` | No | Auth.js API base path (default: `"/api/auth"`) |
238
+
239
+ ### `CallbackPage`
240
+
241
+ **Use case:** Handles the OAuth callback after Immutable authentication. This component processes the authorization code from the URL and establishes the session.
242
+
243
+ **When to use:**
244
+ - Required for redirect-based login flows (when user is redirected to Immutable login page)
245
+ - Create a page at your `redirectUri` path (e.g., `/callback`)
246
+
247
+ **How it works:**
248
+ 1. User clicks "Sign In" → redirected to Immutable login
249
+ 2. After login, Immutable redirects to your callback URL with auth code
250
+ 3. `CallbackPage` exchanges the code for tokens and creates the session
251
+ 4. User is redirected to your app (e.g., `/dashboard`)
252
+
253
+ ```tsx
254
+ // app/callback/page.tsx
255
+ // Use case: Basic callback page that redirects to dashboard after login
256
+ "use client";
257
+
258
+ import { CallbackPage } from "@imtbl/auth-next-client";
259
+
260
+ const config = {
261
+ clientId: process.env.NEXT_PUBLIC_IMMUTABLE_CLIENT_ID!,
262
+ redirectUri: `${process.env.NEXT_PUBLIC_BASE_URL}/callback`,
263
+ };
264
+
265
+ export default function Callback() {
266
+ return <CallbackPage config={config} redirectTo="/dashboard" />;
267
+ }
268
+ ```
269
+
270
+ #### Dynamic Redirect Based on User
271
+
272
+ ```tsx
273
+ // app/callback/page.tsx
274
+ // Use case: Redirect new users to onboarding, existing users to dashboard
275
+ "use client";
276
+
277
+ import { CallbackPage } from "@imtbl/auth-next-client";
278
+
279
+ export default function Callback() {
280
+ return (
281
+ <CallbackPage
282
+ config={config}
283
+ redirectTo={(user) => {
284
+ // Redirect based on user properties
285
+ return user.email ? "/dashboard" : "/onboarding";
286
+ }}
287
+ onSuccess={async (user) => {
288
+ // Track successful login
289
+ await analytics.track("user_logged_in", { userId: user.sub });
290
+ }}
291
+ onError={(error) => {
292
+ // Log authentication failures
293
+ console.error("Login failed:", error);
294
+ }}
295
+ />
296
+ );
297
+ }
298
+ ```
299
+
300
+ #### Custom Loading and Error UI
301
+
302
+ ```tsx
303
+ // app/callback/page.tsx
304
+ // Use case: Branded callback page with custom loading and error states
305
+ "use client";
306
+
307
+ import { CallbackPage } from "@imtbl/auth-next-client";
308
+ import { Spinner, ErrorCard } from "@/components/ui";
309
+
310
+ export default function Callback() {
311
+ return (
312
+ <CallbackPage
313
+ config={config}
314
+ redirectTo="/dashboard"
315
+ loadingComponent={
316
+ <div className="flex flex-col items-center justify-center min-h-screen">
317
+ <Spinner size="lg" />
318
+ <p className="mt-4 text-gray-600">Completing sign in...</p>
319
+ </div>
320
+ }
321
+ errorComponent={(error) => (
322
+ <div className="flex items-center justify-center min-h-screen">
323
+ <ErrorCard
324
+ title="Authentication Failed"
325
+ message={error}
326
+ action={{ label: "Try Again", href: "/login" }}
327
+ />
328
+ </div>
329
+ )}
330
+ />
331
+ );
332
+ }
333
+ ```
334
+
335
+ #### Props
336
+
337
+ | Prop | Type | Required | Description |
338
+ |------|------|----------|-------------|
339
+ | `config` | `object` | Yes | Authentication configuration (same as provider) |
340
+ | `redirectTo` | `string \| ((user) => string)` | No | Redirect destination after login (default: `"/"`) |
341
+ | `loadingComponent` | `ReactElement` | No | Custom loading component |
342
+ | `errorComponent` | `(error: string) => ReactElement` | No | Custom error component |
343
+ | `onSuccess` | `(user) => void \| Promise<void>` | No | Success callback (runs before redirect) |
344
+ | `onError` | `(error: string) => void` | No | Error callback (runs before error UI shows) |
345
+
346
+ ## Hooks
347
+
348
+ This package provides hooks for different authentication needs:
349
+
350
+ | Hook | Use Case |
351
+ |------|----------|
352
+ | `useImmutableAuth` | Full auth state and methods (sign in, sign out, get tokens) |
353
+ | `useAccessToken` | Just need to make authenticated API calls |
354
+ | `useHydratedData` | Display SSR-fetched data with client-side fallback |
355
+
356
+ ### `useImmutableAuth()`
357
+
358
+ **Use case:** The main hook for authentication. Use this when you need to check auth state, trigger sign in/out, or make authenticated API calls.
359
+
360
+ **When to use:**
361
+ - Login/logout buttons
362
+ - Displaying user information
363
+ - Conditionally rendering content based on auth state
364
+ - Making authenticated API calls from client components
365
+
366
+ ```tsx
367
+ // components/Header.tsx
368
+ // Use case: Navigation header with login/logout and user info
369
+ "use client";
370
+
371
+ import { useImmutableAuth } from "@imtbl/auth-next-client";
372
+
373
+ export function Header() {
374
+ const {
375
+ user, // User profile (sub, email, nickname)
376
+ isLoading, // True during initial session fetch
377
+ isLoggingIn, // True while popup is open
378
+ isAuthenticated,// True when user is logged in
379
+ signIn, // Opens login popup
380
+ signOut, // Signs out from both Auth.js and Immutable
381
+ getAccessToken, // Returns valid token (refreshes if needed)
382
+ } = useImmutableAuth();
383
+
384
+ if (isLoading) {
385
+ return <HeaderSkeleton />;
386
+ }
387
+
388
+ if (isAuthenticated) {
389
+ return (
390
+ <header>
391
+ <span>Welcome, {user?.email || user?.sub}</span>
392
+ <button onClick={() => signOut()}>Sign Out</button>
393
+ </header>
394
+ );
395
+ }
396
+
397
+ return (
398
+ <header>
399
+ <button onClick={() => signIn()} disabled={isLoggingIn}>
400
+ {isLoggingIn ? "Signing in..." : "Sign In with Immutable"}
401
+ </button>
402
+ </header>
403
+ );
404
+ }
405
+ ```
406
+
407
+ #### Making Authenticated API Calls
408
+
409
+ ```tsx
410
+ // components/InventoryButton.tsx
411
+ // Use case: Fetch user data from your API using the access token
412
+ "use client";
413
+
414
+ import { useImmutableAuth } from "@imtbl/auth-next-client";
415
+ import { useState } from "react";
416
+
417
+ export function InventoryButton() {
418
+ const { getAccessToken, isAuthenticated } = useImmutableAuth();
419
+ const [inventory, setInventory] = useState(null);
420
+
421
+ const fetchInventory = async () => {
422
+ // getAccessToken() automatically refreshes expired tokens
423
+ const token = await getAccessToken();
424
+ const response = await fetch("/api/user/inventory", {
425
+ headers: { Authorization: `Bearer ${token}` },
426
+ });
427
+ setInventory(await response.json());
428
+ };
429
+
430
+ if (!isAuthenticated) return null;
431
+
432
+ return (
433
+ <button onClick={fetchInventory}>
434
+ Load My Inventory
435
+ </button>
436
+ );
437
+ }
438
+ ```
439
+
440
+ #### Return Value
441
+
442
+ | Property | Type | Description |
443
+ |----------|------|-------------|
444
+ | `user` | `ImmutableUserClient \| null` | Current user profile |
445
+ | `session` | `Session \| null` | Full Auth.js session with tokens |
446
+ | `isLoading` | `boolean` | Whether initial auth state is loading |
447
+ | `isLoggingIn` | `boolean` | Whether a login flow is in progress |
448
+ | `isAuthenticated` | `boolean` | Whether user is authenticated |
449
+ | `signIn` | `(options?) => Promise<void>` | Start sign-in flow (opens popup) |
450
+ | `signOut` | `() => Promise<void>` | Sign out from both Auth.js and Immutable |
451
+ | `getAccessToken` | `() => Promise<string>` | Get valid access token (refreshes if needed) |
452
+ | `auth` | `Auth \| null` | Underlying `@imtbl/auth` instance for advanced use |
453
+
454
+ #### Sign-In Options
455
+
456
+ ```tsx
457
+ signIn({
458
+ useCachedSession: true, // Try to use cached session first
459
+ // Additional options from @imtbl/auth LoginOptions
460
+ });
461
+ ```
462
+
463
+ ### `useAccessToken()`
464
+
465
+ **Use case:** A simpler hook when you only need to make authenticated API calls and don't need the full auth state.
466
+
467
+ **When to use:**
468
+ - Components that only make API calls (no login UI)
469
+ - When you want to keep the component focused on its domain logic
470
+ - Utility hooks or functions that need to fetch authenticated data
471
+
472
+ ```tsx
473
+ // hooks/useUserAssets.ts
474
+ // Use case: Custom hook that fetches user's NFT assets
475
+ "use client";
476
+
477
+ import { useAccessToken } from "@imtbl/auth-next-client";
478
+ import { useState, useEffect } from "react";
479
+
480
+ export function useUserAssets() {
481
+ const getAccessToken = useAccessToken();
482
+ const [assets, setAssets] = useState([]);
483
+ const [loading, setLoading] = useState(true);
484
+
485
+ useEffect(() => {
486
+ async function fetchAssets() {
487
+ try {
488
+ const token = await getAccessToken();
489
+ const response = await fetch("/api/assets", {
490
+ headers: { Authorization: `Bearer ${token}` },
491
+ });
492
+ setAssets(await response.json());
493
+ } finally {
494
+ setLoading(false);
495
+ }
496
+ }
497
+ fetchAssets();
498
+ }, [getAccessToken]);
499
+
500
+ return { assets, loading };
501
+ }
502
+ ```
503
+
504
+ ### `useHydratedData(props, fetcher)`
505
+
506
+ **Use case:** Display data that was fetched server-side (SSR), with automatic client-side fallback when SSR was skipped (e.g., token was expired).
507
+
508
+ **When to use:**
509
+ - Client Components that receive data from `getAuthenticatedData` (server-side)
510
+ - Pages that benefit from SSR but need client fallback for token refresh
511
+ - When you want seamless SSR → CSR transitions without flash of loading states
512
+
513
+ **When NOT to use:**
514
+ - Components that only fetch client-side (use `useImmutableAuth().getAccessToken()` instead)
515
+ - Components that don't receive server-fetched props
516
+
517
+ **How it works:**
518
+ 1. Server uses `getAuthenticatedData` to fetch data (if token valid) or skip (if expired)
519
+ 2. Server passes result (`{ data, ssr, session }`) to Client Component
520
+ 3. Client uses `useHydratedData` to either use SSR data immediately OR fetch client-side
521
+
522
+ ```tsx
523
+ // app/profile/page.tsx (Server Component)
524
+ // Use case: Profile page with SSR data fetching
525
+ import { auth } from "@/lib/auth";
526
+ import { getAuthenticatedData } from "@imtbl/auth-next-server";
527
+ import { ProfileClient } from "./ProfileClient";
528
+
529
+ export default async function ProfilePage() {
530
+ // Server fetches data if token is valid, skips if expired
531
+ const result = await getAuthenticatedData(auth, async (token) => {
532
+ return fetchProfile(token);
533
+ });
534
+
535
+ // Pass the result to the Client Component
536
+ return <ProfileClient {...result} />;
537
+ }
538
+ ```
539
+
540
+ ```tsx
541
+ // app/profile/ProfileClient.tsx (Client Component)
542
+ // Use case: Display profile with SSR data or client-side fallback
543
+ "use client";
544
+
545
+ import { useHydratedData, type AuthPropsWithData } from "@imtbl/auth-next-client";
546
+
547
+ interface Profile {
548
+ name: string;
549
+ email: string;
550
+ avatarUrl: string;
551
+ }
552
+
553
+ export function ProfileClient(props: AuthPropsWithData<Profile>) {
554
+ // useHydratedData handles both cases:
555
+ // - If ssr=true: uses data immediately (no loading state)
556
+ // - If ssr=false: refreshes token and fetches client-side
557
+ const { data, isLoading, error, refetch } = useHydratedData(
558
+ props,
559
+ async (token) => fetchProfile(token) // Same fetcher as server
560
+ );
561
+
562
+ // Only shows loading state when client-side fetch is happening
563
+ if (isLoading) return <ProfileSkeleton />;
564
+ if (error) return <div>Error: {error.message}</div>;
565
+ if (!data) return <div>No profile found</div>;
566
+
567
+ return (
568
+ <div>
569
+ <img src={data.avatarUrl} alt={data.name} />
570
+ <h1>{data.name}</h1>
571
+ <p>{data.email}</p>
572
+ <button onClick={refetch}>Refresh</button>
573
+ </div>
574
+ );
575
+ }
576
+ ```
577
+
578
+ #### The SSR/CSR Flow Explained
579
+
580
+ | Scenario | Server | Client | User Experience |
581
+ |----------|--------|--------|-----------------|
582
+ | Token valid | Fetches data, `ssr: true` | Uses data immediately | Instant content (SSR) |
583
+ | Token expired | Skips fetch, `ssr: false` | Refreshes token, fetches | Brief loading, then content |
584
+ | Server fetch fails | Returns `fetchError` | Retries automatically | Brief loading, then content |
585
+
586
+ #### Return Value
587
+
588
+ | Property | Type | Description |
589
+ |----------|------|-------------|
590
+ | `data` | `T \| null` | The fetched data |
591
+ | `isLoading` | `boolean` | Whether data is being fetched |
592
+ | `error` | `Error \| null` | Fetch error if any |
593
+ | `refetch` | `() => Promise<void>` | Function to refetch data |
594
+
595
+ ## Choosing the Right Data Fetching Pattern
596
+
597
+ | Pattern | Server Fetches | When to Use |
598
+ |---------|---------------|-------------|
599
+ | `getAuthProps` + `getAccessToken()` | No | Client-only fetching (infinite scroll, real-time, full control) |
600
+ | `getAuthenticatedData` + `useHydratedData` | Yes | SSR with client fallback (best performance + reliability) |
601
+ | Client-only with `getAccessToken()` | No | Simple components, non-critical data |
602
+
603
+ ### Decision Guide
604
+
605
+ **Use SSR pattern (`getAuthenticatedData` + `useHydratedData`) when:**
606
+ - Page benefits from fast initial load (user profile, settings, inventory)
607
+ - SEO matters (public profile pages with auth-dependent content)
608
+ - You want the best user experience (no loading flash for authenticated users)
609
+
610
+ **Use client-only pattern (`getAccessToken()`) when:**
611
+ - Data changes frequently (real-time updates, notifications)
612
+ - Infinite scroll or pagination
613
+ - Non-critical secondary data (recommendations, suggestions)
614
+ - Simple components where SSR complexity isn't worth it
615
+
616
+ ## Types
617
+
618
+ ### User Types
619
+
620
+ ```typescript
621
+ interface ImmutableUserClient {
622
+ sub: string; // Immutable user ID
623
+ email?: string;
624
+ nickname?: string;
625
+ }
626
+
627
+ interface ZkEvmInfo {
628
+ ethAddress: string;
629
+ userAdminAddress: string;
630
+ }
631
+ ```
632
+
633
+ ### Props Types
634
+
635
+ ```typescript
636
+ // From server for passing to client components
637
+ interface AuthProps {
638
+ session: Session | null;
639
+ ssr: boolean;
640
+ authError?: string;
641
+ }
642
+
643
+ interface AuthPropsWithData<T> extends AuthProps {
644
+ data: T | null;
645
+ fetchError?: string;
646
+ }
647
+ ```
648
+
649
+ ### Re-exported Types
650
+
651
+ For convenience, common types are re-exported from `@imtbl/auth-next-server`:
652
+
653
+ ```typescript
654
+ import type {
655
+ ImmutableAuthConfig,
656
+ ImmutableTokenData,
657
+ ImmutableUser,
658
+ AuthProps,
659
+ AuthPropsWithData,
660
+ ProtectedAuthProps,
661
+ ProtectedAuthPropsWithData,
662
+ } from "@imtbl/auth-next-client";
663
+ ```
664
+
665
+ ## Advanced Usage
666
+
667
+ ### Multiple Environments
668
+
669
+ Support multiple Immutable environments (dev, sandbox, production):
670
+
671
+ ```tsx
672
+ // lib/auth-config.ts
673
+ export function getAuthConfig(env: "dev" | "sandbox" | "production") {
674
+ const configs = {
675
+ dev: {
676
+ clientId: "dev-client-id",
677
+ authenticationDomain: "https://auth.dev.immutable.com",
678
+ },
679
+ sandbox: {
680
+ clientId: "sandbox-client-id",
681
+ authenticationDomain: "https://auth.immutable.com",
682
+ },
683
+ production: {
684
+ clientId: "prod-client-id",
685
+ authenticationDomain: "https://auth.immutable.com",
686
+ },
687
+ };
688
+
689
+ return {
690
+ ...configs[env],
691
+ redirectUri: `${window.location.origin}/callback`,
692
+ };
693
+ }
694
+ ```
695
+
696
+ ```tsx
697
+ // app/providers.tsx
698
+ "use client";
699
+
700
+ import { ImmutableAuthProvider } from "@imtbl/auth-next-client";
701
+ import { getAuthConfig } from "@/lib/auth-config";
702
+
703
+ export function Providers({ children, environment }: {
704
+ children: React.ReactNode;
705
+ environment: "dev" | "sandbox" | "production";
706
+ }) {
707
+ const config = getAuthConfig(environment);
708
+ const basePath = `/api/auth/${environment}`;
709
+
710
+ return (
711
+ <ImmutableAuthProvider config={config} basePath={basePath}>
712
+ {children}
713
+ </ImmutableAuthProvider>
714
+ );
715
+ }
716
+ ```
717
+
718
+ ### Accessing the Auth Instance
719
+
720
+ For advanced use cases, you can access the underlying `@imtbl/auth` instance:
721
+
722
+ ```tsx
723
+ import { useImmutableAuth } from "@imtbl/auth-next-client";
724
+
725
+ function AdvancedComponent() {
726
+ const { auth } = useImmutableAuth();
727
+
728
+ const handleAdvanced = async () => {
729
+ if (auth) {
730
+ // Direct access to @imtbl/auth methods
731
+ const user = await auth.getUser();
732
+ const idToken = await auth.getIdToken();
733
+ }
734
+ };
735
+ }
736
+ ```
737
+
738
+ ### Token Refresh Events
739
+
740
+ The provider automatically handles token refresh events and syncs them to the Auth.js session. You can observe these by watching the session:
741
+
742
+ ```tsx
743
+ import { useImmutableAuth } from "@imtbl/auth-next-client";
744
+
745
+ function TokenMonitor() {
746
+ const { session } = useImmutableAuth();
747
+
748
+ useEffect(() => {
749
+ if (session?.accessToken) {
750
+ console.log("Token updated:", session.accessTokenExpires);
751
+ }
752
+ }, [session?.accessToken]);
753
+ }
754
+ ```
755
+
756
+ ## Error Handling
757
+
758
+ The `session.error` field indicates authentication issues:
759
+
760
+ | Error | Description | Handling |
761
+ |-------|-------------|----------|
762
+ | `"TokenExpired"` | Access token expired | `getAccessToken()` will auto-refresh |
763
+ | `"RefreshTokenError"` | Refresh token invalid | Prompt user to sign in again |
764
+
765
+ ```tsx
766
+ import { useImmutableAuth } from "@imtbl/auth-next-client";
767
+
768
+ function ProtectedContent() {
769
+ const { session, signIn, isAuthenticated } = useImmutableAuth();
770
+
771
+ if (session?.error === "RefreshTokenError") {
772
+ return (
773
+ <div>
774
+ <p>Your session has expired. Please sign in again.</p>
775
+ <button onClick={() => signIn()}>Sign In</button>
776
+ </div>
777
+ );
778
+ }
779
+
780
+ if (!isAuthenticated) {
781
+ return <div>Please sign in to continue.</div>;
782
+ }
783
+
784
+ return <div>Protected content here</div>;
785
+ }
786
+ ```
787
+
788
+ ## Related Packages
789
+
790
+ - [`@imtbl/auth-next-server`](../auth-next-server) - Server-side authentication utilities
791
+ - [`@imtbl/auth`](../auth) - Core authentication library
792
+
793
+ ## License
794
+
795
+ Apache-2.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imtbl/auth-next-client",
3
- "version": "2.12.5-alpha.13",
3
+ "version": "2.12.5-alpha.15",
4
4
  "description": "Immutable Auth.js v5 integration for Next.js - Client-side components",
5
5
  "author": "Immutable",
6
6
  "license": "Apache-2.0",
@@ -11,7 +11,7 @@
11
11
  "type": "module",
12
12
  "main": "./dist/node/index.cjs",
13
13
  "module": "./dist/node/index.js",
14
- "types": "./dist/node/index.d.ts",
14
+ "types": "./dist/types/index.d.ts",
15
15
  "exports": {
16
16
  ".": {
17
17
  "development": {
@@ -20,15 +20,15 @@
20
20
  "default": "./dist/node/index.js"
21
21
  },
22
22
  "default": {
23
- "types": "./dist/node/index.d.ts",
23
+ "types": "./dist/types/index.d.ts",
24
24
  "require": "./dist/node/index.cjs",
25
25
  "default": "./dist/node/index.js"
26
26
  }
27
27
  }
28
28
  },
29
29
  "dependencies": {
30
- "@imtbl/auth": "2.12.5-alpha.13",
31
- "@imtbl/auth-next-server": "2.12.5-alpha.13"
30
+ "@imtbl/auth": "2.12.5-alpha.15",
31
+ "@imtbl/auth-next-server": "2.12.5-alpha.15"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "next": "^14.2.0 || ^15.0.0",
@@ -61,10 +61,12 @@
61
61
  "typescript": "^5.6.2"
62
62
  },
63
63
  "scripts": {
64
- "build": "tsup && pnpm build:types",
65
- "build:types": "tsc --project tsconfig.types.json",
66
- "clean": "rm -rf dist",
67
- "lint": "eslint src/**/*.{ts,tsx} --max-warnings=0",
64
+ "build": "pnpm transpile && pnpm typegen",
65
+ "transpile": "tsup src/index.ts --config ./tsup.config.ts",
66
+ "typegen": "tsc --customConditions default --emitDeclarationOnly --outDir dist/types",
67
+ "pack:root": "pnpm pack --pack-destination $(dirname $(pnpm root -w))",
68
+ "lint": "eslint ./src --ext .ts,.jsx,.tsx --max-warnings=0",
69
+ "typecheck": "tsc --customConditions default --noEmit --jsx preserve",
68
70
  "test": "jest --passWithNoTests"
69
71
  }
70
72
  }
package/tsup.config.ts CHANGED
@@ -1,15 +1,6 @@
1
1
  import { defineConfig, type Options } from "tsup";
2
2
 
3
- // Peer dependencies that should never be bundled
4
- const peerExternal = [
5
- "react",
6
- "next",
7
- "next-auth",
8
- "next/navigation",
9
- "next/headers",
10
- "next/server",
11
- ];
12
-
3
+ // Base configuration shared across all builds
13
4
  const baseConfig: Options = {
14
5
  outDir: "dist/node",
15
6
  format: ["esm", "cjs"],
@@ -19,15 +10,15 @@ const baseConfig: Options = {
19
10
  };
20
11
 
21
12
  export default defineConfig([
13
+ // Client-side entry (needs 'use client' directive for Next.js)
22
14
  {
23
15
  ...baseConfig,
24
16
  entry: {
25
- index: "src/index.ts",
17
+ index: "src/index.ts",
26
18
  },
27
- external: peerExternal,
28
- clean: true,
19
+ clean: false, // Don't clean since server build runs first
29
20
  banner: {
30
21
  js: "'use client';",
31
22
  },
32
23
  },
33
- ]);
24
+ ]);
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes