@deiondz/better-auth-razorpay 1.0.0 → 2.0.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 +258 -49
- package/api/cancel-subscription.ts +35 -80
- package/api/create-or-update-subscription.ts +254 -0
- package/api/get-plans.ts +26 -53
- package/api/index.ts +3 -5
- package/api/list-subscriptions.ts +79 -0
- package/api/restore-subscription.ts +79 -0
- package/api/webhook.ts +153 -121
- package/client/hooks.ts +248 -0
- package/client/types.ts +108 -0
- package/index.ts +110 -77
- package/lib/index.ts +10 -4
- package/lib/schemas.ts +20 -44
- package/lib/types.ts +121 -68
- package/package.json +12 -2
- package/api/get-subscription.ts +0 -174
- package/api/pause-subscription.ts +0 -138
- package/api/resume-subscription.ts +0 -150
- package/api/subscribe.ts +0 -188
- package/api/verify-payment.ts +0 -129
package/README.md
CHANGED
|
@@ -4,6 +4,10 @@ A comprehensive subscription management plugin for Better Auth that integrates w
|
|
|
4
4
|
|
|
5
5
|
> **📚 Always consult [better-auth.com/docs](https://better-auth.com/docs) for the latest Better Auth API and best practices.**
|
|
6
6
|
|
|
7
|
+
## Credits
|
|
8
|
+
|
|
9
|
+
This plugin is inspired by and aligned with the design of the [better-auth-razorpay](https://github.com/iamjasonkendrick/better-auth-razorpay) community plugin. Credit and thanks go to the original author **[Jason Kendrick](https://github.com/iamjasonkendrick)** ([@iamjasonkendrick](https://github.com/iamjasonkendrick)) for the subscription flow, callback API, and feature set that this implementation follows.
|
|
10
|
+
|
|
7
11
|
## Table of Contents
|
|
8
12
|
|
|
9
13
|
- [Overview](#overview)
|
|
@@ -12,24 +16,26 @@ A comprehensive subscription management plugin for Better Auth that integrates w
|
|
|
12
16
|
- [Database Setup](#database-setup)
|
|
13
17
|
- [API Endpoints](#api-endpoints)
|
|
14
18
|
- [Client Usage](#client-usage)
|
|
19
|
+
- [TanStack Query Hooks](#tanstack-query-hooks)
|
|
15
20
|
- [Webhook Setup](#webhook-setup)
|
|
16
21
|
- [TypeScript Types](#typescript-types)
|
|
17
22
|
- [Error Handling](#error-handling)
|
|
18
23
|
- [Usage Examples](#usage-examples)
|
|
19
24
|
- [Best Practices](#best-practices)
|
|
20
25
|
- [Troubleshooting](#troubleshooting)
|
|
26
|
+
- [Credits](#credits)
|
|
21
27
|
|
|
22
28
|
## Overview
|
|
23
29
|
|
|
24
|
-
The Razorpay plugin provides a
|
|
30
|
+
The Razorpay plugin provides a subscription management solution aligned with the [community plugin design](https://github.com/iamjasonkendrick/better-auth-razorpay):
|
|
25
31
|
|
|
26
|
-
- ✅ **Subscription
|
|
27
|
-
- ✅ **
|
|
28
|
-
- ✅ **
|
|
29
|
-
- ✅ **
|
|
30
|
-
- ✅ **
|
|
31
|
-
- ✅ **
|
|
32
|
-
- ✅ **
|
|
32
|
+
- ✅ **Subscription flow**: Create-or-update (returns checkout URL), cancel (at period end or immediately), restore, list active/trialing subscriptions
|
|
33
|
+
- ✅ **Named plans**: Plans with `name`, `monthlyPlanId`, optional `annualPlanId`, `limits`, and `freeTrial`
|
|
34
|
+
- ✅ **Customer on sign-up**: Optional Razorpay customer creation when a user signs up, with `onCustomerCreate` and `getCustomerCreateParams`
|
|
35
|
+
- ✅ **Webhook handling**: Subscription events (activated, cancelled, expired, etc.) with optional `onSubscriptionActivated`, `onSubscriptionCancel`, `onSubscriptionUpdate`, and global `onEvent`
|
|
36
|
+
- ✅ **Authorization**: `authorizeReference` for list/create actions; `requireEmailVerification` for subscriptions
|
|
37
|
+
- ✅ **Type safety**: Full TypeScript support with `SubscriptionRecord`, `RazorpayPlan`, and plugin options
|
|
38
|
+
- ✅ **TanStack Query**: Works with TanStack Query; use our optional [pre-built hooks](#tanstack-query-hooks) or build your own hooks around `authClient.api` (GET/POST to the Razorpay endpoints).
|
|
33
39
|
|
|
34
40
|
## Installation
|
|
35
41
|
|
|
@@ -59,35 +65,45 @@ The package includes `razorpay` and `zod` as dependencies.
|
|
|
59
65
|
|
|
60
66
|
```typescript
|
|
61
67
|
// src/lib/auth.ts (or your auth configuration file)
|
|
68
|
+
import Razorpay from 'razorpay'
|
|
62
69
|
import { betterAuth } from 'better-auth'
|
|
63
70
|
import { razorpayPlugin } from 'better-auth-razorpay'
|
|
64
71
|
|
|
72
|
+
const razorpayClient = new Razorpay({
|
|
73
|
+
key_id: process.env.RAZORPAY_KEY_ID!,
|
|
74
|
+
key_secret: process.env.RAZORPAY_KEY_SECRET!,
|
|
75
|
+
})
|
|
76
|
+
|
|
65
77
|
export const auth = betterAuth({
|
|
66
78
|
// ... your Better Auth configuration
|
|
67
79
|
database: mongodbAdapter(await connect()), // or your adapter
|
|
68
80
|
secret: process.env.BETTER_AUTH_SECRET,
|
|
69
|
-
baseURL: process.env.BETTER_AUTH_URL,
|
|
81
|
+
baseURL: process.env.BETTER_AUTH_URL,
|
|
70
82
|
|
|
71
83
|
plugins: [
|
|
72
84
|
razorpayPlugin({
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
85
|
+
razorpayClient,
|
|
86
|
+
razorpayWebhookSecret: process.env.RAZORPAY_WEBHOOK_SECRET,
|
|
87
|
+
createCustomerOnSignUp: true, // optional
|
|
88
|
+
subscription: {
|
|
89
|
+
enabled: true,
|
|
90
|
+
plans: [
|
|
91
|
+
{
|
|
92
|
+
name: 'Starter',
|
|
93
|
+
monthlyPlanId: 'plan_xxxxxxxxxxxx',
|
|
94
|
+
annualPlanId: 'plan_yyyyyyyyyyyy', // optional
|
|
95
|
+
limits: { features: 5 },
|
|
96
|
+
freeTrial: { days: 7 }, // optional
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
onSubscriptionActivated: async ({ subscription, plan }) => {
|
|
100
|
+
console.log(`Subscription ${subscription.id} activated for plan ${plan.name}`)
|
|
101
|
+
},
|
|
102
|
+
},
|
|
80
103
|
onWebhookEvent: async (payload, context) => {
|
|
81
|
-
// Optional: Custom webhook event handling
|
|
82
104
|
const { event, subscription, payment } = payload
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// Send emails, update external systems, analytics, etc.
|
|
86
|
-
if (event === 'subscription.charged') {
|
|
87
|
-
await sendPaymentConfirmationEmail(user.email, {
|
|
88
|
-
amount: payment?.amount,
|
|
89
|
-
subscriptionId: subscription.id,
|
|
90
|
-
})
|
|
105
|
+
if (event === 'subscription.charged' && payment) {
|
|
106
|
+
// Send confirmation email, etc.
|
|
91
107
|
}
|
|
92
108
|
},
|
|
93
109
|
}),
|
|
@@ -145,21 +161,93 @@ npx @better-auth/cli@latest generate
|
|
|
145
161
|
|
|
146
162
|
```typescript
|
|
147
163
|
interface RazorpayPluginOptions {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
164
|
+
razorpayClient: Razorpay // Required: Initialized Razorpay instance (key_id, key_secret)
|
|
165
|
+
razorpayWebhookSecret?: string // Optional: Webhook secret for signature verification
|
|
166
|
+
createCustomerOnSignUp?: boolean // Optional: Create Razorpay customer on user sign-up (default: false)
|
|
167
|
+
onCustomerCreate?: (args) => Promise<void>
|
|
168
|
+
getCustomerCreateParams?: (args) => Promise<{ params?: Record<string, unknown> }>
|
|
169
|
+
subscription?: SubscriptionOptions // Optional: { enabled, plans, callbacks, authorizeReference, ... }
|
|
170
|
+
onEvent?: (event) => Promise<void>
|
|
171
|
+
onWebhookEvent?: (payload, context) => Promise<void> // Optional: Custom webhook callback
|
|
153
172
|
}
|
|
154
173
|
```
|
|
155
174
|
|
|
175
|
+
### Callback functions
|
|
176
|
+
|
|
177
|
+
The plugin supports the same callback hooks as the [community plugin](https://github.com/iamjasonkendrick/better-auth-razorpay). You can use them for emails, analytics, external systems, or custom logic.
|
|
178
|
+
|
|
179
|
+
| Callback | When it runs |
|
|
180
|
+
|----------|----------------|
|
|
181
|
+
| **`onCustomerCreate`** | After a Razorpay customer is created (when `createCustomerOnSignUp` is true and the user signs up). Receives `{ user, razorpayCustomer }`. |
|
|
182
|
+
| **`getCustomerCreateParams`** | Before creating a Razorpay customer on sign-up. Return `{ params }` (e.g. `notes`) to add custom data to the customer. |
|
|
183
|
+
| **`getSubscriptionCreateParams`** | Before creating a Razorpay subscription (create-or-update). Return `{ params }` (e.g. `notes`) to add custom data to the subscription. Receives `{ user, session, plan, subscription }`. |
|
|
184
|
+
| **`authorizeReference`** | Before create-or-update and before listing subscriptions for a `referenceId` other than the current user. Return `true` to allow. Receives `{ user, referenceId, action }`. |
|
|
185
|
+
| **`onSubscriptionCreated`** | After a new subscription is created (create-or-update). Receives `{ razorpaySubscription, subscription, plan }`. |
|
|
186
|
+
| **`onSubscriptionActivated`** | When the webhook receives `subscription.activated`. Receives `{ event, razorpaySubscription, subscription, plan }`. |
|
|
187
|
+
| **`onSubscriptionUpdate`** | When the webhook receives any other subscription event (e.g. `charged`, `paused`, `resumed`, `pending`, `halted`). Receives `{ event, subscription }`. |
|
|
188
|
+
| **`onSubscriptionCancel`** | When the webhook receives `subscription.cancelled` or `subscription.expired`. Receives `{ event, razorpaySubscription, subscription }`. |
|
|
189
|
+
| **`onEvent`** | After every processed webhook event. Receives the full event payload `{ event, ...payload }`. |
|
|
190
|
+
| **`onWebhookEvent`** | Legacy: after webhook processing, with payload and user context. Receives `(payload, context)` where `context` has `userId` and `user`. |
|
|
191
|
+
| **`freeTrial.onTrialStart`** | Optional, on a plan’s `freeTrial`. Call when you consider a subscription’s trial to have started (e.g. from your own logic or webhook handling). Receives `(subscription)`. |
|
|
192
|
+
| **`freeTrial.onTrialEnd`** | Optional, on a plan’s `freeTrial`. Call when you consider a subscription’s trial to have ended. Receives `{ subscription }`. |
|
|
193
|
+
|
|
194
|
+
Example: using callbacks in your config:
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
razorpayPlugin({
|
|
198
|
+
razorpayClient,
|
|
199
|
+
razorpayWebhookSecret: process.env.RAZORPAY_WEBHOOK_SECRET,
|
|
200
|
+
createCustomerOnSignUp: true,
|
|
201
|
+
onCustomerCreate: async ({ user, razorpayCustomer }) => {
|
|
202
|
+
console.log(`Razorpay customer created for user ${user.id}: ${razorpayCustomer.id}`)
|
|
203
|
+
},
|
|
204
|
+
getCustomerCreateParams: async ({ user, session }) => ({
|
|
205
|
+
params: { notes: { internalUserId: user.id } },
|
|
206
|
+
}),
|
|
207
|
+
subscription: {
|
|
208
|
+
enabled: true,
|
|
209
|
+
plans: [
|
|
210
|
+
{
|
|
211
|
+
name: 'Starter',
|
|
212
|
+
monthlyPlanId: 'plan_xxx',
|
|
213
|
+
freeTrial: {
|
|
214
|
+
days: 7,
|
|
215
|
+
onTrialStart: async (subscription) => console.log('Trial started', subscription.id),
|
|
216
|
+
onTrialEnd: async ({ subscription }) => console.log('Trial ended', subscription.id),
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
getSubscriptionCreateParams: async ({ user, plan, subscription }) => ({
|
|
221
|
+
params: { notes: { planName: plan.name } },
|
|
222
|
+
}),
|
|
223
|
+
onSubscriptionCreated: async ({ razorpaySubscription, subscription, plan }) => {
|
|
224
|
+
console.log(`Subscription ${subscription.id} created for plan ${plan.name}`)
|
|
225
|
+
},
|
|
226
|
+
onSubscriptionActivated: async ({ event, subscription, plan }) => {
|
|
227
|
+
console.log(`Subscription ${subscription.id} activated`)
|
|
228
|
+
},
|
|
229
|
+
onSubscriptionUpdate: async ({ event, subscription }) => {
|
|
230
|
+
console.log(`Subscription ${subscription.id} updated: ${event}`)
|
|
231
|
+
},
|
|
232
|
+
onSubscriptionCancel: async ({ event, subscription }) => {
|
|
233
|
+
console.log(`Subscription ${subscription.id} cancelled/expired: ${event}`)
|
|
234
|
+
},
|
|
235
|
+
authorizeReference: async ({ user, referenceId, action }) => user.id === referenceId,
|
|
236
|
+
},
|
|
237
|
+
onEvent: async (event) => console.log('Razorpay event:', event.event),
|
|
238
|
+
onWebhookEvent: async (payload, context) => {
|
|
239
|
+
// Custom logic: emails, analytics, etc.
|
|
240
|
+
},
|
|
241
|
+
})
|
|
242
|
+
```
|
|
243
|
+
|
|
156
244
|
### User Fields (Plug-and-Play)
|
|
157
245
|
|
|
158
|
-
The plugin extends the Better Auth user schema with
|
|
246
|
+
The plugin extends the Better Auth user schema with:
|
|
159
247
|
|
|
160
|
-
-
|
|
248
|
+
- **user**: `razorpayCustomerId` (optional) — set when `createCustomerOnSignUp` is true or when a customer is created for subscriptions.
|
|
161
249
|
|
|
162
|
-
You do **not** need to add
|
|
250
|
+
You do **not** need to add it manually to `user.additionalFields` unless you prefer to define it yourself.
|
|
163
251
|
|
|
164
252
|
## Database Setup
|
|
165
253
|
|
|
@@ -167,15 +255,12 @@ You do **not** need to add them manually to `user.additionalFields` unless you p
|
|
|
167
255
|
|
|
168
256
|
The plugin automatically creates the following database models via Better Auth's schema system:
|
|
169
257
|
|
|
170
|
-
**`
|
|
171
|
-
- `
|
|
172
|
-
- `subscriptionId` (string) - Razorpay subscription ID
|
|
173
|
-
- `planId` (string) - Plan ID
|
|
174
|
-
- `status` (string) - Subscription status
|
|
258
|
+
**`user`** (extended)
|
|
259
|
+
- `razorpayCustomerId` (string, optional) — Razorpay customer ID when customer creation is enabled.
|
|
175
260
|
|
|
176
|
-
**`
|
|
177
|
-
- `
|
|
178
|
-
- `
|
|
261
|
+
**`subscription`**
|
|
262
|
+
- `id`, `plan`, `referenceId`, `razorpayCustomerId`, `razorpaySubscriptionId`, `status`, `trialStart`, `trialEnd`, `periodStart`, `periodEnd`, `cancelAtPeriodEnd`, `seats`, `groupId`, `createdAt`, `updatedAt`
|
|
263
|
+
- `status` values: `created`, `active`, `pending`, `halted`, `cancelled`, `completed`, `expired`, `trialing`
|
|
179
264
|
|
|
180
265
|
### Database Adapters
|
|
181
266
|
|
|
@@ -190,11 +275,22 @@ The plugin works with all Better Auth database adapters:
|
|
|
190
275
|
|
|
191
276
|
## API Endpoints
|
|
192
277
|
|
|
193
|
-
All endpoints are prefixed with `/api/auth/razorpay/` (or your configured `basePath`).
|
|
278
|
+
All endpoints are prefixed with `/api/auth/razorpay/` (or your configured `basePath`).
|
|
279
|
+
|
|
280
|
+
### Subscription flow
|
|
281
|
+
|
|
282
|
+
| Action | Method | Endpoint | Description |
|
|
283
|
+
|--------|--------|----------|-------------|
|
|
284
|
+
| Create or update | `POST` | `subscription/create-or-update` | Start a subscription or update; returns `checkoutUrl` for Razorpay payment page. Body: `plan`, `annual?`, `seats?`, `subscriptionId?`, `successUrl?`, `disableRedirect?`. |
|
|
285
|
+
| Cancel | `POST` | `subscription/cancel` | Cancel by local subscription ID. Body: `subscriptionId`, `immediately?`. |
|
|
286
|
+
| Restore | `POST` | `subscription/restore` | Restore a subscription scheduled to cancel. Body: `subscriptionId`. |
|
|
287
|
+
| List | `GET` | `subscription/list` | List active/trialing subscriptions. Query: `referenceId?` (default: current user). |
|
|
288
|
+
| Get plans | `GET` | `get-plans` | Return configured plans (name, monthlyPlanId, annualPlanId, limits, freeTrial). |
|
|
289
|
+
| Webhook | `POST` | `webhook` | Razorpay webhook URL; configure in Razorpay Dashboard. |
|
|
194
290
|
|
|
195
291
|
### 1. Get Plans
|
|
196
292
|
|
|
197
|
-
Retrieve all configured subscription plans.
|
|
293
|
+
Retrieve all configured subscription plans (from plugin config; no Razorpay API call).
|
|
198
294
|
|
|
199
295
|
**Endpoint:** `GET /api/auth/razorpay/get-plans`
|
|
200
296
|
|
|
@@ -205,7 +301,7 @@ Retrieve all configured subscription plans.
|
|
|
205
301
|
```typescript
|
|
206
302
|
{
|
|
207
303
|
success: true,
|
|
208
|
-
data:
|
|
304
|
+
data: Array<{ name: string; monthlyPlanId: string; annualPlanId?: string; limits?: Record<string, number>; freeTrial?: { days: number } }>
|
|
209
305
|
}
|
|
210
306
|
```
|
|
211
307
|
|
|
@@ -577,12 +673,11 @@ import { authClient } from '@/lib/auth-client'
|
|
|
577
673
|
// GET request
|
|
578
674
|
const plans = await authClient.api.get('/razorpay/get-plans')
|
|
579
675
|
|
|
580
|
-
// POST request
|
|
581
|
-
const
|
|
582
|
-
body: {
|
|
583
|
-
plan_id: 'plan_1234567890',
|
|
584
|
-
},
|
|
676
|
+
// POST request (create or update subscription)
|
|
677
|
+
const result = await authClient.api.post('/razorpay/subscription/create-or-update', {
|
|
678
|
+
body: { plan: 'Starter', annual: false, seats: 1 },
|
|
585
679
|
})
|
|
680
|
+
// result.data.checkoutUrl -> redirect user to Razorpay payment page
|
|
586
681
|
```
|
|
587
682
|
|
|
588
683
|
### Type Safety
|
|
@@ -602,6 +697,120 @@ type Session = typeof authClient.$Infer.Session
|
|
|
602
697
|
type User = typeof authClient.$Infer.Session.user
|
|
603
698
|
```
|
|
604
699
|
|
|
700
|
+
## TanStack Query Hooks
|
|
701
|
+
|
|
702
|
+
The plugin works with **TanStack Query**: you can use it with `useQuery`/`useMutation` and `authClient.api` in any way you like. We provide optional pre-built hooks below; if you prefer a different setup, build your own hooks around the same endpoints (e.g. `authClient.api.get('/razorpay/get-plans')`, `authClient.api.post('/razorpay/subscription/create-or-update', { body })`, etc.).
|
|
703
|
+
|
|
704
|
+
To use our pre-built hooks, install peer dependencies:
|
|
705
|
+
|
|
706
|
+
```bash
|
|
707
|
+
npm install @tanstack/react-query react
|
|
708
|
+
# or yarn / pnpm / bun
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
Import from `better-auth-razorpay/hooks` and pass your Better Auth client as the first argument:
|
|
712
|
+
|
|
713
|
+
```tsx
|
|
714
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
715
|
+
import { createAuthClient } from 'better-auth/react'
|
|
716
|
+
import { razorpayClientPlugin } from 'better-auth-razorpay/client'
|
|
717
|
+
import {
|
|
718
|
+
usePlans,
|
|
719
|
+
useSubscriptions,
|
|
720
|
+
useCreateOrUpdateSubscription,
|
|
721
|
+
useCancelSubscription,
|
|
722
|
+
useRestoreSubscription,
|
|
723
|
+
razorpayQueryKeys,
|
|
724
|
+
} from 'better-auth-razorpay/hooks'
|
|
725
|
+
import type { CreateOrUpdateSubscriptionInput } from 'better-auth-razorpay/hooks'
|
|
726
|
+
|
|
727
|
+
const queryClient = new QueryClient()
|
|
728
|
+
// auth from your server config (e.g. import type { auth } from './auth')
|
|
729
|
+
const authClient = createAuthClient<typeof auth>({
|
|
730
|
+
plugins: [razorpayClientPlugin()],
|
|
731
|
+
})
|
|
732
|
+
|
|
733
|
+
function App() {
|
|
734
|
+
return (
|
|
735
|
+
<QueryClientProvider client={queryClient}>
|
|
736
|
+
<SubscriptionUI />
|
|
737
|
+
</QueryClientProvider>
|
|
738
|
+
)
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function SubscriptionUI() {
|
|
742
|
+
// Plans (no auth required)
|
|
743
|
+
const { data: plans, isLoading: plansLoading } = usePlans(authClient)
|
|
744
|
+
|
|
745
|
+
// Current user's subscriptions (requires session)
|
|
746
|
+
const { data: subscriptions, isLoading: subsLoading } = useSubscriptions(authClient)
|
|
747
|
+
|
|
748
|
+
const createOrUpdate = useCreateOrUpdateSubscription(authClient)
|
|
749
|
+
const cancel = useCancelSubscription(authClient)
|
|
750
|
+
const restore = useRestoreSubscription(authClient)
|
|
751
|
+
|
|
752
|
+
const handleSubscribe = () => {
|
|
753
|
+
createOrUpdate.mutate(
|
|
754
|
+
{ plan: 'Starter', annual: false },
|
|
755
|
+
{
|
|
756
|
+
onSuccess: (data) => {
|
|
757
|
+
window.location.href = data.checkoutUrl // Redirect to Razorpay
|
|
758
|
+
},
|
|
759
|
+
}
|
|
760
|
+
)
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const handleCancel = (subscriptionId: string) => {
|
|
764
|
+
cancel.mutate({ subscriptionId, immediately: false })
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
const handleRestore = (subscriptionId: string) => {
|
|
768
|
+
restore.mutate({ subscriptionId })
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
if (plansLoading) return <div>Loading plans...</div>
|
|
772
|
+
return (
|
|
773
|
+
<div>
|
|
774
|
+
{plans?.map((p) => (
|
|
775
|
+
<button key={p.name} onClick={handleSubscribe} disabled={createOrUpdate.isPending}>
|
|
776
|
+
Subscribe to {p.name}
|
|
777
|
+
</button>
|
|
778
|
+
))}
|
|
779
|
+
{subscriptions?.map((s) => (
|
|
780
|
+
<div key={s.id}>
|
|
781
|
+
{s.plan} – {s.status}
|
|
782
|
+
{s.cancelAtPeriodEnd ? (
|
|
783
|
+
<button onClick={() => handleRestore(s.id)}>Restore</button>
|
|
784
|
+
) : (
|
|
785
|
+
<button onClick={() => handleCancel(s.id)}>Cancel</button>
|
|
786
|
+
)}
|
|
787
|
+
</div>
|
|
788
|
+
))}
|
|
789
|
+
</div>
|
|
790
|
+
)
|
|
791
|
+
}
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
### Hooks reference
|
|
795
|
+
|
|
796
|
+
| Hook | Type | Description |
|
|
797
|
+
|------|------|-------------|
|
|
798
|
+
| `usePlans(client, options?)` | `useQuery` | Fetches configured plans (GET `/razorpay/get-plans`). |
|
|
799
|
+
| `useSubscriptions(client, input?, options?)` | `useQuery` | Lists active/trialing subscriptions (GET `/razorpay/subscription/list`). Optional `referenceId` in input or options. |
|
|
800
|
+
| `useCreateOrUpdateSubscription(client, options?)` | `useMutation` | Creates or updates subscription; returns `checkoutUrl`. Invalidates subscriptions list on success. |
|
|
801
|
+
| `useCancelSubscription(client, options?)` | `useMutation` | Cancels by local subscription ID; optional `immediately`. Invalidates subscriptions list on success. |
|
|
802
|
+
| `useRestoreSubscription(client, options?)` | `useMutation` | Restores a subscription scheduled to cancel. Invalidates subscriptions list on success. |
|
|
803
|
+
|
|
804
|
+
**Query keys** (for manual invalidation or prefetching):
|
|
805
|
+
|
|
806
|
+
```ts
|
|
807
|
+
import { razorpayQueryKeys } from 'better-auth-razorpay/hooks'
|
|
808
|
+
|
|
809
|
+
razorpayQueryKeys.plans() // ['razorpay', 'plans']
|
|
810
|
+
razorpayQueryKeys.subscriptions() // ['razorpay', 'subscriptions', 'me']
|
|
811
|
+
razorpayQueryKeys.subscriptions('user-id') // ['razorpay', 'subscriptions', 'user-id']
|
|
812
|
+
```
|
|
813
|
+
|
|
605
814
|
## Webhook Setup
|
|
606
815
|
|
|
607
816
|
### 1. Configure Webhook in Razorpay Dashboard
|
|
@@ -4,97 +4,71 @@ import {
|
|
|
4
4
|
cancelSubscriptionSchema,
|
|
5
5
|
handleRazorpayError,
|
|
6
6
|
type RazorpaySubscription,
|
|
7
|
-
type
|
|
7
|
+
type SubscriptionRecord,
|
|
8
8
|
} from '../lib'
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* @param razorpay - The Razorpay instance initialized with API credentials
|
|
14
|
-
* @returns A Better Auth endpoint handler
|
|
15
|
-
*
|
|
16
|
-
* @remarks
|
|
17
|
-
* This endpoint:
|
|
18
|
-
* - Requires user authentication via session
|
|
19
|
-
* - Verifies subscription ownership before cancellation
|
|
20
|
-
* - Cancels subscription at period end (not immediately)
|
|
21
|
-
* - Updates user record with cancellation flag and period end date
|
|
22
|
-
* - Subscription remains active until the current period ends
|
|
23
|
-
*
|
|
24
|
-
* @example
|
|
25
|
-
* Request body:
|
|
26
|
-
* ```json
|
|
27
|
-
* {
|
|
28
|
-
* "subscription_id": "sub_1234567890"
|
|
29
|
-
* }
|
|
30
|
-
* ```
|
|
11
|
+
* POST /api/auth/razorpay/subscription/cancel
|
|
12
|
+
* Cancels subscription by local subscription ID. Optionally cancel immediately.
|
|
31
13
|
*/
|
|
32
14
|
export const cancelSubscription = (razorpay: Razorpay) =>
|
|
33
15
|
createAuthEndpoint(
|
|
34
|
-
'/razorpay/cancel
|
|
16
|
+
'/razorpay/subscription/cancel',
|
|
35
17
|
{ method: 'POST', use: [sessionMiddleware] },
|
|
36
|
-
async (
|
|
18
|
+
async (ctx) => {
|
|
37
19
|
try {
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
// Get user ID from session
|
|
42
|
-
const userId = _ctx.context.session?.user?.id
|
|
43
|
-
|
|
20
|
+
const body = cancelSubscriptionSchema.parse(ctx.body)
|
|
21
|
+
const userId = ctx.context.session?.user?.id
|
|
44
22
|
if (!userId) {
|
|
45
23
|
return {
|
|
46
24
|
success: false,
|
|
47
|
-
error: {
|
|
48
|
-
code: 'UNAUTHORIZED',
|
|
49
|
-
description: 'User not authenticated',
|
|
50
|
-
},
|
|
25
|
+
error: { code: 'UNAUTHORIZED', description: 'User not authenticated' },
|
|
51
26
|
}
|
|
52
27
|
}
|
|
53
28
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
})) as RazorpaySubscriptionRecord | null
|
|
29
|
+
const record = (await ctx.context.adapter.findOne({
|
|
30
|
+
model: 'subscription',
|
|
31
|
+
where: [{ field: 'id', value: body.subscriptionId }],
|
|
32
|
+
})) as SubscriptionRecord | null
|
|
59
33
|
|
|
60
|
-
if (!
|
|
34
|
+
if (!record) {
|
|
61
35
|
return {
|
|
62
36
|
success: false,
|
|
63
|
-
error: {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
37
|
+
error: { code: 'SUBSCRIPTION_NOT_FOUND', description: 'Subscription not found' },
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (record.referenceId !== userId) {
|
|
41
|
+
return {
|
|
42
|
+
success: false,
|
|
43
|
+
error: { code: 'FORBIDDEN', description: 'Subscription does not belong to you' },
|
|
67
44
|
}
|
|
68
45
|
}
|
|
69
46
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (subscriptionUserId !== userId) {
|
|
47
|
+
const rpId = record.razorpaySubscriptionId
|
|
48
|
+
if (!rpId) {
|
|
73
49
|
return {
|
|
74
50
|
success: false,
|
|
75
|
-
error: {
|
|
76
|
-
code: 'UNAUTHORIZED',
|
|
77
|
-
description: 'Subscription does not belong to authenticated user',
|
|
78
|
-
},
|
|
51
|
+
error: { code: 'INVALID_STATE', description: 'No Razorpay subscription linked' },
|
|
79
52
|
}
|
|
80
53
|
}
|
|
81
54
|
|
|
82
|
-
//
|
|
55
|
+
// cancel_at_cycle_end: true = cancel at period end, false = cancel immediately
|
|
83
56
|
const subscription = (await razorpay.subscriptions.cancel(
|
|
84
|
-
|
|
85
|
-
|
|
57
|
+
rpId,
|
|
58
|
+
!body.immediately
|
|
86
59
|
)) as RazorpaySubscription
|
|
87
60
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
where: [{ field: 'id', value: userId }],
|
|
61
|
+
await ctx.context.adapter.update({
|
|
62
|
+
model: 'subscription',
|
|
63
|
+
where: [{ field: 'id', value: body.subscriptionId }],
|
|
92
64
|
update: {
|
|
93
65
|
data: {
|
|
94
|
-
|
|
95
|
-
|
|
66
|
+
status: 'cancelled',
|
|
67
|
+
cancelAtPeriodEnd: !body.immediately,
|
|
68
|
+
periodEnd: subscription.current_end
|
|
96
69
|
? new Date(subscription.current_end * 1000)
|
|
97
|
-
:
|
|
70
|
+
: record.periodEnd,
|
|
71
|
+
updatedAt: new Date(),
|
|
98
72
|
},
|
|
99
73
|
},
|
|
100
74
|
})
|
|
@@ -103,29 +77,10 @@ export const cancelSubscription = (razorpay: Razorpay) =>
|
|
|
103
77
|
success: true,
|
|
104
78
|
data: {
|
|
105
79
|
id: subscription.id,
|
|
106
|
-
entity: subscription.entity,
|
|
107
|
-
plan_id: subscription.plan_id,
|
|
108
80
|
status: subscription.status,
|
|
109
|
-
|
|
81
|
+
plan_id: subscription.plan_id,
|
|
110
82
|
current_end: subscription.current_end,
|
|
111
83
|
ended_at: subscription.ended_at,
|
|
112
|
-
quantity: subscription.quantity,
|
|
113
|
-
notes: subscription.notes,
|
|
114
|
-
charge_at: subscription.charge_at,
|
|
115
|
-
start_at: subscription.start_at,
|
|
116
|
-
end_at: subscription.end_at,
|
|
117
|
-
auth_attempts: subscription.auth_attempts,
|
|
118
|
-
total_count: subscription.total_count,
|
|
119
|
-
paid_count: subscription.paid_count,
|
|
120
|
-
customer_notify: subscription.customer_notify,
|
|
121
|
-
created_at: subscription.created_at,
|
|
122
|
-
expire_by: subscription.expire_by,
|
|
123
|
-
short_url: subscription.short_url,
|
|
124
|
-
has_scheduled_changes: subscription.has_scheduled_changes,
|
|
125
|
-
change_scheduled_at: subscription.change_scheduled_at,
|
|
126
|
-
source: subscription.source,
|
|
127
|
-
offer_id: subscription.offer_id,
|
|
128
|
-
remaining_count: subscription.remaining_count,
|
|
129
84
|
},
|
|
130
85
|
}
|
|
131
86
|
} catch (error) {
|