@deiondz/better-auth-razorpay 1.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 +1201 -0
- package/api/cancel-subscription.ts +135 -0
- package/api/get-plans.ts +63 -0
- package/api/get-subscription.ts +174 -0
- package/api/index.ts +8 -0
- package/api/pause-subscription.ts +138 -0
- package/api/resume-subscription.ts +150 -0
- package/api/subscribe.ts +188 -0
- package/api/verify-payment.ts +129 -0
- package/api/webhook.ts +273 -0
- package/client.ts +8 -0
- package/index.ts +123 -0
- package/lib/error-handler.ts +99 -0
- package/lib/index.ts +22 -0
- package/lib/schemas.ts +58 -0
- package/lib/types.ts +151 -0
- package/package.json +58 -0
package/README.md
ADDED
|
@@ -0,0 +1,1201 @@
|
|
|
1
|
+
# Razorpay Plugin for Better Auth
|
|
2
|
+
|
|
3
|
+
A comprehensive subscription management plugin for Better Auth that integrates with Razorpay for handling recurring payments, subscriptions, and webhooks.
|
|
4
|
+
|
|
5
|
+
> **📚 Always consult [better-auth.com/docs](https://better-auth.com/docs) for the latest Better Auth API and best practices.**
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Overview](#overview)
|
|
10
|
+
- [Installation](#installation)
|
|
11
|
+
- [Configuration](#configuration)
|
|
12
|
+
- [Database Setup](#database-setup)
|
|
13
|
+
- [API Endpoints](#api-endpoints)
|
|
14
|
+
- [Client Usage](#client-usage)
|
|
15
|
+
- [Webhook Setup](#webhook-setup)
|
|
16
|
+
- [TypeScript Types](#typescript-types)
|
|
17
|
+
- [Error Handling](#error-handling)
|
|
18
|
+
- [Usage Examples](#usage-examples)
|
|
19
|
+
- [Best Practices](#best-practices)
|
|
20
|
+
- [Troubleshooting](#troubleshooting)
|
|
21
|
+
|
|
22
|
+
## Overview
|
|
23
|
+
|
|
24
|
+
The Razorpay plugin provides a complete subscription management solution with the following features:
|
|
25
|
+
|
|
26
|
+
- ✅ **Subscription Management**: Create, pause, resume, and cancel subscriptions
|
|
27
|
+
- ✅ **Payment Verification**: Secure payment signature verification
|
|
28
|
+
- ✅ **Webhook Handling**: Automatic processing of Razorpay webhook events
|
|
29
|
+
- ✅ **Plan Management**: Retrieve and manage subscription plans
|
|
30
|
+
- ✅ **Type Safety**: Full TypeScript support with comprehensive type definitions
|
|
31
|
+
- ✅ **Error Handling**: Robust error handling with detailed error codes
|
|
32
|
+
- ✅ **Security**: Production-safe error messages and signature verification
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
### Prerequisites
|
|
37
|
+
|
|
38
|
+
- Better Auth configured in your project
|
|
39
|
+
- Razorpay account with API credentials
|
|
40
|
+
- Plans created in Razorpay dashboard
|
|
41
|
+
|
|
42
|
+
### Setup
|
|
43
|
+
|
|
44
|
+
1. **Install the Package**
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm install better-auth-razorpay
|
|
48
|
+
# or
|
|
49
|
+
yarn add better-auth-razorpay
|
|
50
|
+
# or
|
|
51
|
+
pnpm add better-auth-razorpay
|
|
52
|
+
# or
|
|
53
|
+
bun add better-auth-razorpay
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The package includes `razorpay` and `zod` as dependencies.
|
|
57
|
+
|
|
58
|
+
2. **Configure the Plugin**
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
// src/lib/auth.ts (or your auth configuration file)
|
|
62
|
+
import { betterAuth } from 'better-auth'
|
|
63
|
+
import { razorpayPlugin } from 'better-auth-razorpay'
|
|
64
|
+
|
|
65
|
+
export const auth = betterAuth({
|
|
66
|
+
// ... your Better Auth configuration
|
|
67
|
+
database: mongodbAdapter(await connect()), // or your adapter
|
|
68
|
+
secret: process.env.BETTER_AUTH_SECRET,
|
|
69
|
+
baseURL: process.env.BETTER_AUTH_URL, // Optional if env var is set
|
|
70
|
+
|
|
71
|
+
plugins: [
|
|
72
|
+
razorpayPlugin({
|
|
73
|
+
keyId: process.env.RAZORPAY_KEY_ID!,
|
|
74
|
+
keySecret: process.env.RAZORPAY_KEY_SECRET!,
|
|
75
|
+
webhookSecret: process.env.RAZORPAY_WEBHOOK_SECRET,
|
|
76
|
+
plans: [
|
|
77
|
+
'plan_1234567890', // Basic Plan
|
|
78
|
+
'plan_0987654321', // Premium Plan
|
|
79
|
+
],
|
|
80
|
+
onWebhookEvent: async (payload, context) => {
|
|
81
|
+
// Optional: Custom webhook event handling
|
|
82
|
+
const { event, subscription, payment } = payload
|
|
83
|
+
const { userId, user } = context
|
|
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
|
+
})
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
}),
|
|
94
|
+
],
|
|
95
|
+
})
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
3. **Add Client Plugin**
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
// src/lib/auth-client.ts
|
|
102
|
+
import { createAuthClient } from 'better-auth/react'
|
|
103
|
+
import { razorpayClientPlugin } from 'better-auth-razorpay/client'
|
|
104
|
+
import type { auth } from './auth'
|
|
105
|
+
|
|
106
|
+
export const authClient = createAuthClient<typeof auth>({
|
|
107
|
+
baseURL: process.env.PUBLIC_URL, // Optional if same domain
|
|
108
|
+
plugins: [
|
|
109
|
+
razorpayClientPlugin(),
|
|
110
|
+
// ... other client plugins
|
|
111
|
+
],
|
|
112
|
+
})
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
4. **Environment Variables**
|
|
116
|
+
|
|
117
|
+
Add to your `.env` file:
|
|
118
|
+
|
|
119
|
+
```env
|
|
120
|
+
# Better Auth (if not using env vars)
|
|
121
|
+
BETTER_AUTH_SECRET=your_32_char_minimum_secret
|
|
122
|
+
BETTER_AUTH_URL=https://yourdomain.com
|
|
123
|
+
|
|
124
|
+
# Razorpay
|
|
125
|
+
RAZORPAY_KEY_ID=rzp_test_xxxxxxxxxxxx
|
|
126
|
+
RAZORPAY_KEY_SECRET=your_secret_key
|
|
127
|
+
RAZORPAY_WEBHOOK_SECRET=your_webhook_secret
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
5. **Run Database Migration**
|
|
131
|
+
|
|
132
|
+
After adding the plugin, run the Better Auth CLI to apply schema changes:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
npx @better-auth/cli@latest migrate
|
|
136
|
+
# or for Prisma/Drizzle
|
|
137
|
+
npx @better-auth/cli@latest generate
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**Important:** Re-run the CLI after adding or changing plugins to update your database schema.
|
|
141
|
+
|
|
142
|
+
## Configuration
|
|
143
|
+
|
|
144
|
+
### Plugin Options
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
interface RazorpayPluginOptions {
|
|
148
|
+
keyId: string // Required: Razorpay Key ID
|
|
149
|
+
keySecret: string // Required: Razorpay Key Secret
|
|
150
|
+
webhookSecret?: string // Optional: Webhook secret for signature verification
|
|
151
|
+
plans: string[] // Required: Array of plan IDs from Razorpay dashboard
|
|
152
|
+
onWebhookEvent?: OnWebhookEventCallback // Optional: Custom webhook callback
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### User Fields (Plug-and-Play)
|
|
157
|
+
|
|
158
|
+
The plugin extends the Better Auth user schema with subscription-related fields. When you add the plugin and run `npx @better-auth/cli@latest migrate` (or `generate`), these columns are added to your user table automatically:
|
|
159
|
+
|
|
160
|
+
- `subscriptionId`, `subscriptionPlanId`, `subscriptionStatus`, `subscriptionCurrentPeriodEnd`, `cancelAtPeriodEnd`, `lastPaymentDate`, `nextBillingDate`
|
|
161
|
+
|
|
162
|
+
You do **not** need to add them manually to `user.additionalFields` unless you prefer to define them yourself.
|
|
163
|
+
|
|
164
|
+
## Database Setup
|
|
165
|
+
|
|
166
|
+
### Automatic Schema Creation
|
|
167
|
+
|
|
168
|
+
The plugin automatically creates the following database models via Better Auth's schema system:
|
|
169
|
+
|
|
170
|
+
**`razorpaySubscription`**
|
|
171
|
+
- `userId` (string) - User ID
|
|
172
|
+
- `subscriptionId` (string) - Razorpay subscription ID
|
|
173
|
+
- `planId` (string) - Plan ID
|
|
174
|
+
- `status` (string) - Subscription status
|
|
175
|
+
|
|
176
|
+
**`razorpayCustomer`** (for future use)
|
|
177
|
+
- `userId` (string, unique) - User ID
|
|
178
|
+
- `razorpayCustomerId` (string, unique) - Razorpay customer ID
|
|
179
|
+
|
|
180
|
+
### Database Adapters
|
|
181
|
+
|
|
182
|
+
The plugin works with all Better Auth database adapters:
|
|
183
|
+
|
|
184
|
+
- **MongoDB**: `mongodbAdapter()`
|
|
185
|
+
- **Prisma**: `prismaAdapter()`
|
|
186
|
+
- **Drizzle**: `drizzleAdapter()`
|
|
187
|
+
- **Direct connections**: PostgreSQL, MySQL, SQLite
|
|
188
|
+
|
|
189
|
+
**Important:** Better Auth uses adapter model names, NOT underlying table names. If your Prisma model is `User` mapping to table `users`, use the model name in configuration.
|
|
190
|
+
|
|
191
|
+
## API Endpoints
|
|
192
|
+
|
|
193
|
+
All endpoints are prefixed with `/api/auth/razorpay/` (or your configured `basePath`). Endpoints use Better Auth's `createAuthEndpoint` and automatically handle authentication via `sessionMiddleware` where required.
|
|
194
|
+
|
|
195
|
+
### 1. Get Plans
|
|
196
|
+
|
|
197
|
+
Retrieve all configured subscription plans.
|
|
198
|
+
|
|
199
|
+
**Endpoint:** `GET /api/auth/razorpay/get-plans`
|
|
200
|
+
|
|
201
|
+
**Authentication:** Not required (public endpoint)
|
|
202
|
+
|
|
203
|
+
**Response:**
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
{
|
|
207
|
+
success: true,
|
|
208
|
+
data: RazorpayPlan[]
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**Client Usage:**
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
import { authClient } from '@/lib/auth-client'
|
|
216
|
+
|
|
217
|
+
// Using Better Auth client
|
|
218
|
+
const response = await authClient.api.get('/razorpay/get-plans')
|
|
219
|
+
const { data } = response
|
|
220
|
+
|
|
221
|
+
// Or using fetch directly
|
|
222
|
+
const response = await fetch('/api/auth/razorpay/get-plans')
|
|
223
|
+
const { data } = await response.json()
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
### 2. Subscribe
|
|
229
|
+
|
|
230
|
+
Create a new subscription for the authenticated user.
|
|
231
|
+
|
|
232
|
+
**Endpoint:** `POST /api/auth/razorpay/subscribe`
|
|
233
|
+
|
|
234
|
+
**Authentication:** Required (uses `sessionMiddleware`)
|
|
235
|
+
|
|
236
|
+
**Request Body:**
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
{
|
|
240
|
+
plan_id: string // Required: Plan ID from Razorpay
|
|
241
|
+
total_count?: number // Optional: Total billing cycles (default: 12)
|
|
242
|
+
quantity?: number // Optional: Quantity (default: 1)
|
|
243
|
+
start_at?: number // Optional: Start timestamp (Unix)
|
|
244
|
+
expire_by?: number // Optional: Expiry timestamp (Unix)
|
|
245
|
+
customer_notify?: boolean // Optional: Send notification (default: true)
|
|
246
|
+
addons?: Array<{ // Optional: Addons
|
|
247
|
+
item: {
|
|
248
|
+
name: string
|
|
249
|
+
amount: number
|
|
250
|
+
currency: string
|
|
251
|
+
}
|
|
252
|
+
}>
|
|
253
|
+
offer_id?: string // Optional: Offer ID
|
|
254
|
+
notes?: Record<string, string> // Optional: Custom notes
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
**Response:**
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
{
|
|
262
|
+
success: true,
|
|
263
|
+
data: RazorpaySubscription
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**Client Usage:**
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
import { authClient } from '@/lib/auth-client'
|
|
271
|
+
|
|
272
|
+
// Using Better Auth client
|
|
273
|
+
const response = await authClient.api.post('/razorpay/subscribe', {
|
|
274
|
+
body: {
|
|
275
|
+
plan_id: 'plan_1234567890',
|
|
276
|
+
total_count: 12,
|
|
277
|
+
quantity: 1,
|
|
278
|
+
},
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
if (response.success) {
|
|
282
|
+
// Redirect to Razorpay checkout
|
|
283
|
+
window.location.href = response.data.short_url
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**Error Codes:**
|
|
288
|
+
- `PLAN_NOT_FOUND` - Plan ID not in configured plans
|
|
289
|
+
- `SUBSCRIPTION_ALREADY_EXISTS` - User already has an active subscription
|
|
290
|
+
- `UNAUTHORIZED` - User not authenticated
|
|
291
|
+
- `USER_NOT_FOUND` - User record not found
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
### 3. Get Subscription
|
|
296
|
+
|
|
297
|
+
Retrieve current subscription details for the authenticated user.
|
|
298
|
+
|
|
299
|
+
**Endpoint:** `GET /api/auth/razorpay/get-subscription`
|
|
300
|
+
|
|
301
|
+
**Authentication:** Required (uses `sessionMiddleware`)
|
|
302
|
+
|
|
303
|
+
**Response:**
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
{
|
|
307
|
+
success: true,
|
|
308
|
+
data: RazorpaySubscription | null // null if no subscription
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
**Client Usage:**
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
import { authClient } from '@/lib/auth-client'
|
|
316
|
+
|
|
317
|
+
// Using Better Auth client
|
|
318
|
+
const response = await authClient.api.get('/razorpay/get-subscription')
|
|
319
|
+
|
|
320
|
+
if (response.success && response.data) {
|
|
321
|
+
console.log('Subscription status:', response.data.status)
|
|
322
|
+
console.log('Plan ID:', response.data.plan_id)
|
|
323
|
+
console.log('Cancel at period end:', response.data.cancel_at_period_end)
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
**Error Codes:**
|
|
328
|
+
- `UNAUTHORIZED` - User not authenticated
|
|
329
|
+
- `USER_NOT_FOUND` - User record not found
|
|
330
|
+
- `SUBSCRIPTION_FETCH_FAILED` - Failed to fetch from Razorpay API
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
### 4. Verify Payment
|
|
335
|
+
|
|
336
|
+
Verify payment signature after Razorpay checkout completion.
|
|
337
|
+
|
|
338
|
+
**Endpoint:** `POST /api/auth/razorpay/verify-payment`
|
|
339
|
+
|
|
340
|
+
**Authentication:** Required (uses `sessionMiddleware`)
|
|
341
|
+
|
|
342
|
+
**Request Body:**
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
{
|
|
346
|
+
razorpay_payment_id: string // Required: Payment ID from Razorpay
|
|
347
|
+
razorpay_subscription_id: string // Required: Subscription ID from Razorpay
|
|
348
|
+
razorpay_signature: string // Required: Payment signature
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
**Response:**
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
{
|
|
356
|
+
success: true,
|
|
357
|
+
data: {
|
|
358
|
+
message: 'Payment verified successfully',
|
|
359
|
+
payment_id: string,
|
|
360
|
+
subscription_id: string
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
**Client Usage:**
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
import { authClient } from '@/lib/auth-client'
|
|
369
|
+
|
|
370
|
+
// After Razorpay checkout success callback
|
|
371
|
+
const handlePaymentSuccess = async (razorpayResponse: {
|
|
372
|
+
razorpay_payment_id: string
|
|
373
|
+
razorpay_subscription_id: string
|
|
374
|
+
razorpay_signature: string
|
|
375
|
+
}) => {
|
|
376
|
+
const response = await authClient.api.post('/razorpay/verify-payment', {
|
|
377
|
+
body: razorpayResponse,
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
if (response.success) {
|
|
381
|
+
console.log('Payment verified:', response.data.message)
|
|
382
|
+
// Redirect to success page
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
**Error Codes:**
|
|
388
|
+
- `SIGNATURE_VERIFICATION_FAILED` - Invalid payment signature
|
|
389
|
+
- `UNAUTHORIZED` - User not authenticated
|
|
390
|
+
- `SUBSCRIPTION_NOT_FOUND` - Subscription record not found
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
### 5. Pause Subscription
|
|
395
|
+
|
|
396
|
+
Pause an active subscription.
|
|
397
|
+
|
|
398
|
+
**Endpoint:** `POST /api/auth/razorpay/pause-subscription`
|
|
399
|
+
|
|
400
|
+
**Authentication:** Required (uses `sessionMiddleware`)
|
|
401
|
+
|
|
402
|
+
**Request Body:**
|
|
403
|
+
|
|
404
|
+
```typescript
|
|
405
|
+
{
|
|
406
|
+
subscription_id: string // Required: Subscription ID
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
**Response:**
|
|
411
|
+
|
|
412
|
+
```typescript
|
|
413
|
+
{
|
|
414
|
+
success: true,
|
|
415
|
+
data: RazorpaySubscription
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
**Client Usage:**
|
|
420
|
+
|
|
421
|
+
```typescript
|
|
422
|
+
import { authClient } from '@/lib/auth-client'
|
|
423
|
+
|
|
424
|
+
const response = await authClient.api.post('/razorpay/pause-subscription', {
|
|
425
|
+
body: {
|
|
426
|
+
subscription_id: 'sub_1234567890',
|
|
427
|
+
},
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
if (response.success) {
|
|
431
|
+
console.log('Subscription paused:', response.data.status)
|
|
432
|
+
}
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
**Error Codes:**
|
|
436
|
+
- `UNAUTHORIZED` - User not authenticated or subscription doesn't belong to user
|
|
437
|
+
- `SUBSCRIPTION_NOT_FOUND` - Subscription not found
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
### 6. Resume Subscription
|
|
442
|
+
|
|
443
|
+
Resume a paused subscription.
|
|
444
|
+
|
|
445
|
+
**Endpoint:** `POST /api/auth/razorpay/resume-subscription`
|
|
446
|
+
|
|
447
|
+
**Authentication:** Required (uses `sessionMiddleware`)
|
|
448
|
+
|
|
449
|
+
**Request Body:**
|
|
450
|
+
|
|
451
|
+
```typescript
|
|
452
|
+
{
|
|
453
|
+
subscription_id: string // Required: Subscription ID
|
|
454
|
+
}
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
**Response:**
|
|
458
|
+
|
|
459
|
+
```typescript
|
|
460
|
+
{
|
|
461
|
+
success: true,
|
|
462
|
+
data: RazorpaySubscription
|
|
463
|
+
}
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
**Client Usage:**
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
import { authClient } from '@/lib/auth-client'
|
|
470
|
+
|
|
471
|
+
const response = await authClient.api.post('/razorpay/resume-subscription', {
|
|
472
|
+
body: {
|
|
473
|
+
subscription_id: 'sub_1234567890',
|
|
474
|
+
},
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
if (response.success) {
|
|
478
|
+
console.log('Subscription resumed:', response.data.status)
|
|
479
|
+
}
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
**Error Codes:**
|
|
483
|
+
- `UNAUTHORIZED` - User not authenticated or subscription doesn't belong to user
|
|
484
|
+
- `SUBSCRIPTION_NOT_FOUND` - Subscription not found
|
|
485
|
+
- `INVALID_STATUS` - Subscription is not paused
|
|
486
|
+
|
|
487
|
+
---
|
|
488
|
+
|
|
489
|
+
### 7. Cancel Subscription
|
|
490
|
+
|
|
491
|
+
Cancel a subscription at the end of the current billing period.
|
|
492
|
+
|
|
493
|
+
**Endpoint:** `POST /api/auth/razorpay/cancel-subscription`
|
|
494
|
+
|
|
495
|
+
**Authentication:** Required (uses `sessionMiddleware`)
|
|
496
|
+
|
|
497
|
+
**Request Body:**
|
|
498
|
+
|
|
499
|
+
```typescript
|
|
500
|
+
{
|
|
501
|
+
subscription_id: string // Required: Subscription ID
|
|
502
|
+
}
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
**Response:**
|
|
506
|
+
|
|
507
|
+
```typescript
|
|
508
|
+
{
|
|
509
|
+
success: true,
|
|
510
|
+
data: RazorpaySubscription
|
|
511
|
+
}
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
**Note:** This cancels the subscription at period end, not immediately. The subscription remains active until the current billing period ends.
|
|
515
|
+
|
|
516
|
+
**Client Usage:**
|
|
517
|
+
|
|
518
|
+
```typescript
|
|
519
|
+
import { authClient } from '@/lib/auth-client'
|
|
520
|
+
|
|
521
|
+
const response = await authClient.api.post('/razorpay/cancel-subscription', {
|
|
522
|
+
body: {
|
|
523
|
+
subscription_id: 'sub_1234567890',
|
|
524
|
+
},
|
|
525
|
+
})
|
|
526
|
+
|
|
527
|
+
if (response.success) {
|
|
528
|
+
console.log('Subscription will cancel at period end')
|
|
529
|
+
}
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
**Error Codes:**
|
|
533
|
+
- `UNAUTHORIZED` - User not authenticated or subscription doesn't belong to user
|
|
534
|
+
- `SUBSCRIPTION_NOT_FOUND` - Subscription not found
|
|
535
|
+
|
|
536
|
+
---
|
|
537
|
+
|
|
538
|
+
### 8. Webhook
|
|
539
|
+
|
|
540
|
+
Handle Razorpay webhook events (automatically called by Razorpay).
|
|
541
|
+
|
|
542
|
+
**Endpoint:** `POST /api/auth/razorpay/webhook`
|
|
543
|
+
|
|
544
|
+
**Authentication:** Not required (webhook endpoint)
|
|
545
|
+
|
|
546
|
+
**Headers:**
|
|
547
|
+
- `x-razorpay-signature` - Webhook signature (required)
|
|
548
|
+
|
|
549
|
+
**Supported Events:**
|
|
550
|
+
- `subscription.authenticated` - Subscription authenticated
|
|
551
|
+
- `subscription.activated` - Subscription activated
|
|
552
|
+
- `subscription.charged` - Payment charged
|
|
553
|
+
- `subscription.cancelled` - Subscription cancelled
|
|
554
|
+
- `subscription.paused` - Subscription paused
|
|
555
|
+
- `subscription.resumed` - Subscription resumed
|
|
556
|
+
- `subscription.pending` - Subscription pending
|
|
557
|
+
- `subscription.halted` - Subscription halted
|
|
558
|
+
|
|
559
|
+
**Response:**
|
|
560
|
+
|
|
561
|
+
```typescript
|
|
562
|
+
{
|
|
563
|
+
success: boolean
|
|
564
|
+
message?: string
|
|
565
|
+
}
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
## Client Usage
|
|
569
|
+
|
|
570
|
+
### Better Auth Client Methods
|
|
571
|
+
|
|
572
|
+
The plugin integrates with Better Auth's client API. Use `authClient.api` for all endpoint calls:
|
|
573
|
+
|
|
574
|
+
```typescript
|
|
575
|
+
import { authClient } from '@/lib/auth-client'
|
|
576
|
+
|
|
577
|
+
// GET request
|
|
578
|
+
const plans = await authClient.api.get('/razorpay/get-plans')
|
|
579
|
+
|
|
580
|
+
// POST request
|
|
581
|
+
const subscription = await authClient.api.post('/razorpay/subscribe', {
|
|
582
|
+
body: {
|
|
583
|
+
plan_id: 'plan_1234567890',
|
|
584
|
+
},
|
|
585
|
+
})
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
### Type Safety
|
|
589
|
+
|
|
590
|
+
Infer types from your auth configuration:
|
|
591
|
+
|
|
592
|
+
```typescript
|
|
593
|
+
import type { auth } from '@/lib/auth'
|
|
594
|
+
import { createAuthClient } from 'better-auth/react'
|
|
595
|
+
|
|
596
|
+
export const authClient = createAuthClient<typeof auth>({
|
|
597
|
+
// ... config
|
|
598
|
+
})
|
|
599
|
+
|
|
600
|
+
// Infer session type
|
|
601
|
+
type Session = typeof authClient.$Infer.Session
|
|
602
|
+
type User = typeof authClient.$Infer.Session.user
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
## Webhook Setup
|
|
606
|
+
|
|
607
|
+
### 1. Configure Webhook in Razorpay Dashboard
|
|
608
|
+
|
|
609
|
+
1. Go to Razorpay Dashboard → Settings → Webhooks
|
|
610
|
+
2. Add webhook URL: `https://yourdomain.com/api/auth/razorpay/webhook`
|
|
611
|
+
3. Select events to subscribe:
|
|
612
|
+
- `subscription.authenticated`
|
|
613
|
+
- `subscription.activated`
|
|
614
|
+
- `subscription.charged`
|
|
615
|
+
- `subscription.cancelled`
|
|
616
|
+
- `subscription.paused`
|
|
617
|
+
- `subscription.resumed`
|
|
618
|
+
- `subscription.pending`
|
|
619
|
+
- `subscription.halted`
|
|
620
|
+
4. Copy the webhook secret
|
|
621
|
+
|
|
622
|
+
### 2. Configure Webhook Secret
|
|
623
|
+
|
|
624
|
+
Add the webhook secret to your environment variables:
|
|
625
|
+
|
|
626
|
+
```env
|
|
627
|
+
RAZORPAY_WEBHOOK_SECRET=whsec_xxxxxxxxxxxx
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
### 3. Custom Webhook Handler (Optional)
|
|
631
|
+
|
|
632
|
+
You can provide a custom callback function to handle webhook events:
|
|
633
|
+
|
|
634
|
+
```typescript
|
|
635
|
+
razorpayPlugin({
|
|
636
|
+
// ... other options
|
|
637
|
+
onWebhookEvent: async (payload, context) => {
|
|
638
|
+
const { event, subscription, payment } = payload
|
|
639
|
+
const { userId, user } = context
|
|
640
|
+
|
|
641
|
+
switch (event) {
|
|
642
|
+
case 'subscription.charged':
|
|
643
|
+
// Send payment confirmation email
|
|
644
|
+
await sendEmail(user.email, 'Payment Successful', {
|
|
645
|
+
amount: payment?.amount,
|
|
646
|
+
subscriptionId: subscription.id,
|
|
647
|
+
})
|
|
648
|
+
break
|
|
649
|
+
|
|
650
|
+
case 'subscription.cancelled':
|
|
651
|
+
// Send cancellation email
|
|
652
|
+
await sendEmail(user.email, 'Subscription Cancelled')
|
|
653
|
+
break
|
|
654
|
+
|
|
655
|
+
case 'subscription.activated':
|
|
656
|
+
// Update external systems
|
|
657
|
+
await updateCRM(userId, { subscriptionActive: true })
|
|
658
|
+
break
|
|
659
|
+
|
|
660
|
+
// Handle other events...
|
|
661
|
+
}
|
|
662
|
+
},
|
|
663
|
+
})
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
**Important:** Webhook callback errors are handled silently and don't break core webhook processing. The callback is for custom business logic only.
|
|
667
|
+
|
|
668
|
+
## TypeScript Types
|
|
669
|
+
|
|
670
|
+
### Import Types
|
|
671
|
+
|
|
672
|
+
```typescript
|
|
673
|
+
import type {
|
|
674
|
+
RazorpaySubscription,
|
|
675
|
+
RazorpaySubscriptionRecord,
|
|
676
|
+
RazorpayUserRecord,
|
|
677
|
+
RazorpayApiResponse,
|
|
678
|
+
RazorpaySuccessResponse,
|
|
679
|
+
RazorpayErrorResponse,
|
|
680
|
+
RazorpayPluginOptions,
|
|
681
|
+
OnWebhookEventCallback,
|
|
682
|
+
} from 'better-auth-razorpay'
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
### Response Types
|
|
686
|
+
|
|
687
|
+
```typescript
|
|
688
|
+
// Success response
|
|
689
|
+
interface RazorpaySuccessResponse<T> {
|
|
690
|
+
success: true
|
|
691
|
+
data: T
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// Error response
|
|
695
|
+
interface RazorpayErrorResponse {
|
|
696
|
+
success: false
|
|
697
|
+
error: {
|
|
698
|
+
code: string
|
|
699
|
+
description: string
|
|
700
|
+
[key: string]: unknown // Additional error metadata (development only)
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Union type
|
|
705
|
+
type RazorpayApiResponse<T> = RazorpaySuccessResponse<T> | RazorpayErrorResponse
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
### Subscription Type
|
|
709
|
+
|
|
710
|
+
```typescript
|
|
711
|
+
interface RazorpaySubscription {
|
|
712
|
+
id: string
|
|
713
|
+
entity: string
|
|
714
|
+
plan_id: string
|
|
715
|
+
status: string
|
|
716
|
+
current_start: number
|
|
717
|
+
current_end: number
|
|
718
|
+
ended_at: number | null
|
|
719
|
+
quantity: number
|
|
720
|
+
notes: Record<string, string> | null
|
|
721
|
+
charge_at: number
|
|
722
|
+
start_at: number
|
|
723
|
+
end_at: number
|
|
724
|
+
auth_attempts: number
|
|
725
|
+
total_count: number
|
|
726
|
+
paid_count: number
|
|
727
|
+
customer_notify: boolean
|
|
728
|
+
created_at: number
|
|
729
|
+
expire_by: number | null
|
|
730
|
+
short_url: string
|
|
731
|
+
has_scheduled_changes: boolean
|
|
732
|
+
change_scheduled_at: number | null
|
|
733
|
+
source: string
|
|
734
|
+
offer_id: string | null
|
|
735
|
+
remaining_count: string
|
|
736
|
+
}
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
## Error Handling
|
|
740
|
+
|
|
741
|
+
### Error Response Format
|
|
742
|
+
|
|
743
|
+
All endpoints return errors in a consistent format:
|
|
744
|
+
|
|
745
|
+
```typescript
|
|
746
|
+
{
|
|
747
|
+
success: false,
|
|
748
|
+
error: {
|
|
749
|
+
code: string, // Error code (e.g., 'UNAUTHORIZED', 'PLAN_NOT_FOUND')
|
|
750
|
+
description: string, // Human-readable error message
|
|
751
|
+
[key: string]: unknown // Additional error metadata (development only)
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
### Common Error Codes
|
|
757
|
+
|
|
758
|
+
| Code | Description |
|
|
759
|
+
|------|-------------|
|
|
760
|
+
| `INVALID_REQUEST` | Validation error (Zod schema validation failed) |
|
|
761
|
+
| `UNAUTHORIZED` | User not authenticated or subscription doesn't belong to user |
|
|
762
|
+
| `PLAN_NOT_FOUND` | Plan ID not found in configured plans |
|
|
763
|
+
| `SUBSCRIPTION_NOT_FOUND` | Subscription record not found |
|
|
764
|
+
| `SUBSCRIPTION_ALREADY_EXISTS` | User already has an active subscription |
|
|
765
|
+
| `INVALID_STATUS` | Subscription is not in the expected status |
|
|
766
|
+
| `SIGNATURE_VERIFICATION_FAILED` | Payment signature verification failed |
|
|
767
|
+
| `NETWORK_ERROR` | Network connection failed |
|
|
768
|
+
| `TIMEOUT_ERROR` | Request timed out |
|
|
769
|
+
| `RAZORPAY_ERROR` | Razorpay API error |
|
|
770
|
+
| `UNKNOWN_ERROR` | Unexpected error occurred |
|
|
771
|
+
|
|
772
|
+
### Error Handling Example
|
|
773
|
+
|
|
774
|
+
```typescript
|
|
775
|
+
import { authClient } from '@/lib/auth-client'
|
|
776
|
+
|
|
777
|
+
try {
|
|
778
|
+
const response = await authClient.api.post('/razorpay/subscribe', {
|
|
779
|
+
body: { plan_id: 'plan_123' },
|
|
780
|
+
})
|
|
781
|
+
|
|
782
|
+
if (!response.success) {
|
|
783
|
+
switch (response.error.code) {
|
|
784
|
+
case 'PLAN_NOT_FOUND':
|
|
785
|
+
toast.error('Plan not available')
|
|
786
|
+
break
|
|
787
|
+
case 'SUBSCRIPTION_ALREADY_EXISTS':
|
|
788
|
+
toast.error('You already have an active subscription')
|
|
789
|
+
break
|
|
790
|
+
default:
|
|
791
|
+
toast.error(response.error.description)
|
|
792
|
+
}
|
|
793
|
+
return
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Handle success
|
|
797
|
+
window.location.href = response.data.short_url
|
|
798
|
+
} catch (error) {
|
|
799
|
+
console.error('Network error:', error)
|
|
800
|
+
toast.error('Network error. Please try again.')
|
|
801
|
+
}
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
## Usage Examples
|
|
805
|
+
|
|
806
|
+
### Complete Subscription Flow
|
|
807
|
+
|
|
808
|
+
```typescript
|
|
809
|
+
import { authClient } from '@/lib/auth-client'
|
|
810
|
+
|
|
811
|
+
async function handleSubscriptionFlow() {
|
|
812
|
+
// 1. Get available plans
|
|
813
|
+
const plansResponse = await authClient.api.get('/razorpay/get-plans')
|
|
814
|
+
if (!plansResponse.success) {
|
|
815
|
+
console.error('Failed to fetch plans')
|
|
816
|
+
return
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const plans = plansResponse.data
|
|
820
|
+
const selectedPlan = plans[0]
|
|
821
|
+
|
|
822
|
+
// 2. Create subscription
|
|
823
|
+
const subscribeResponse = await authClient.api.post('/razorpay/subscribe', {
|
|
824
|
+
body: {
|
|
825
|
+
plan_id: selectedPlan.id,
|
|
826
|
+
total_count: 12,
|
|
827
|
+
},
|
|
828
|
+
})
|
|
829
|
+
|
|
830
|
+
if (!subscribeResponse.success) {
|
|
831
|
+
console.error('Failed to create subscription:', subscribeResponse.error)
|
|
832
|
+
return
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// 3. Redirect to Razorpay checkout
|
|
836
|
+
window.location.href = subscribeResponse.data.short_url
|
|
837
|
+
|
|
838
|
+
// 4. After payment, verify payment (in Razorpay success handler)
|
|
839
|
+
// This is handled in the Razorpay checkout callback
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Razorpay checkout success handler
|
|
843
|
+
function handleRazorpaySuccess(response: {
|
|
844
|
+
razorpay_payment_id: string
|
|
845
|
+
razorpay_subscription_id: string
|
|
846
|
+
razorpay_signature: string
|
|
847
|
+
}) {
|
|
848
|
+
authClient.api.post('/razorpay/verify-payment', {
|
|
849
|
+
body: response,
|
|
850
|
+
}).then((result) => {
|
|
851
|
+
if (result.success) {
|
|
852
|
+
// Redirect to success page
|
|
853
|
+
window.location.href = '/subscription/success'
|
|
854
|
+
}
|
|
855
|
+
})
|
|
856
|
+
}
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
### React Hook Example with TanStack Query
|
|
860
|
+
|
|
861
|
+
```typescript
|
|
862
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
863
|
+
import { authClient } from '@/lib/auth-client'
|
|
864
|
+
|
|
865
|
+
// Get plans
|
|
866
|
+
export function usePlans() {
|
|
867
|
+
return useQuery({
|
|
868
|
+
queryKey: ['razorpay', 'plans'],
|
|
869
|
+
queryFn: async () => {
|
|
870
|
+
const response = await authClient.api.get('/razorpay/get-plans')
|
|
871
|
+
if (!response.success) throw new Error(response.error.description)
|
|
872
|
+
return response.data
|
|
873
|
+
},
|
|
874
|
+
})
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Get subscription
|
|
878
|
+
export function useSubscription() {
|
|
879
|
+
return useQuery({
|
|
880
|
+
queryKey: ['razorpay', 'subscription'],
|
|
881
|
+
queryFn: async () => {
|
|
882
|
+
const response = await authClient.api.get('/razorpay/get-subscription')
|
|
883
|
+
if (!response.success) throw new Error(response.error.description)
|
|
884
|
+
return response.data
|
|
885
|
+
},
|
|
886
|
+
})
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// Subscribe
|
|
890
|
+
export function useSubscribe() {
|
|
891
|
+
const queryClient = useQueryClient()
|
|
892
|
+
|
|
893
|
+
return useMutation({
|
|
894
|
+
mutationFn: async (planId: string) => {
|
|
895
|
+
const response = await authClient.api.post('/razorpay/subscribe', {
|
|
896
|
+
body: { plan_id: planId },
|
|
897
|
+
})
|
|
898
|
+
if (!response.success) throw new Error(response.error.description)
|
|
899
|
+
return response.data
|
|
900
|
+
},
|
|
901
|
+
onSuccess: () => {
|
|
902
|
+
queryClient.invalidateQueries({ queryKey: ['razorpay', 'subscription'] })
|
|
903
|
+
},
|
|
904
|
+
})
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Cancel subscription
|
|
908
|
+
export function useCancelSubscription() {
|
|
909
|
+
const queryClient = useQueryClient()
|
|
910
|
+
|
|
911
|
+
return useMutation({
|
|
912
|
+
mutationFn: async (subscriptionId: string) => {
|
|
913
|
+
const response = await authClient.api.post('/razorpay/cancel-subscription', {
|
|
914
|
+
body: { subscription_id: subscriptionId },
|
|
915
|
+
})
|
|
916
|
+
if (!response.success) throw new Error(response.error.description)
|
|
917
|
+
return response.data
|
|
918
|
+
},
|
|
919
|
+
onSuccess: () => {
|
|
920
|
+
queryClient.invalidateQueries({ queryKey: ['razorpay', 'subscription'] })
|
|
921
|
+
},
|
|
922
|
+
})
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// Pause subscription
|
|
926
|
+
export function usePauseSubscription() {
|
|
927
|
+
const queryClient = useQueryClient()
|
|
928
|
+
|
|
929
|
+
return useMutation({
|
|
930
|
+
mutationFn: async (subscriptionId: string) => {
|
|
931
|
+
const response = await authClient.api.post('/razorpay/pause-subscription', {
|
|
932
|
+
body: { subscription_id: subscriptionId },
|
|
933
|
+
})
|
|
934
|
+
if (!response.success) throw new Error(response.error.description)
|
|
935
|
+
return response.data
|
|
936
|
+
},
|
|
937
|
+
onSuccess: () => {
|
|
938
|
+
queryClient.invalidateQueries({ queryKey: ['razorpay', 'subscription'] })
|
|
939
|
+
},
|
|
940
|
+
})
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Resume subscription
|
|
944
|
+
export function useResumeSubscription() {
|
|
945
|
+
const queryClient = useQueryClient()
|
|
946
|
+
|
|
947
|
+
return useMutation({
|
|
948
|
+
mutationFn: async (subscriptionId: string) => {
|
|
949
|
+
const response = await authClient.api.post('/razorpay/resume-subscription', {
|
|
950
|
+
body: { subscription_id: subscriptionId },
|
|
951
|
+
})
|
|
952
|
+
if (!response.success) throw new Error(response.error.description)
|
|
953
|
+
return response.data
|
|
954
|
+
},
|
|
955
|
+
onSuccess: () => {
|
|
956
|
+
queryClient.invalidateQueries({ queryKey: ['razorpay', 'subscription'] })
|
|
957
|
+
},
|
|
958
|
+
})
|
|
959
|
+
}
|
|
960
|
+
```
|
|
961
|
+
|
|
962
|
+
### React Component Example
|
|
963
|
+
|
|
964
|
+
```typescript
|
|
965
|
+
'use client'
|
|
966
|
+
|
|
967
|
+
import { usePlans, useSubscription, useSubscribe, useCancelSubscription } from '@/hooks/use-razorpay'
|
|
968
|
+
|
|
969
|
+
export function SubscriptionPage() {
|
|
970
|
+
const { data: plans, isLoading: plansLoading } = usePlans()
|
|
971
|
+
const { data: subscription, isLoading: subLoading } = useSubscription()
|
|
972
|
+
const subscribe = useSubscribe()
|
|
973
|
+
const cancel = useCancelSubscription()
|
|
974
|
+
|
|
975
|
+
const handleSubscribe = async (planId: string) => {
|
|
976
|
+
try {
|
|
977
|
+
const subscription = await subscribe.mutateAsync(planId)
|
|
978
|
+
// Redirect to Razorpay checkout
|
|
979
|
+
window.location.href = subscription.short_url
|
|
980
|
+
} catch (error) {
|
|
981
|
+
console.error('Failed to create subscription:', error)
|
|
982
|
+
// Handle error (show toast, etc.)
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
const handleCancel = async () => {
|
|
987
|
+
if (!subscription) return
|
|
988
|
+
try {
|
|
989
|
+
await cancel.mutateAsync(subscription.id)
|
|
990
|
+
alert('Subscription will be cancelled at period end')
|
|
991
|
+
} catch (error) {
|
|
992
|
+
console.error('Failed to cancel subscription:', error)
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
if (plansLoading || subLoading) {
|
|
997
|
+
return <div>Loading...</div>
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
return (
|
|
1001
|
+
<div>
|
|
1002
|
+
{subscription ? (
|
|
1003
|
+
<div>
|
|
1004
|
+
<h2>Current Subscription</h2>
|
|
1005
|
+
<p>Status: {subscription.status}</p>
|
|
1006
|
+
<p>Plan: {subscription.plan_id}</p>
|
|
1007
|
+
{subscription.cancel_at_period_end && (
|
|
1008
|
+
<p className="text-warning">Will cancel at period end</p>
|
|
1009
|
+
)}
|
|
1010
|
+
<button
|
|
1011
|
+
onClick={handleCancel}
|
|
1012
|
+
disabled={cancel.isPending}
|
|
1013
|
+
>
|
|
1014
|
+
{cancel.isPending ? 'Cancelling...' : 'Cancel Subscription'}
|
|
1015
|
+
</button>
|
|
1016
|
+
</div>
|
|
1017
|
+
) : (
|
|
1018
|
+
<div>
|
|
1019
|
+
<h2>Select a Plan</h2>
|
|
1020
|
+
{plans?.map((plan) => (
|
|
1021
|
+
<div key={plan.id}>
|
|
1022
|
+
<h3>{plan.name || plan.id}</h3>
|
|
1023
|
+
<p>₹{plan.amount / 100}/month</p>
|
|
1024
|
+
<button
|
|
1025
|
+
onClick={() => handleSubscribe(plan.id)}
|
|
1026
|
+
disabled={subscribe.isPending}
|
|
1027
|
+
>
|
|
1028
|
+
{subscribe.isPending ? 'Processing...' : 'Subscribe'}
|
|
1029
|
+
</button>
|
|
1030
|
+
</div>
|
|
1031
|
+
))}
|
|
1032
|
+
</div>
|
|
1033
|
+
)}
|
|
1034
|
+
</div>
|
|
1035
|
+
)
|
|
1036
|
+
}
|
|
1037
|
+
```
|
|
1038
|
+
|
|
1039
|
+
### Razorpay Checkout Integration
|
|
1040
|
+
|
|
1041
|
+
```typescript
|
|
1042
|
+
import { loadScript } from '@/lib/razorpay-script' // Your script loader
|
|
1043
|
+
|
|
1044
|
+
async function initializeRazorpayCheckout(subscriptionId: string) {
|
|
1045
|
+
// Load Razorpay script
|
|
1046
|
+
await loadScript('https://checkout.razorpay.com/v1/checkout.js')
|
|
1047
|
+
|
|
1048
|
+
const options = {
|
|
1049
|
+
subscription_id: subscriptionId,
|
|
1050
|
+
key: process.env.NEXT_PUBLIC_RAZORPAY_KEY_ID!,
|
|
1051
|
+
name: 'Your App Name',
|
|
1052
|
+
description: 'Subscription Payment',
|
|
1053
|
+
handler: async function (response: {
|
|
1054
|
+
razorpay_payment_id: string
|
|
1055
|
+
razorpay_subscription_id: string
|
|
1056
|
+
razorpay_signature: string
|
|
1057
|
+
}) {
|
|
1058
|
+
// Verify payment
|
|
1059
|
+
const verifyResponse = await authClient.api.post('/razorpay/verify-payment', {
|
|
1060
|
+
body: response,
|
|
1061
|
+
})
|
|
1062
|
+
|
|
1063
|
+
if (verifyResponse.success) {
|
|
1064
|
+
// Redirect to success page
|
|
1065
|
+
window.location.href = '/subscription/success'
|
|
1066
|
+
} else {
|
|
1067
|
+
// Handle error
|
|
1068
|
+
console.error('Payment verification failed:', verifyResponse.error)
|
|
1069
|
+
}
|
|
1070
|
+
},
|
|
1071
|
+
prefill: {
|
|
1072
|
+
name: user.name,
|
|
1073
|
+
email: user.email,
|
|
1074
|
+
},
|
|
1075
|
+
theme: {
|
|
1076
|
+
color: '#3399cc',
|
|
1077
|
+
},
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
const razorpay = new (window as any).Razorpay(options)
|
|
1081
|
+
razorpay.open()
|
|
1082
|
+
}
|
|
1083
|
+
```
|
|
1084
|
+
|
|
1085
|
+
## Best Practices
|
|
1086
|
+
|
|
1087
|
+
### 1. Better Auth Configuration
|
|
1088
|
+
|
|
1089
|
+
- **Use environment variables** - Prefer `BETTER_AUTH_SECRET` and `BETTER_AUTH_URL` env vars over config
|
|
1090
|
+
- **Re-run CLI after changes** - Always run `npx @better-auth/cli@latest migrate` after adding/changing plugins
|
|
1091
|
+
- **Model names vs table names** - Use ORM model names in config, not DB table names
|
|
1092
|
+
- **Type inference** - Use `typeof auth.$Infer.Session` for type safety
|
|
1093
|
+
|
|
1094
|
+
### 2. Plan Management
|
|
1095
|
+
|
|
1096
|
+
- **Create plans in Razorpay dashboard** - Plans are managed in Razorpay, not through the API
|
|
1097
|
+
- **Use plan IDs in configuration** - Add all plan IDs to the `plans` array in plugin options
|
|
1098
|
+
- **Validate plans client-side** - Always validate plan IDs before allowing users to subscribe
|
|
1099
|
+
|
|
1100
|
+
### 3. Security
|
|
1101
|
+
|
|
1102
|
+
- **Always verify payment signatures** - Never skip signature verification
|
|
1103
|
+
- **Use HTTPS** - Always use HTTPS in production
|
|
1104
|
+
- **Protect webhook secret** - Never expose webhook secret in client-side code
|
|
1105
|
+
- **Validate user ownership** - The plugin automatically validates subscription ownership
|
|
1106
|
+
- **Production error messages** - Error messages are automatically sanitized in production
|
|
1107
|
+
|
|
1108
|
+
### 4. Error Handling
|
|
1109
|
+
|
|
1110
|
+
- **Handle all error codes** - Check for specific error codes and provide user-friendly messages
|
|
1111
|
+
- **Log errors server-side** - Log detailed errors server-side, show generic messages to users
|
|
1112
|
+
- **Retry logic** - Implement retry logic for network errors and timeouts
|
|
1113
|
+
- **Use Better Auth error patterns** - Follow Better Auth's error handling conventions
|
|
1114
|
+
|
|
1115
|
+
### 5. Webhook Handling
|
|
1116
|
+
|
|
1117
|
+
- **Idempotent operations** - Ensure webhook handlers are idempotent
|
|
1118
|
+
- **Handle failures gracefully** - Webhook callback errors don't break core functionality
|
|
1119
|
+
- **Monitor webhook events** - Log webhook events for debugging and analytics
|
|
1120
|
+
- **Use webhook callback** - Leverage `onWebhookEvent` for custom business logic
|
|
1121
|
+
|
|
1122
|
+
### 6. Subscription Lifecycle
|
|
1123
|
+
|
|
1124
|
+
- **Check subscription status** - Always check subscription status before allowing actions
|
|
1125
|
+
- **Handle edge cases** - Account for paused, cancelled, and expired subscriptions
|
|
1126
|
+
- **Update UI accordingly** - Reflect subscription status changes in your UI
|
|
1127
|
+
- **Use webhooks for updates** - Rely on webhooks for status updates rather than polling
|
|
1128
|
+
|
|
1129
|
+
### 7. Performance
|
|
1130
|
+
|
|
1131
|
+
- **Cache plans** - Plans don't change frequently, consider caching
|
|
1132
|
+
- **Optimize queries** - The plugin already optimizes database queries
|
|
1133
|
+
- **Use webhooks** - Rely on webhooks for status updates rather than polling
|
|
1134
|
+
- **TanStack Query** - Use TanStack Query for client-side caching and state management
|
|
1135
|
+
|
|
1136
|
+
### 8. TypeScript Best Practices
|
|
1137
|
+
|
|
1138
|
+
- **Infer types from auth** - Use `typeof auth.$Infer.Session` for type safety
|
|
1139
|
+
- **Type client properly** - Use `createAuthClient<typeof auth>()` for full type inference
|
|
1140
|
+
- **Export types** - Export and reuse types from the plugin
|
|
1141
|
+
|
|
1142
|
+
## Troubleshooting
|
|
1143
|
+
|
|
1144
|
+
### Common Issues
|
|
1145
|
+
|
|
1146
|
+
**1. "Plan not found in configured plans"**
|
|
1147
|
+
- Ensure the plan ID exists in Razorpay dashboard
|
|
1148
|
+
- Add the plan ID to the `plans` array in plugin configuration
|
|
1149
|
+
- Re-run Better Auth CLI after updating plans
|
|
1150
|
+
|
|
1151
|
+
**2. "Webhook signature verification failed"**
|
|
1152
|
+
- Verify webhook secret matches Razorpay dashboard
|
|
1153
|
+
- Ensure webhook URL is correct: `https://yourdomain.com/api/auth/razorpay/webhook`
|
|
1154
|
+
- Check that request body is not modified
|
|
1155
|
+
- Verify `x-razorpay-signature` header is present
|
|
1156
|
+
|
|
1157
|
+
**3. "Subscription already exists"**
|
|
1158
|
+
- User already has an active subscription
|
|
1159
|
+
- Cancel or pause existing subscription first
|
|
1160
|
+
- Check subscription status before creating new one
|
|
1161
|
+
|
|
1162
|
+
**4. "User not authenticated"**
|
|
1163
|
+
- Ensure user is logged in via Better Auth
|
|
1164
|
+
- Check session middleware is properly configured
|
|
1165
|
+
- Verify `sessionMiddleware` is used in endpoint configuration
|
|
1166
|
+
|
|
1167
|
+
**5. "Subscription not found"**
|
|
1168
|
+
- Subscription may have been deleted
|
|
1169
|
+
- Check subscription ID is correct
|
|
1170
|
+
- Verify subscription belongs to the user
|
|
1171
|
+
|
|
1172
|
+
**6. Database Schema Issues**
|
|
1173
|
+
- Run `npx @better-auth/cli@latest migrate` after adding plugin
|
|
1174
|
+
- For Prisma/Drizzle: Run `npx @better-auth/cli@latest generate`
|
|
1175
|
+
- Check that user additional fields are properly configured
|
|
1176
|
+
|
|
1177
|
+
**7. Type Errors**
|
|
1178
|
+
- Ensure you're using `createAuthClient<typeof auth>()` for type inference
|
|
1179
|
+
- Import types from `better-auth-razorpay`
|
|
1180
|
+
- Check that plugin is properly exported
|
|
1181
|
+
|
|
1182
|
+
## Resources
|
|
1183
|
+
|
|
1184
|
+
- [Better Auth Documentation](https://better-auth.com/docs)
|
|
1185
|
+
- [Better Auth Options Reference](https://better-auth.com/docs/reference/options)
|
|
1186
|
+
- [Better Auth LLMs.txt](https://better-auth.com/llms.txt)
|
|
1187
|
+
- [Razorpay API Documentation](https://razorpay.com/docs/api/)
|
|
1188
|
+
- [Razorpay Subscriptions Guide](https://razorpay.com/docs/payments/subscriptions/)
|
|
1189
|
+
|
|
1190
|
+
## Support
|
|
1191
|
+
|
|
1192
|
+
For issues, questions, or contributions:
|
|
1193
|
+
|
|
1194
|
+
1. Check the [Better Auth documentation](https://better-auth.com/docs)
|
|
1195
|
+
2. Review [Razorpay API documentation](https://razorpay.com/docs/api/)
|
|
1196
|
+
3. Open an issue on GitHub
|
|
1197
|
+
|
|
1198
|
+
## License
|
|
1199
|
+
|
|
1200
|
+
This plugin is part of the Better Auth ecosystem and follows the same license as Better Auth.
|
|
1201
|
+
|