@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 +795 -0
- package/package.json +11 -9
- package/tsup.config.ts +5 -14
- /package/dist/{node → types}/callback.d.ts +0 -0
- /package/dist/{node → types}/constants.d.ts +0 -0
- /package/dist/{node → types}/index.d.ts +0 -0
- /package/dist/{node → types}/provider.d.ts +0 -0
- /package/dist/{node → types}/types.d.ts +0 -0
- /package/dist/{node → types}/utils/token.d.ts +0 -0
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.
|
|
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/
|
|
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/
|
|
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.
|
|
31
|
-
"@imtbl/auth-next-server": "2.12.5-alpha.
|
|
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": "
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
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
|
-
//
|
|
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
|
-
|
|
17
|
+
index: "src/index.ts",
|
|
26
18
|
},
|
|
27
|
-
|
|
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
|