azirid-react 0.6.0

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,1311 @@
1
+ # azirid-react
2
+
3
+ Authentication components and hooks for React and Next.js — powered by [TanStack Query](https://tanstack.com/query) and [Zod](https://zod.dev).
4
+
5
+ Drop-in `<LoginForm>`, `<SignupForm>` and more, **or** use the headless hooks to build fully custom UIs.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install azirid-react
11
+ # or
12
+ pnpm add azirid-react
13
+ # or
14
+ yarn add azirid-react
15
+ ```
16
+
17
+ ### Peer dependencies
18
+
19
+ ```bash
20
+ npm install react react-dom @tanstack/react-query
21
+ # Tailwind CSS is optional – only needed if you use the built-in components
22
+ ```
23
+
24
+ ---
25
+
26
+ ## Architecture: Proxy vs Direct Mode
27
+
28
+ `azirid-react` supports two connection modes to the Azirid API. Choose the one that fits your stack:
29
+
30
+ | | Proxy mode | Direct mode |
31
+ | --- | --- | --- |
32
+ | **Best for** | Next.js (App Router) | React SPA (Vite, CRA, Remix) |
33
+ | **Security** | First-party cookies, no CORS | Requires CORS on API |
34
+ | **Setup** | Route handler + Provider | Provider only |
35
+ | **How it works** | Browser → your app `/api/auth/*` → Azirid API | Browser → Azirid API directly |
36
+
37
+ ### Proxy mode (recommended for Next.js)
38
+
39
+ Requests go to your Next.js app's `/api/auth/*` route handler, which securely proxies them to the Azirid API. Cookies are first-party (same domain), so no CORS configuration is needed.
40
+
41
+ ```tsx
42
+ // No apiUrl prop → proxy mode is activated automatically
43
+ <AziridProvider publishableKey="pk_live_...">
44
+ ```
45
+
46
+ ### Direct mode (for React SPA / Vite)
47
+
48
+ Requests go directly to the Azirid API. Requires CORS to be configured on the API server.
49
+
50
+ ```tsx
51
+ // apiUrl prop → direct mode
52
+ <AziridProvider apiUrl="https://api.azirid.com" publishableKey="pk_live_...">
53
+ ```
54
+
55
+ ---
56
+
57
+ ## Quick Start — Next.js (Proxy Mode)
58
+
59
+ ### 1. Create the route handler
60
+
61
+ ```ts
62
+ // app/api/auth/[...path]/route.ts
63
+ export { GET, POST, PUT, PATCH, DELETE } from 'azirid-react/next'
64
+ ```
65
+
66
+ That's it. The SDK handles proxy logic, cookie fixing, and header forwarding internally. Works with Next.js 14, 15, and 16+ automatically.
67
+
68
+ ### 2. Set the API URL (optional)
69
+
70
+ ```env
71
+ # .env (server-side only — never exposed to the browser)
72
+ # Default: https://api.azirid.com
73
+ # For local development with the API running locally:
74
+ AZIRID_API_URL=http://localhost:3000
75
+ ```
76
+
77
+ ### 3. Wrap your app with `<AziridProvider>`
78
+
79
+ ```tsx
80
+ // app/layout.tsx
81
+ import { AziridProvider } from 'azirid-react'
82
+
83
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
84
+ return (
85
+ <AziridProvider
86
+ publishableKey={process.env.NEXT_PUBLIC_AZIRID_PK!}
87
+ onLoginSuccess={(data) => console.log('Logged in:', data.user)}
88
+ onLogoutSuccess={() => console.log('Logged out')}
89
+ onSessionExpired={() => (window.location.href = '/login')}
90
+ >
91
+ {children}
92
+ </AziridProvider>
93
+ )
94
+ }
95
+ ```
96
+
97
+ ### 4. Add the login page
98
+
99
+ ```tsx
100
+ // app/login/page.tsx
101
+ import { LoginForm } from 'azirid-react'
102
+
103
+ export default function LoginPage() {
104
+ return <LoginForm />
105
+ }
106
+ ```
107
+
108
+ ---
109
+
110
+ ## Quick Start — React SPA (Direct Mode)
111
+
112
+ ### 1. Wrap your app with `<AziridProvider>`
113
+
114
+ ```tsx
115
+ // main.tsx (Vite / CRA)
116
+ import { AziridProvider } from 'azirid-react'
117
+
118
+ createRoot(document.getElementById('root')!).render(
119
+ <AziridProvider
120
+ apiUrl={import.meta.env.VITE_AZIRID_API_URL || 'https://api.azirid.com'}
121
+ publishableKey={import.meta.env.VITE_AZIRID_PK}
122
+ onLoginSuccess={(data) => console.log('Logged in:', data.user)}
123
+ >
124
+ <App />
125
+ </AziridProvider>,
126
+ )
127
+ ```
128
+
129
+ ### 2. Configure your environment
130
+
131
+ ```env
132
+ # .env
133
+ VITE_AZIRID_API_URL=https://api.azirid.com
134
+ VITE_AZIRID_PK=pk_live_...
135
+ ```
136
+
137
+ ### 3. Use the forms or hooks
138
+
139
+ ```tsx
140
+ import { LoginForm } from 'azirid-react'
141
+
142
+ export default function LoginPage() {
143
+ return <LoginForm />
144
+ }
145
+ ```
146
+
147
+ No route handler or proxy needed — requests go directly to the API.
148
+
149
+ ---
150
+
151
+ ## Headless hooks
152
+
153
+ All hooks require `<AziridProvider>` in the tree.
154
+
155
+ ### `useAzirid` — session state
156
+
157
+ ```tsx
158
+ import { useAzirid } from 'azirid-react'
159
+
160
+ function Navbar() {
161
+ const { user, isAuthenticated, isLoading, login, logout } = useAzirid()
162
+
163
+ if (isLoading) return <Spinner />
164
+
165
+ return isAuthenticated ? (
166
+ <div>
167
+ <span>Hello, {user!.email}</span>
168
+ <button onClick={logout}>Sign out</button>
169
+ </div>
170
+ ) : (
171
+ <button onClick={() => login({ email: '...', password: '...' })}>Sign in</button>
172
+ )
173
+ }
174
+ ```
175
+
176
+ ### `useLogin`
177
+
178
+ ```tsx
179
+ import { useLogin } from 'azirid-react'
180
+
181
+ function CustomLoginForm() {
182
+ const { login, isLoading, error } = useLogin({
183
+ onSuccess: (data) => console.log(data.user),
184
+ onError: (msg) => console.error(msg),
185
+ })
186
+
187
+ return (
188
+ <form
189
+ onSubmit={(e) => {
190
+ e.preventDefault()
191
+ const fd = new FormData(e.currentTarget)
192
+ login({
193
+ email: fd.get('email') as string,
194
+ password: fd.get('password') as string,
195
+ })
196
+ }}
197
+ >
198
+ <input name="email" type="email" />
199
+ <input name="password" type="password" />
200
+ {error && <p>{error}</p>}
201
+ <button disabled={isLoading}>Sign in</button>
202
+ </form>
203
+ )
204
+ }
205
+ ```
206
+
207
+ ### `useSignup`
208
+
209
+ ```tsx
210
+ import { useSignup } from 'azirid-react'
211
+
212
+ const { signup, isLoading, error } = useSignup({
213
+ onSuccess: (data) => console.log('Registered:', data.user),
214
+ })
215
+
216
+ signup({ email: 'user@example.com', password: 'secret' })
217
+ ```
218
+
219
+ ### `useLogout`
220
+
221
+ ```tsx
222
+ import { useLogout } from 'azirid-react'
223
+
224
+ const { logout, isLoading } = useLogout({
225
+ onSuccess: () => router.push('/login'),
226
+ })
227
+ ```
228
+
229
+ ### `useSession`
230
+
231
+ ```tsx
232
+ import { useSession } from 'azirid-react'
233
+
234
+ const { user, accessToken, isAuthenticated } = useSession()
235
+ ```
236
+
237
+ ### `useMagicLink`
238
+
239
+ ```tsx
240
+ import { useMagicLink } from 'azirid-react'
241
+
242
+ const { requestMagicLink, verifyMagicLink, isLoading } = useMagicLink()
243
+
244
+ requestMagicLink({ email: 'user@example.com' })
245
+ verifyMagicLink({ token: '...' })
246
+ ```
247
+
248
+ ### `useSocialLogin`
249
+
250
+ ```tsx
251
+ import { useSocialLogin } from 'azirid-react'
252
+
253
+ const { loginWithProvider, isLoading } = useSocialLogin()
254
+
255
+ loginWithProvider({ provider: 'google' }) // "google" | "github"
256
+ ```
257
+
258
+ ### `usePasskeys`
259
+
260
+ ```tsx
261
+ import { usePasskeys } from 'azirid-react'
262
+
263
+ const { passkeys, registerPasskey, removePasskey, isLoading } = usePasskeys()
264
+ ```
265
+
266
+ ### `useChangePassword`
267
+
268
+ ```tsx
269
+ import { useChangePassword } from 'azirid-react'
270
+
271
+ const { changePassword, isLoading, error } = useChangePassword()
272
+
273
+ changePassword({ currentPassword: 'old', newPassword: 'new' })
274
+ ```
275
+
276
+ ### `useBootstrap`
277
+
278
+ Manually re-run the session bootstrap (useful after SSO redirects).
279
+
280
+ ```tsx
281
+ import { useBootstrap } from 'azirid-react'
282
+
283
+ const { bootstrap, isBootstrapping } = useBootstrap()
284
+ ```
285
+
286
+ ### `useRefresh`
287
+
288
+ Manually refresh the access token.
289
+
290
+ ```tsx
291
+ import { useRefresh } from 'azirid-react'
292
+
293
+ const { refresh } = useRefresh()
294
+ ```
295
+
296
+ ### `useAziridClient`
297
+
298
+ Access the raw `AccessClient` instance for custom API calls.
299
+
300
+ ```tsx
301
+ import { useAziridClient } from 'azirid-react'
302
+
303
+ function CustomAction() {
304
+ const client = useAziridClient()
305
+
306
+ async function fetchCustomData() {
307
+ const data = await client.get('/v1/custom-endpoint')
308
+ console.log(data)
309
+ }
310
+
311
+ return <button onClick={fetchCustomData}>Fetch</button>
312
+ }
313
+ ```
314
+
315
+ ### `useFormState`
316
+
317
+ Headless form hook with Zod validation. Powers the built-in form components — use it to build fully custom forms.
318
+
319
+ ```tsx
320
+ import { useFormState, loginSchema } from 'azirid-react'
321
+
322
+ function CustomForm() {
323
+ const { values, errors, isSubmitting, handleChange, handleSubmit, reset } = useFormState(
324
+ { email: '', password: '' },
325
+ loginSchema,
326
+ async (values) => {
327
+ // Submit logic
328
+ },
329
+ )
330
+
331
+ return (
332
+ <form onSubmit={handleSubmit}>
333
+ <input value={values.email} onChange={handleChange('email')} />
334
+ {errors.find((e) => e.field === 'email')?.message}
335
+ <button disabled={isSubmitting}>Submit</button>
336
+ </form>
337
+ )
338
+ }
339
+ ```
340
+
341
+ ### `usePasswordToggle`
342
+
343
+ Simple toggle between `"password"` and `"text"` input types.
344
+
345
+ ```tsx
346
+ import { usePasswordToggle } from 'azirid-react'
347
+
348
+ function PasswordInput() {
349
+ const { visible, toggle, type } = usePasswordToggle()
350
+
351
+ return (
352
+ <div>
353
+ <input type={type} name="password" />
354
+ <button type="button" onClick={toggle}>
355
+ {visible ? 'Hide' : 'Show'}
356
+ </button>
357
+ </div>
358
+ )
359
+ }
360
+ ```
361
+
362
+ ### Zod Schemas
363
+
364
+ The SDK exports pre-built Zod schemas with Spanish validation messages, plus factory functions for custom messages.
365
+
366
+ ```tsx
367
+ import {
368
+ loginSchema,
369
+ signupSchema,
370
+ changePasswordSchema,
371
+ magicLinkRequestSchema,
372
+ magicLinkVerifySchema,
373
+ socialLoginSchema,
374
+ passkeyRegisterStartSchema,
375
+ } from 'azirid-react'
376
+
377
+ // Default schemas (Spanish messages)
378
+ loginSchema.parse({ email: 'user@example.com', password: 'secret' })
379
+
380
+ // Factory functions for custom messages
381
+ import { createLoginSchema, createSignupSchema } from 'azirid-react'
382
+
383
+ const customSchema = createLoginSchema({
384
+ emailRequired: 'Email is required',
385
+ emailInvalid: 'Must be a valid email',
386
+ passwordRequired: 'Password is required',
387
+ passwordMinLength: 'At least 10 characters',
388
+ })
389
+ ```
390
+
391
+ | Schema | Fields |
392
+ | --- | --- |
393
+ | `loginSchema` | `email`, `password` |
394
+ | `signupSchema` | `email`, `password`, `acceptedTosVersion?`, `acceptedPrivacyVersion?` |
395
+ | `changePasswordSchema` | `currentPassword`, `newPassword` |
396
+ | `magicLinkRequestSchema` | `email` |
397
+ | `magicLinkVerifySchema` | `token` |
398
+ | `socialLoginSchema` | `provider`, `providerAccountId`, `email`, `emailVerified?`, ... |
399
+ | `passkeyRegisterStartSchema` | `deviceName?` |
400
+
401
+ ---
402
+
403
+ ## Billing & Payments
404
+
405
+ All billing hooks require `<AziridProvider>` in the tree and an authenticated user.
406
+
407
+ ### `usePlans`
408
+
409
+ Fetch all available billing plans for the current app.
410
+
411
+ ```tsx
412
+ import { usePlans } from 'azirid-react'
413
+
414
+ function PricingPage() {
415
+ const { data: plans, isLoading } = usePlans()
416
+
417
+ if (isLoading) return <Spinner />
418
+
419
+ return (
420
+ <ul>
421
+ {plans?.map((plan) => (
422
+ <li key={plan.id}>
423
+ {plan.name} — ${(plan.amount / 100).toFixed(2)}/{plan.interval.toLowerCase()}
424
+ </li>
425
+ ))}
426
+ </ul>
427
+ )
428
+ }
429
+ ```
430
+
431
+ ### `useSubscription`
432
+
433
+ Get the current user's active subscription.
434
+
435
+ ```tsx
436
+ import { useSubscription } from 'azirid-react'
437
+
438
+ function SubscriptionStatus() {
439
+ const { data: sub } = useSubscription()
440
+
441
+ if (!sub) return <p>No active subscription</p>
442
+
443
+ return (
444
+ <p>
445
+ {sub.plan.name} — {sub.status}
446
+ {sub.cancelAtPeriodEnd && ' (cancels at period end)'}
447
+ </p>
448
+ )
449
+ }
450
+ ```
451
+
452
+ ### `useCheckout`
453
+
454
+ Initiate a checkout session. Auto-redirects to the payment provider on success (unless the provider is `MANUAL_TRANSFER` or `PAYPHONE`).
455
+
456
+ ```tsx
457
+ import { useCheckout } from 'azirid-react'
458
+
459
+ function UpgradeButton({ planId }: { planId: string }) {
460
+ const { checkout, isPending } = useCheckout({
461
+ onSuccess: (data) => console.log('Checkout created:', data),
462
+ onError: (err) => console.error(err),
463
+ })
464
+
465
+ return (
466
+ <button
467
+ disabled={isPending}
468
+ onClick={() =>
469
+ checkout({
470
+ planId,
471
+ successUrl: `${window.location.origin}/billing?success=true`,
472
+ cancelUrl: `${window.location.origin}/billing`,
473
+ })
474
+ }
475
+ >
476
+ Upgrade
477
+ </button>
478
+ )
479
+ }
480
+ ```
481
+
482
+ ### `useInvoices`
483
+
484
+ List all invoices for the authenticated user.
485
+
486
+ ```tsx
487
+ import { useInvoices } from 'azirid-react'
488
+
489
+ function InvoiceHistory() {
490
+ const { data: invoices } = useInvoices()
491
+
492
+ return (
493
+ <ul>
494
+ {invoices?.map((inv) => (
495
+ <li key={inv.id}>
496
+ ${(inv.amount / 100).toFixed(2)} — {inv.status}
497
+ {inv.invoiceUrl && <a href={inv.invoiceUrl}>View</a>}
498
+ </li>
499
+ ))}
500
+ </ul>
501
+ )
502
+ }
503
+ ```
504
+
505
+ ### `usePaymentProviders`
506
+
507
+ Fetch available payment providers for the app (e.g., Stripe, PayPal, manual transfer).
508
+
509
+ ```tsx
510
+ import { usePaymentProviders } from 'azirid-react'
511
+
512
+ const { data: providers } = usePaymentProviders()
513
+ // [{ provider: 'STRIPE', checkout: true, subscriptions: true }, ...]
514
+ ```
515
+
516
+ ### `useSubmitTransferProof`
517
+
518
+ Submit proof of a manual bank transfer payment.
519
+
520
+ ```tsx
521
+ import { useSubmitTransferProof } from 'azirid-react'
522
+
523
+ const { submit, isPending } = useSubmitTransferProof({
524
+ onSuccess: (proof) => console.log('Proof submitted:', proof.id),
525
+ })
526
+
527
+ submit({
528
+ planId: 'plan_123',
529
+ fileUrl: 'https://storage.example.com/receipt.pdf',
530
+ amount: 9999,
531
+ currency: 'USD',
532
+ notes: 'Bank transfer from Account #1234',
533
+ })
534
+ ```
535
+
536
+ ### `useTransferProofs`
537
+
538
+ List submitted transfer proofs and their review status.
539
+
540
+ ```tsx
541
+ import { useTransferProofs } from 'azirid-react'
542
+
543
+ const { data: proofs } = useTransferProofs()
544
+ // [{ id, status: 'PENDING_REVIEW' | 'APPROVED' | 'REJECTED', ... }]
545
+ ```
546
+
547
+ ### `usePayphoneConfirm`
548
+
549
+ Confirm a Payphone payment callback. Used on the Payphone return URL page.
550
+
551
+ ```tsx
552
+ import { usePayphoneConfirm } from 'azirid-react'
553
+
554
+ const confirm = usePayphoneConfirm({
555
+ onSuccess: (data) => console.log('Payment confirmed:', data.status),
556
+ })
557
+
558
+ confirm.mutate({ id: 12345, clientTransactionId: 'txn_abc' })
559
+ ```
560
+
561
+ ### Billing Components
562
+
563
+ #### `PricingTable`
564
+
565
+ Drop-in pricing grid with checkout flow integrated.
566
+
567
+ ```tsx
568
+ import { PricingTable } from 'azirid-react'
569
+
570
+ <PricingTable
571
+ successUrl={`${window.location.origin}/billing?success=true`}
572
+ cancelUrl={`${window.location.origin}/billing`}
573
+ columns={3}
574
+ onPlanSelect={(plan) => console.log('Selected:', plan.name)}
575
+ />
576
+ ```
577
+
578
+ | Prop | Type | Default | Description |
579
+ | --- | --- | --- | --- |
580
+ | `successUrl` | `string` | — | **Required.** Redirect URL after successful payment |
581
+ | `cancelUrl` | `string` | — | **Required.** Redirect URL on cancel |
582
+ | `columns` | `number` | `3` | Number of columns in the grid |
583
+ | `onPlanSelect` | `(plan) => void` | — | Called when a plan is selected |
584
+ | `className` | `string` | — | Additional CSS classes |
585
+
586
+ #### `PayButton`
587
+
588
+ Flexible payment button with provider selection modal. Supports both plans and payment intents.
589
+
590
+ ```tsx
591
+ import { PayButton } from 'azirid-react'
592
+
593
+ <PayButton
594
+ planId="plan_123"
595
+ successUrl="/billing?success=true"
596
+ cancelUrl="/billing"
597
+ onSuccess={(data) => console.log('Payment success:', data)}
598
+ >
599
+ Subscribe Now
600
+ </PayButton>
601
+ ```
602
+
603
+ | Prop | Type | Default | Description |
604
+ | --- | --- | --- | --- |
605
+ | `planId` | `string` | — | Plan to purchase (use `planId` or `intentId`) |
606
+ | `intentId` | `string` | — | Payment intent ID (alternative to `planId`) |
607
+ | `successUrl` | `string` | — | **Required.** Redirect URL after success |
608
+ | `cancelUrl` | `string` | — | **Required.** Redirect URL on cancel |
609
+ | `onSuccess` | `(data) => void` | — | Called on successful checkout |
610
+ | `onError` | `(error) => void` | — | Called on error |
611
+ | `children` | `ReactNode` | — | Button label |
612
+ | `disabled` | `boolean` | — | Disable the button |
613
+
614
+ #### `CheckoutButton`
615
+
616
+ Simple checkout button for a specific plan.
617
+
618
+ ```tsx
619
+ import { CheckoutButton } from 'azirid-react'
620
+
621
+ <CheckoutButton
622
+ planId="plan_123"
623
+ successUrl="/billing?success=true"
624
+ cancelUrl="/billing"
625
+ >
626
+ Subscribe
627
+ </CheckoutButton>
628
+ ```
629
+
630
+ #### `SubscriptionBadge`
631
+
632
+ Color-coded badge showing the current subscription status.
633
+
634
+ ```tsx
635
+ import { SubscriptionBadge } from 'azirid-react'
636
+
637
+ <SubscriptionBadge />
638
+ // Renders: "Pro · Active" (green), "Free · Trialing" (blue), etc.
639
+ ```
640
+
641
+ Status colors: ACTIVE (green), TRIALING (blue), PAST_DUE (yellow), CANCELED/UNPAID (red), INCOMPLETE (gray).
642
+
643
+ #### `InvoiceList`
644
+
645
+ Table of invoices with status badges and download links.
646
+
647
+ ```tsx
648
+ import { InvoiceList } from 'azirid-react'
649
+
650
+ <InvoiceList />
651
+ ```
652
+
653
+ #### `PayphoneCallback`
654
+
655
+ Page component for handling Payphone payment callbacks. Reads `id` and `clientTransactionId` from URL query params automatically.
656
+
657
+ ```tsx
658
+ // app/payphone/callback/page.tsx
659
+ import { PayphoneCallback } from 'azirid-react'
660
+
661
+ export default function PayphoneCallbackPage() {
662
+ return (
663
+ <PayphoneCallback
664
+ onSuccess={(data) => console.log('Confirmed:', data.status)}
665
+ onError={(err) => console.error(err)}
666
+ />
667
+ )
668
+ }
669
+ ```
670
+
671
+ ---
672
+
673
+ ## Referrals
674
+
675
+ All referral hooks require `<AziridProvider>` in the tree and an authenticated user.
676
+
677
+ ### `useReferral`
678
+
679
+ Fetch the current user's referral info and copy referral link to clipboard.
680
+
681
+ ```tsx
682
+ import { useReferral } from 'azirid-react'
683
+
684
+ function ReferralSection() {
685
+ const { data, copyToClipboard } = useReferral()
686
+
687
+ if (!data) return null
688
+
689
+ return (
690
+ <div>
691
+ <p>Your referral code: {data.referralCode}</p>
692
+ <input readOnly value={data.referralUrl} />
693
+ <button onClick={copyToClipboard}>Copy Link</button>
694
+ <p>
695
+ {data.completedReferrals} completed / {data.totalReferred} total
696
+ </p>
697
+ </div>
698
+ )
699
+ }
700
+ ```
701
+
702
+ ### `useReferralStats`
703
+
704
+ Fetch detailed referral history with rewards.
705
+
706
+ ```tsx
707
+ import { useReferralStats } from 'azirid-react'
708
+
709
+ function ReferralHistory() {
710
+ const { data } = useReferralStats()
711
+
712
+ return (
713
+ <ul>
714
+ {data?.referrals.map((ref) => (
715
+ <li key={ref.id}>
716
+ {ref.referredEmail} — {ref.status}
717
+ {ref.rewardAmount && ` ($${ref.rewardAmount})`}
718
+ </li>
719
+ ))}
720
+ </ul>
721
+ )
722
+ }
723
+ ```
724
+
725
+ ### Referral Components
726
+
727
+ #### `ReferralCard`
728
+
729
+ Card displaying the referral link with copy button and stats.
730
+
731
+ ```tsx
732
+ import { ReferralCard } from 'azirid-react'
733
+
734
+ <ReferralCard
735
+ title="Refer a Friend"
736
+ description="Share your link and earn rewards for each signup."
737
+ />
738
+ ```
739
+
740
+ | Prop | Type | Default | Description |
741
+ | --- | --- | --- | --- |
742
+ | `title` | `string` | `"Refer a Friend"` | Card title |
743
+ | `description` | `string` | — | Card description |
744
+ | `className` | `string` | — | Additional CSS classes |
745
+
746
+ #### `ReferralStats`
747
+
748
+ Table showing referral history with status and reward badges.
749
+
750
+ ```tsx
751
+ import { ReferralStats } from 'azirid-react'
752
+
753
+ <ReferralStats />
754
+ ```
755
+
756
+ ---
757
+
758
+ ## Internationalization (i18n)
759
+
760
+ Built-in support for **English** and **Spanish**. The SDK ships two complete dictionaries; pass a `locale` prop to switch languages.
761
+
762
+ ```tsx
763
+ import { AziridProvider } from 'azirid-react'
764
+ ;<AziridProvider publishableKey="pk_live_..." locale="en">
765
+ {/* All form labels, validation messages, and UI text render in English */}
766
+ {children}
767
+ </AziridProvider>
768
+ ```
769
+
770
+ ### Supported locales
771
+
772
+ | Locale | Language |
773
+ | ------ | ----------------- |
774
+ | `"es"` | Spanish (default) |
775
+ | `"en"` | English |
776
+
777
+ ### Custom messages
778
+
779
+ Override any string by passing a partial `messages` object:
780
+
781
+ ```tsx
782
+ <AziridProvider
783
+ publishableKey="pk_live_..."
784
+ locale="en"
785
+ messages={{
786
+ login: { title: 'Welcome back!', submit: 'Sign in' },
787
+ validation: { emailRequired: 'Please enter your email' },
788
+ }}
789
+ >
790
+ {children}
791
+ </AziridProvider>
792
+ ```
793
+
794
+ ### Using i18n hooks directly
795
+
796
+ ```tsx
797
+ import { useMessages, useBranding } from 'azirid-react'
798
+
799
+ function CustomForm() {
800
+ const msg = useMessages() // resolved messages for current locale
801
+ return <label>{msg.login.emailLabel}</label>
802
+ }
803
+ ```
804
+
805
+ ### Locale-aware Zod schemas
806
+
807
+ ```tsx
808
+ import { createLoginSchema, createSignupSchema } from 'azirid-react'
809
+
810
+ // Pass custom validation messages
811
+ const schema = createLoginSchema({
812
+ emailRequired: 'Email is required',
813
+ emailInvalid: 'Must be a valid email',
814
+ passwordRequired: 'Password is required',
815
+ passwordMin: 'At least 8 characters',
816
+ })
817
+ ```
818
+
819
+ ---
820
+
821
+ ## Branding
822
+
823
+ The bootstrap endpoint returns branding data configured in the Azirid dashboard (Settings > Branding). The built-in form components automatically apply branding.
824
+
825
+ ### Auto-branding from bootstrap
826
+
827
+ If branding is configured for your app, the forms will automatically:
828
+
829
+ - Show your **logo** (from `branding.logoUrl`) above the form
830
+ - Use your **display name** as the form title
831
+ - Apply your **primary color** to the submit button
832
+ - Show/hide the **"Secured by Azirid"** badge
833
+
834
+ No extra code needed — just configure branding in the dashboard.
835
+
836
+ ### Overriding branding with props
837
+
838
+ Per-component props always take priority over branding context:
839
+
840
+ ```tsx
841
+ <LoginForm
842
+ logo={<MyCustomLogo />} // overrides branding.logoUrl
843
+ title="Sign in to Acme" // overrides branding.displayName
844
+ submitText="Continue"
845
+ />
846
+ ```
847
+
848
+ ### Using branding hooks
849
+
850
+ ```tsx
851
+ import { useBranding } from 'azirid-react'
852
+
853
+ function CustomHeader() {
854
+ const branding = useBranding() // AppBranding | null
855
+
856
+ return (
857
+ <div>
858
+ {branding?.logoUrl && <img src={branding.logoUrl} alt="Logo" />}
859
+ <h1 style={{ color: branding?.primaryColor ?? '#000' }}>
860
+ {branding?.displayName ?? 'My App'}
861
+ </h1>
862
+ </div>
863
+ )
864
+ }
865
+ ```
866
+
867
+ ### "Secured by Azirid" badge
868
+
869
+ The `<SecuredByBadge />` component renders below each form. It's hidden when `branding.removeBranding` is `true` (configurable in the dashboard).
870
+
871
+ ```tsx
872
+ import { SecuredByBadge } from "azirid-react";
873
+
874
+ // Use in custom form layouts
875
+ <form>
876
+ {/* ... your form fields ... */}
877
+ </form>
878
+ <SecuredByBadge />
879
+ ```
880
+
881
+ ---
882
+
883
+ ## createAccessClient
884
+
885
+ Under the hood `AziridProvider` creates an `AccessClient` via `createAccessClient`. You can also create a client directly to make raw API calls.
886
+
887
+ ```ts
888
+ import { createAccessClient, BASE_PATHS } from 'azirid-react'
889
+ import type { AccessClientConfig } from 'azirid-react'
890
+
891
+ // Direct mode — point to the API
892
+ const client = createAccessClient(
893
+ {
894
+ baseUrl: 'https://api.azirid.com',
895
+ basePath: BASE_PATHS.direct, // '/v1/users/auth'
896
+ },
897
+ { publishableKey: 'pk_live_...' },
898
+ )
899
+
900
+ // Set tokens after login
901
+ client.setAccessToken('eyJ...')
902
+ client.setRefreshToken('...')
903
+
904
+ // Make arbitrary authenticated calls
905
+ const data = await client.get(client.paths.me)
906
+ const result = await client.post('/v1/custom-endpoint', { foo: 'bar' })
907
+ ```
908
+
909
+ ### `createAccessClient` signature
910
+
911
+ ```ts
912
+ function createAccessClient(
913
+ config: AccessClientConfig,
914
+ appContext?: { publishableKey: string; tenantId?: string },
915
+ ): AccessClient
916
+ ```
917
+
918
+ | Param | Type | Description |
919
+ | ----------- | -------------------- | ---------------------------------------------------------------- |
920
+ | `config` | `AccessClientConfig` | `{ baseUrl, basePath?, headers? }` |
921
+ | `appContext` | `object` | Optional. `publishableKey` and `tenantId` |
922
+
923
+ ---
924
+
925
+ ## AziridProvider props
926
+
927
+ | Prop | Type | Default | Description |
928
+ | ------------------ | ------------------------- | -------- | ---------------------------------------------------------------------------------------------- |
929
+ | `children` | `ReactNode` | — | **Required.** Your app tree |
930
+ | `apiUrl` | `string` | — | API URL for direct mode. Omit for proxy mode (recommended in Next.js) |
931
+ | `publishableKey` | `string` | — | Publishable key (e.g. `pk_live_...`) |
932
+ | `tenantId` | `string` | — | Tenant ID for multi-tenant apps |
933
+ | `fetchOptions` | `Record<string, string>` | — | Extra headers to send with every request |
934
+ | `autoBootstrap` | `boolean` | `true` | Auto-restore session on mount |
935
+ | `refreshInterval` | `number` | `50000` | Token refresh interval in ms. `0` to disable |
936
+ | `sessionSyncUrl` | `string \| false` | auto | URL for session cookie sync. Auto-activates in dev mode. Pass `false` to disable |
937
+ | `onLoginSuccess` | `(data) => void` | — | Called after successful login |
938
+ | `onSignupSuccess` | `(data) => void` | — | Called after successful signup |
939
+ | `onLogoutSuccess` | `() => void` | — | Called after logout |
940
+ | `onSessionExpired` | `() => void` | — | Called when refresh fails |
941
+ | `onError` | `(msg: string) => void` | — | Called on any auth error |
942
+ | `locale` | `"es" \| "en"` | `"es"` | UI language for built-in forms and validation messages |
943
+ | `messages` | `Partial<AccessMessages>` | — | Override any i18n string (merged on top of the locale dictionary) |
944
+
945
+ ---
946
+
947
+ ## Next.js Integration
948
+
949
+ `azirid-react` supports **Next.js 14, 15, and 16+** with full compatibility for each version's API conventions.
950
+
951
+ ### Proxy Route Handler (all versions)
952
+
953
+ Create the file `app/api/auth/[...path]/route.ts` — one line is all you need:
954
+
955
+ ```ts
956
+ // app/api/auth/[...path]/route.ts
957
+ export { GET, POST, PUT, PATCH, DELETE } from 'azirid-react/next'
958
+ ```
959
+
960
+ That's it. The SDK handles all the proxy logic, cookie fixing, and header forwarding internally.
961
+ It works with Next.js 14, 15, and 16+ automatically.
962
+
963
+ ### Custom API URL or debug logging
964
+
965
+ ```ts
966
+ // app/api/auth/[...path]/route.ts
967
+ import { createAziridRouteHandlers } from 'azirid-react/next'
968
+
969
+ export const { GET, POST, PUT, PATCH, DELETE } = createAziridRouteHandlers({
970
+ apiUrl: 'https://my-custom-api.com',
971
+ debug: true, // logs proxy requests to console
972
+ })
973
+ ```
974
+
975
+ ### Environment variable
976
+
977
+ The proxy reads `AZIRID_API_URL` (server-side only) to know where to forward requests:
978
+
979
+ ```env
980
+ # .env
981
+ # Default: https://api.azirid.com
982
+ # For local development:
983
+ AZIRID_API_URL=http://localhost:3000
984
+ ```
985
+
986
+ > **Important:** Use `AZIRID_API_URL` (without `NEXT_PUBLIC_` prefix) — the API URL should never be exposed to the browser. The proxy runs server-side only.
987
+
988
+ ### Next.js Config
989
+
990
+ #### Next.js 16+ (`next.config.ts`)
991
+
992
+ Turbopack is the default bundler — `transpilePackages` is no longer needed:
993
+
994
+ ```ts
995
+ // next.config.ts
996
+ import type { NextConfig } from 'next'
997
+
998
+ const nextConfig: NextConfig = {}
999
+
1000
+ export default nextConfig
1001
+ ```
1002
+
1003
+ #### Next.js 14/15 (`next.config.js`)
1004
+
1005
+ ```js
1006
+ // next.config.js
1007
+ const { withAziridProxy } = require('azirid-react/next')
1008
+
1009
+ /** @type {import('next').NextConfig} */
1010
+ module.exports = withAziridProxy()({
1011
+ transpilePackages: ['azirid-react'],
1012
+ })
1013
+ ```
1014
+
1015
+ ### Route Protection (optional)
1016
+
1017
+ > **Not required for basic usage.** Only add this if you need to protect specific routes from unauthenticated users.
1018
+
1019
+ #### Next.js 16+ (`proxy.ts`)
1020
+
1021
+ ```ts
1022
+ // proxy.ts — only needed if you want route protection
1023
+ import { createAziridProxy } from 'azirid-react/next'
1024
+
1025
+ export const proxy = createAziridProxy({
1026
+ protectedRoutes: ['/dashboard', '/settings'],
1027
+ loginUrl: '/login',
1028
+ publicRoutes: ['/login', '/signup', '/forgot-password'],
1029
+ })
1030
+
1031
+ export const config = {
1032
+ matcher: ['/((?!_next|favicon.ico|api/).*)'],
1033
+ }
1034
+ ```
1035
+
1036
+ #### Next.js 14/15 (`middleware.ts`)
1037
+
1038
+ ```ts
1039
+ // middleware.ts — only needed if you want route protection
1040
+ import { createAziridMiddleware } from 'azirid-react/next'
1041
+
1042
+ export default createAziridMiddleware({
1043
+ protectedRoutes: ['/dashboard', '/settings'],
1044
+ loginUrl: '/login',
1045
+ publicRoutes: ['/login', '/signup', '/forgot-password'],
1046
+ })
1047
+
1048
+ export const config = {
1049
+ matcher: ['/((?!_next|favicon.ico|api/).*)'],
1050
+ }
1051
+ ```
1052
+
1053
+ ---
1054
+
1055
+ ## Server-side (Next.js App Router)
1056
+
1057
+ For **Server Components**, **Server Actions**, and **Route Handlers**, use the `azirid-react/server` entry point to read the session token from the httpOnly `__session` cookie.
1058
+
1059
+ ### Setup
1060
+
1061
+ ```ts
1062
+ // lib/access-server.ts
1063
+ import { cookies } from 'next/headers'
1064
+ import { createServerAccess } from 'azirid-react/server'
1065
+
1066
+ // Works with all Next.js versions:
1067
+ // - Next.js 14: cookies() returns a sync cookie store
1068
+ // - Next.js 15/16+: cookies() returns a Promise — handled automatically
1069
+ export const { getSessionToken, getAccessToken } = createServerAccess({ cookies })
1070
+ ```
1071
+
1072
+ ### Server Action example
1073
+
1074
+ ```ts
1075
+ // app/actions/profile.ts
1076
+ 'use server'
1077
+ import { getSessionToken } from '@/lib/access-server'
1078
+
1079
+ export async function getProfile() {
1080
+ const token = await getSessionToken()
1081
+ if (!token) throw new Error('Not authenticated')
1082
+
1083
+ const res = await fetch(`${process.env.AZIRID_API_URL}/v1/users/auth/me`, {
1084
+ headers: { Authorization: `Bearer ${token}` },
1085
+ })
1086
+ return res.json()
1087
+ }
1088
+ ```
1089
+
1090
+ ### Server Component example
1091
+
1092
+ ```tsx
1093
+ // app/dashboard/page.tsx
1094
+ import { redirect } from 'next/navigation'
1095
+ import { getSessionToken } from '@/lib/access-server'
1096
+
1097
+ export default async function DashboardPage() {
1098
+ const token = await getSessionToken()
1099
+ if (!token) redirect('/login')
1100
+
1101
+ const res = await fetch(`${process.env.AZIRID_API_URL}/v1/users/auth/me`, {
1102
+ headers: { Authorization: `Bearer ${token}` },
1103
+ })
1104
+ const user = await res.json()
1105
+
1106
+ return <h1>Hello, {user.email}</h1>
1107
+ }
1108
+ ```
1109
+
1110
+ ### Options
1111
+
1112
+ | Option | Type | Default | Description |
1113
+ | ------------ | -------- | ------------- | ------------------------------------------ |
1114
+ | `cookieName` | `string` | `"__session"` | Name of the httpOnly cookie with the token |
1115
+
1116
+ ### `createSessionSyncHandler`
1117
+
1118
+ Creates a route handler that syncs the access token to a local httpOnly cookie. Useful for cross-origin development setups where the API is on a different domain.
1119
+
1120
+ ```ts
1121
+ // app/api/auth/session/route.ts
1122
+ import { createSessionSyncHandler } from 'azirid-react/server'
1123
+
1124
+ export const { POST, DELETE } = createSessionSyncHandler()
1125
+ ```
1126
+
1127
+ With custom options:
1128
+
1129
+ ```ts
1130
+ export const { POST, DELETE } = createSessionSyncHandler({
1131
+ cookieName: '__session', // default
1132
+ secure: true, // set Secure flag on cookie
1133
+ maxAge: 3600, // cookie max age in seconds (default: 1h)
1134
+ })
1135
+ ```
1136
+
1137
+ | Option | Type | Default | Description |
1138
+ | ------------ | --------- | ------------- | ----------------------------------- |
1139
+ | `cookieName` | `string` | `"__session"` | Name of the httpOnly cookie |
1140
+ | `secure` | `boolean` | `false` | Set the `Secure` flag on the cookie |
1141
+ | `maxAge` | `number` | `3600` | Cookie max age in seconds |
1142
+
1143
+ ---
1144
+
1145
+ ## Version Compatibility
1146
+
1147
+ | Feature | Next.js 14 | Next.js 15 | Next.js 16+ |
1148
+ | ------------------- | ---------------- | ------------------- | ------------------- |
1149
+ | React | 18.x | 18.x / 19.x | 19.x+ |
1150
+ | Node.js | >= 18.0.0 | >= 18.17.0 | >= 20.9.0 |
1151
+ | Config file | `next.config.js` | `next.config.js/ts` | `next.config.ts` |
1152
+ | Request interceptor | `middleware.ts` | `middleware.ts` | `proxy.ts` |
1153
+ | `cookies()` | sync | async (with compat) | async only |
1154
+ | `params` | sync | async (with compat) | async only |
1155
+ | Bundler | Webpack | Webpack/Turbopack | Turbopack (default) |
1156
+ | `transpilePackages` | Required | Required | Not needed |
1157
+
1158
+ ---
1159
+
1160
+ ## Tailwind CSS (optional)
1161
+
1162
+ The built-in form components (`LoginForm`, `SignupForm`, etc.) use Tailwind utility classes. Add `azirid-react` to your `content` glob so Tailwind picks them up.
1163
+
1164
+ ```js
1165
+ // tailwind.config.js
1166
+ module.exports = {
1167
+ content: ['./src/**/*.{ts,tsx}', './node_modules/azirid-react/dist/**/*.{js,mjs}'],
1168
+ }
1169
+ ```
1170
+
1171
+ ---
1172
+
1173
+ ## Utilities
1174
+
1175
+ ### `SDK_VERSION`
1176
+
1177
+ ```tsx
1178
+ import { SDK_VERSION } from 'azirid-react'
1179
+
1180
+ console.log(`azirid-react v${SDK_VERSION}`)
1181
+ ```
1182
+
1183
+ ### `isAuthError`
1184
+
1185
+ Check if an error is an authentication error (401 or 403).
1186
+
1187
+ ```tsx
1188
+ import { isAuthError } from 'azirid-react'
1189
+
1190
+ try {
1191
+ await someApiCall()
1192
+ } catch (err) {
1193
+ if (isAuthError(err)) {
1194
+ // redirect to login
1195
+ }
1196
+ }
1197
+ ```
1198
+
1199
+ ### `removeStyles`
1200
+
1201
+ Remove CSS styles injected by SDK components. Useful when unmounting the provider.
1202
+
1203
+ ```tsx
1204
+ import { removeStyles } from 'azirid-react'
1205
+
1206
+ useEffect(() => {
1207
+ return () => removeStyles()
1208
+ }, [])
1209
+ ```
1210
+
1211
+ ### `cn`
1212
+
1213
+ Tailwind class merge utility (powered by `clsx` + `tailwind-merge`).
1214
+
1215
+ ```tsx
1216
+ import { cn } from 'azirid-react'
1217
+
1218
+ <div className={cn('p-4 bg-white', isActive && 'bg-blue-500')} />
1219
+ ```
1220
+
1221
+ ### `BASE_PATHS` and `buildPaths`
1222
+
1223
+ ```tsx
1224
+ import { BASE_PATHS, buildPaths } from 'azirid-react'
1225
+
1226
+ BASE_PATHS.proxy // '/api/auth'
1227
+ BASE_PATHS.direct // '/v1/users/auth'
1228
+
1229
+ // Build custom path map from a base path
1230
+ const paths = buildPaths('/custom/auth')
1231
+ // paths.login → '/custom/auth/login'
1232
+ // paths.signup → '/custom/auth/signup'
1233
+ // etc.
1234
+ ```
1235
+
1236
+ ---
1237
+
1238
+ ## Types Reference
1239
+
1240
+ All types are exported from `azirid-react` and can be imported directly:
1241
+
1242
+ ```tsx
1243
+ import type {
1244
+ AuthUser,
1245
+ AuthSuccessResponse,
1246
+ AppBranding,
1247
+ SignupData,
1248
+ BillingPlan,
1249
+ UserSubscription,
1250
+ // ...
1251
+ } from 'azirid-react'
1252
+ ```
1253
+
1254
+ ### Auth Types
1255
+
1256
+ | Type | Description |
1257
+ | --- | --- |
1258
+ | `AuthUser` | Authenticated user object (`id`, `email`, `firstName`, `emailVerified`, `mfaEnabled`, `appId`, `tenantId`, ...) |
1259
+ | `AuthSuccessResponse` | Login/signup response (`accessToken`, `user`) |
1260
+ | `AuthState` | Full auth state (`user`, `accessToken`, `isAuthenticated`, `isLoading`, `error`) |
1261
+ | `SignupData` | Signup payload (`email`, `password`, `acceptedTosVersion?`, `referralCode?`, ...) |
1262
+ | `AppBranding` | Branding config (`displayName`, `logoUrl`, `primaryColor`, `removeBranding`, ...) |
1263
+ | `BootstrapResponse` | Bootstrap result (authenticated/unauthenticated + branding) |
1264
+ | `AziridProviderProps` | All provider props (see table above) |
1265
+ | `AziridContextValue` | Context value returned by `useAzirid()` |
1266
+
1267
+ ### Billing Types
1268
+
1269
+ | Type | Description |
1270
+ | --- | --- |
1271
+ | `BillingPlan` | Plan object (`id`, `name`, `amount`, `currency`, `interval`, `features`, ...) |
1272
+ | `UserSubscription` | Subscription (`planId`, `plan`, `status`, `currentPeriodEnd`, `cancelAtPeriodEnd`, ...) |
1273
+ | `CheckoutResponse` | Checkout result (`url`, `sessionId`, `provider`, `plan`, `widgetConfig`, `bankDetails`, ...) |
1274
+ | `BillingInvoice` | Invoice (`amount`, `currency`, `status`, `paidAt`, `invoiceUrl`, ...) |
1275
+ | `PaymentProviderType` | `'STRIPE' \| 'PAYPAL' \| 'PAYPHONE' \| 'NUVEI' \| 'MANUAL_TRANSFER'` |
1276
+ | `AvailableProvider` | Provider info (`provider`, `checkout`, `subscriptions`) |
1277
+ | `SubmitTransferProofData` | Transfer proof payload (`planId`, `fileUrl`, `amount`, ...) |
1278
+ | `TransferProof` | Transfer proof object (`status`: `PENDING_REVIEW \| APPROVED \| REJECTED`, ...) |
1279
+ | `PayphoneWidgetConfig` | Payphone widget config (`token`, `storeId`, `amount`, ...) |
1280
+
1281
+ ### Referral Types
1282
+
1283
+ | Type | Description |
1284
+ | --- | --- |
1285
+ | `ReferralInfo` | Referral info (`referralCode`, `referralUrl`, `totalReferred`, `completedReferrals`, ...) |
1286
+ | `ReferralStatsData` | Stats + referral list (`totalRewards`, `referrals[]`) |
1287
+ | `ReferralItem` | Single referral (`referredEmail`, `status`, `rewardStatus`, `rewardAmount`, ...) |
1288
+
1289
+ ### Form Types
1290
+
1291
+ | Type | Description |
1292
+ | --- | --- |
1293
+ | `LoginFormProps` | LoginForm component props (`title`, `logo`, `labels`, `showSocialButtons`, ...) |
1294
+ | `SignupFormProps` | SignupForm component props |
1295
+ | `FieldError` | Validation error (`{ field: string, message: string }`) |
1296
+ | `UseFormReturn<T>` | Return type of `useFormState()` |
1297
+
1298
+ ### Passkey Types
1299
+
1300
+ | Type | Description |
1301
+ | --- | --- |
1302
+ | `PasskeyItem` | Passkey entry (`id`, `deviceName`, `credentialId`, `lastUsedAt`) |
1303
+ | `PasskeyRegisterStartData` | Registration start payload (`deviceName?`) |
1304
+ | `PasskeyRegisterStartResponse` | Registration challenge (`challengeId`, `options`) |
1305
+ | `PasskeyLoginStartResponse` | Login challenge (`challengeId`, `options`) |
1306
+
1307
+ ---
1308
+
1309
+ ## License
1310
+
1311
+ MIT