@deiondz/better-auth-razorpay 2.0.1 → 2.0.3
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 +49 -18
- package/api/index.ts +1 -0
- package/api/verify-payment.ts +87 -0
- package/client/hooks.ts +79 -22
- package/client/types.ts +45 -1
- package/client.ts +97 -0
- package/index.ts +6 -0
- package/lib/types.ts +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -84,6 +84,7 @@ export const auth = betterAuth({
|
|
|
84
84
|
razorpayPlugin({
|
|
85
85
|
razorpayClient,
|
|
86
86
|
razorpayWebhookSecret: process.env.RAZORPAY_WEBHOOK_SECRET,
|
|
87
|
+
razorpayKeySecret: process.env.RAZORPAY_KEY_SECRET, // optional: enables verify-payment endpoint
|
|
87
88
|
createCustomerOnSignUp: true, // optional
|
|
88
89
|
subscription: {
|
|
89
90
|
enabled: true,
|
|
@@ -111,7 +112,9 @@ export const auth = betterAuth({
|
|
|
111
112
|
})
|
|
112
113
|
```
|
|
113
114
|
|
|
114
|
-
3. **Add Client Plugin**
|
|
115
|
+
3. **Add Client Plugin (required to avoid 404s)**
|
|
116
|
+
|
|
117
|
+
Add the Razorpay client plugin so requests use the correct paths. **Without it, calls like `authClient.api.get('/razorpay/get-plans')` can hit wrong URLs (e.g. `POST /api/auth/api/get` 404).** The plugin exposes `authClient.razorpay.*` methods that use the plugin’s route map.
|
|
115
118
|
|
|
116
119
|
```typescript
|
|
117
120
|
// src/lib/auth-client.ts
|
|
@@ -141,6 +144,7 @@ BETTER_AUTH_URL=https://yourdomain.com
|
|
|
141
144
|
RAZORPAY_KEY_ID=rzp_test_xxxxxxxxxxxx
|
|
142
145
|
RAZORPAY_KEY_SECRET=your_secret_key
|
|
143
146
|
RAZORPAY_WEBHOOK_SECRET=your_webhook_secret
|
|
147
|
+
# Pass RAZORPAY_KEY_SECRET as razorpayKeySecret in plugin options to enable the verify-payment endpoint (same as client key, not webhook secret).
|
|
144
148
|
```
|
|
145
149
|
|
|
146
150
|
5. **Run Database Migration**
|
|
@@ -163,6 +167,7 @@ npx @better-auth/cli@latest generate
|
|
|
163
167
|
interface RazorpayPluginOptions {
|
|
164
168
|
razorpayClient: Razorpay // Required: Initialized Razorpay instance (key_id, key_secret)
|
|
165
169
|
razorpayWebhookSecret?: string // Optional: Webhook secret for signature verification
|
|
170
|
+
razorpayKeySecret?: string // Optional: API key secret for payment signature verification; when set, enables POST /razorpay/verify-payment (same secret as Razorpay client, not webhook secret)
|
|
166
171
|
createCustomerOnSignUp?: boolean // Optional: Create Razorpay customer on user sign-up (default: false)
|
|
167
172
|
onCustomerCreate?: (args) => Promise<void>
|
|
168
173
|
getCustomerCreateParams?: (args) => Promise<{ params?: Record<string, unknown> }>
|
|
@@ -197,6 +202,7 @@ Example: using callbacks in your config:
|
|
|
197
202
|
razorpayPlugin({
|
|
198
203
|
razorpayClient,
|
|
199
204
|
razorpayWebhookSecret: process.env.RAZORPAY_WEBHOOK_SECRET,
|
|
205
|
+
razorpayKeySecret: process.env.RAZORPAY_KEY_SECRET, // optional: enables verify-payment endpoint
|
|
200
206
|
createCustomerOnSignUp: true,
|
|
201
207
|
onCustomerCreate: async ({ user, razorpayCustomer }) => {
|
|
202
208
|
console.log(`Razorpay customer created for user ${user.id}: ${razorpayCustomer.id}`)
|
|
@@ -429,7 +435,7 @@ if (response.success && response.data) {
|
|
|
429
435
|
|
|
430
436
|
### 4. Verify Payment
|
|
431
437
|
|
|
432
|
-
Verify payment signature after Razorpay checkout completion.
|
|
438
|
+
Verify payment signature after Razorpay checkout completion. This endpoint is **only registered when `razorpayKeySecret`** is set in plugin options. Use the same API key secret as your Razorpay client (not the webhook secret).
|
|
433
439
|
|
|
434
440
|
**Endpoint:** `POST /api/auth/razorpay/verify-payment`
|
|
435
441
|
|
|
@@ -484,6 +490,7 @@ const handlePaymentSuccess = async (razorpayResponse: {
|
|
|
484
490
|
- `SIGNATURE_VERIFICATION_FAILED` - Invalid payment signature
|
|
485
491
|
- `UNAUTHORIZED` - User not authenticated
|
|
486
492
|
- `SUBSCRIPTION_NOT_FOUND` - Subscription record not found
|
|
493
|
+
- `FORBIDDEN` - Subscription does not belong to authenticated user
|
|
487
494
|
|
|
488
495
|
---
|
|
489
496
|
|
|
@@ -663,23 +670,42 @@ Handle Razorpay webhook events (automatically called by Razorpay).
|
|
|
663
670
|
|
|
664
671
|
## Client Usage
|
|
665
672
|
|
|
666
|
-
###
|
|
673
|
+
### Preferred: authClient.razorpay.* (method-based API)
|
|
667
674
|
|
|
668
|
-
|
|
675
|
+
When you add `razorpayClientPlugin()` to `createAuthClient({ plugins: [...] })`, the client gets `authClient.razorpay` with explicit methods. **Use these so requests hit the correct paths** (avoids 404s like `POST /api/auth/api/get`):
|
|
669
676
|
|
|
670
677
|
```typescript
|
|
671
678
|
import { authClient } from '@/lib/auth-client'
|
|
672
679
|
|
|
673
|
-
// GET
|
|
674
|
-
const
|
|
680
|
+
// GET plans
|
|
681
|
+
const plansRes = await authClient.razorpay.getPlans()
|
|
682
|
+
if (plansRes.success) console.log(plansRes.data)
|
|
683
|
+
|
|
684
|
+
// List subscriptions
|
|
685
|
+
const listRes = await authClient.razorpay.listSubscriptions({ referenceId: 'optional' })
|
|
675
686
|
|
|
676
|
-
//
|
|
677
|
-
const result = await authClient.
|
|
678
|
-
|
|
687
|
+
// Create or update subscription (returns checkoutUrl for Razorpay payment page)
|
|
688
|
+
const result = await authClient.razorpay.createOrUpdateSubscription({
|
|
689
|
+
plan: 'Starter',
|
|
690
|
+
annual: false,
|
|
691
|
+
seats: 1,
|
|
692
|
+
})
|
|
693
|
+
if (result.success) window.location.href = result.data.checkoutUrl
|
|
694
|
+
|
|
695
|
+
// Cancel, restore, verify payment
|
|
696
|
+
await authClient.razorpay.cancelSubscription({ subscriptionId: 'sub_xxx', immediately: false })
|
|
697
|
+
await authClient.razorpay.restoreSubscription({ subscriptionId: 'sub_xxx' })
|
|
698
|
+
await authClient.razorpay.verifyPayment({
|
|
699
|
+
razorpay_payment_id: 'pay_xxx',
|
|
700
|
+
razorpay_subscription_id: 'sub_xxx',
|
|
701
|
+
razorpay_signature: '...',
|
|
679
702
|
})
|
|
680
|
-
// result.data.checkoutUrl -> redirect user to Razorpay payment page
|
|
681
703
|
```
|
|
682
704
|
|
|
705
|
+
### Fallback: authClient.api (only if client plugin is not used)
|
|
706
|
+
|
|
707
|
+
If you do not add the client plugin, you can use `authClient.api.get/post` with the Razorpay paths. **This can lead to 404s** (e.g. `POST /api/auth/api/get`) depending on how your auth client resolves paths. Prefer adding the client plugin and using `authClient.razorpay.*` instead.
|
|
708
|
+
|
|
683
709
|
### Type Safety
|
|
684
710
|
|
|
685
711
|
Infer types from your auth configuration:
|
|
@@ -699,7 +725,7 @@ type User = typeof authClient.$Infer.Session.user
|
|
|
699
725
|
|
|
700
726
|
## TanStack Query Hooks
|
|
701
727
|
|
|
702
|
-
The plugin works with **TanStack Query
|
|
728
|
+
The plugin works with **TanStack Query**. We provide optional pre-built hooks that accept your auth client; when you use `razorpayClientPlugin()`, the hooks call `authClient.razorpay.*` so requests hit the correct paths. If you prefer a different setup, use `authClient.razorpay.getPlans()`, `authClient.razorpay.createOrUpdateSubscription(...)`, etc., or build your own hooks around those methods.
|
|
703
729
|
|
|
704
730
|
To use our pre-built hooks, install peer dependencies:
|
|
705
731
|
|
|
@@ -1352,38 +1378,43 @@ async function initializeRazorpayCheckout(subscriptionId: string) {
|
|
|
1352
1378
|
|
|
1353
1379
|
### Common Issues
|
|
1354
1380
|
|
|
1355
|
-
**1. "
|
|
1381
|
+
**1. "POST /api/auth/api/get 404" or Razorpay requests returning 404**
|
|
1382
|
+
- Add the **client plugin** to your auth client: `createAuthClient({ plugins: [razorpayClientPlugin(), ...] })`
|
|
1383
|
+
- Use **method-based API** instead of `authClient.api.get/post`: call `authClient.razorpay.getPlans()`, `authClient.razorpay.createOrUpdateSubscription(...)`, etc., so requests use the plugin’s route map and hit the correct paths
|
|
1384
|
+
- The TanStack hooks (`usePlans`, `useSubscriptions`, etc.) use `authClient.razorpay` when present, so they work correctly once the client plugin is added
|
|
1385
|
+
|
|
1386
|
+
**2. "Plan not found in configured plans"**
|
|
1356
1387
|
- Ensure the plan ID exists in Razorpay dashboard
|
|
1357
1388
|
- Add the plan ID to the `plans` array in plugin configuration
|
|
1358
1389
|
- Re-run Better Auth CLI after updating plans
|
|
1359
1390
|
|
|
1360
|
-
**
|
|
1391
|
+
**3. "Webhook signature verification failed"**
|
|
1361
1392
|
- Verify webhook secret matches Razorpay dashboard
|
|
1362
1393
|
- Ensure webhook URL is correct: `https://yourdomain.com/api/auth/razorpay/webhook`
|
|
1363
1394
|
- Check that request body is not modified
|
|
1364
1395
|
- Verify `x-razorpay-signature` header is present
|
|
1365
1396
|
|
|
1366
|
-
**
|
|
1397
|
+
**4. "Subscription already exists"**
|
|
1367
1398
|
- User already has an active subscription
|
|
1368
1399
|
- Cancel or pause existing subscription first
|
|
1369
1400
|
- Check subscription status before creating new one
|
|
1370
1401
|
|
|
1371
|
-
**
|
|
1402
|
+
**5. "User not authenticated"**
|
|
1372
1403
|
- Ensure user is logged in via Better Auth
|
|
1373
1404
|
- Check session middleware is properly configured
|
|
1374
1405
|
- Verify `sessionMiddleware` is used in endpoint configuration
|
|
1375
1406
|
|
|
1376
|
-
**
|
|
1407
|
+
**6. "Subscription not found"**
|
|
1377
1408
|
- Subscription may have been deleted
|
|
1378
1409
|
- Check subscription ID is correct
|
|
1379
1410
|
- Verify subscription belongs to the user
|
|
1380
1411
|
|
|
1381
|
-
**
|
|
1412
|
+
**7. Database Schema Issues**
|
|
1382
1413
|
- Run `npx @better-auth/cli@latest migrate` after adding plugin
|
|
1383
1414
|
- For Prisma/Drizzle: Run `npx @better-auth/cli@latest generate`
|
|
1384
1415
|
- Check that user additional fields are properly configured
|
|
1385
1416
|
|
|
1386
|
-
**
|
|
1417
|
+
**8. Type Errors**
|
|
1387
1418
|
- Ensure you're using `createAuthClient<typeof auth>()` for type inference
|
|
1388
1419
|
- Import types from `@deiondz/better-auth-razorpay`
|
|
1389
1420
|
- Check that plugin is properly exported
|
package/api/index.ts
CHANGED
|
@@ -3,4 +3,5 @@ export { createOrUpdateSubscription } from './create-or-update-subscription'
|
|
|
3
3
|
export { getPlans } from './get-plans'
|
|
4
4
|
export { listSubscriptions } from './list-subscriptions'
|
|
5
5
|
export { restoreSubscription } from './restore-subscription'
|
|
6
|
+
export { verifyPayment } from './verify-payment'
|
|
6
7
|
export { webhook } from './webhook'
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { createHmac } from 'node:crypto'
|
|
2
|
+
import { createAuthEndpoint, sessionMiddleware } from 'better-auth/api'
|
|
3
|
+
import {
|
|
4
|
+
handleRazorpayError,
|
|
5
|
+
verifyPaymentSchema,
|
|
6
|
+
type SubscriptionRecord,
|
|
7
|
+
} from '../lib'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* POST /api/auth/razorpay/verify-payment
|
|
11
|
+
* Verifies payment signature after Razorpay subscription checkout completion.
|
|
12
|
+
* Requires razorpayKeySecret to be set in plugin options.
|
|
13
|
+
*/
|
|
14
|
+
export const verifyPayment = (keySecret: string) =>
|
|
15
|
+
createAuthEndpoint(
|
|
16
|
+
'/razorpay/verify-payment',
|
|
17
|
+
{ method: 'POST', use: [sessionMiddleware] },
|
|
18
|
+
async (ctx) => {
|
|
19
|
+
try {
|
|
20
|
+
const body = verifyPaymentSchema.parse(ctx.body)
|
|
21
|
+
const { razorpay_payment_id, razorpay_subscription_id, razorpay_signature } = body
|
|
22
|
+
|
|
23
|
+
const generatedSignature = createHmac('sha256', keySecret)
|
|
24
|
+
.update(`${razorpay_payment_id}|${razorpay_subscription_id}`)
|
|
25
|
+
.digest('hex')
|
|
26
|
+
|
|
27
|
+
if (generatedSignature !== razorpay_signature) {
|
|
28
|
+
return {
|
|
29
|
+
success: false,
|
|
30
|
+
error: {
|
|
31
|
+
code: 'SIGNATURE_VERIFICATION_FAILED',
|
|
32
|
+
description: 'Payment signature verification failed',
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const userId = ctx.context.session?.user?.id
|
|
38
|
+
if (!userId) {
|
|
39
|
+
return {
|
|
40
|
+
success: false,
|
|
41
|
+
error: { code: 'UNAUTHORIZED', description: 'User not authenticated' },
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const record = (await ctx.context.adapter.findOne({
|
|
46
|
+
model: 'subscription',
|
|
47
|
+
where: [{ field: 'razorpaySubscriptionId', value: razorpay_subscription_id }],
|
|
48
|
+
})) as SubscriptionRecord | null
|
|
49
|
+
|
|
50
|
+
if (!record) {
|
|
51
|
+
return {
|
|
52
|
+
success: false,
|
|
53
|
+
error: { code: 'SUBSCRIPTION_NOT_FOUND', description: 'Subscription not found' },
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (record.referenceId !== userId) {
|
|
58
|
+
return {
|
|
59
|
+
success: false,
|
|
60
|
+
error: { code: 'FORBIDDEN', description: 'Subscription does not belong to you' },
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
await ctx.context.adapter.update({
|
|
65
|
+
model: 'subscription',
|
|
66
|
+
where: [{ field: 'razorpaySubscriptionId', value: razorpay_subscription_id }],
|
|
67
|
+
update: {
|
|
68
|
+
data: {
|
|
69
|
+
status: 'pending',
|
|
70
|
+
updatedAt: new Date(),
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
success: true,
|
|
77
|
+
data: {
|
|
78
|
+
message: 'Payment verified successfully',
|
|
79
|
+
payment_id: razorpay_payment_id,
|
|
80
|
+
subscription_id: razorpay_subscription_id,
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
} catch (error) {
|
|
84
|
+
return handleRazorpayError(error)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
)
|
package/client/hooks.ts
CHANGED
|
@@ -17,11 +17,13 @@ import type {
|
|
|
17
17
|
CancelSubscriptionInput,
|
|
18
18
|
RestoreSubscriptionInput,
|
|
19
19
|
ListSubscriptionsInput,
|
|
20
|
+
VerifyPaymentInput,
|
|
20
21
|
GetPlansResponse,
|
|
21
22
|
ListSubscriptionsResponse,
|
|
22
23
|
CreateOrUpdateSubscriptionResponse,
|
|
23
24
|
CancelSubscriptionResponse,
|
|
24
25
|
RestoreSubscriptionResponse,
|
|
26
|
+
VerifyPaymentResponse,
|
|
25
27
|
RazorpayApiError,
|
|
26
28
|
} from './types'
|
|
27
29
|
|
|
@@ -44,65 +46,90 @@ function assertSuccess<T>(res: unknown): asserts res is { success: true; data: T
|
|
|
44
46
|
throw new Error('Invalid response')
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
/** Fetch plans (GET /razorpay/get-plans). */
|
|
49
|
+
/** Fetch plans (GET /razorpay/get-plans). Prefers client.razorpay when available to avoid 404s. */
|
|
48
50
|
async function fetchPlans(client: RazorpayAuthClient): Promise<PlanSummary[]> {
|
|
49
|
-
const res =
|
|
51
|
+
const res = client.razorpay
|
|
52
|
+
? await client.razorpay.getPlans()
|
|
53
|
+
: await client.api.get(`${BASE}/get-plans`)
|
|
50
54
|
assertSuccess<PlanSummary[]>(res)
|
|
51
55
|
return res.data
|
|
52
56
|
}
|
|
53
57
|
|
|
54
|
-
/** Fetch subscriptions list (GET /razorpay/subscription/list). */
|
|
58
|
+
/** Fetch subscriptions list (GET /razorpay/subscription/list). Prefers client.razorpay when available. */
|
|
55
59
|
async function fetchSubscriptions(
|
|
56
60
|
client: RazorpayAuthClient,
|
|
57
61
|
input?: ListSubscriptionsInput
|
|
58
62
|
): Promise<ListSubscriptionsResponse['data']> {
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
const res = client.razorpay
|
|
64
|
+
? await client.razorpay.listSubscriptions(input)
|
|
65
|
+
: (() => {
|
|
66
|
+
const query: Record<string, string> = {}
|
|
67
|
+
if (input?.referenceId) query.referenceId = input.referenceId
|
|
68
|
+
const path = `${BASE}/subscription/list`
|
|
69
|
+
return Object.keys(query).length > 0
|
|
70
|
+
? client.api.get(path, { query })
|
|
71
|
+
: client.api.get(path)
|
|
72
|
+
})()
|
|
66
73
|
assertSuccess<ListSubscriptionsResponse['data']>(res)
|
|
67
74
|
return res.data
|
|
68
75
|
}
|
|
69
76
|
|
|
70
|
-
/** Create or update subscription (POST /razorpay/subscription/create-or-update). */
|
|
77
|
+
/** Create or update subscription (POST /razorpay/subscription/create-or-update). Prefers client.razorpay when available. */
|
|
71
78
|
async function createOrUpdateSubscription(
|
|
72
79
|
client: RazorpayAuthClient,
|
|
73
80
|
input: CreateOrUpdateSubscriptionInput
|
|
74
81
|
): Promise<CreateOrUpdateSubscriptionResponse['data']> {
|
|
75
|
-
const res =
|
|
76
|
-
|
|
77
|
-
|
|
82
|
+
const res = client.razorpay
|
|
83
|
+
? await client.razorpay.createOrUpdateSubscription(input)
|
|
84
|
+
: await client.api.post(`${BASE}/subscription/create-or-update`, {
|
|
85
|
+
body: input as unknown as Record<string, unknown>,
|
|
86
|
+
})
|
|
78
87
|
assertSuccess<CreateOrUpdateSubscriptionResponse['data']>(res)
|
|
79
88
|
return res.data
|
|
80
89
|
}
|
|
81
90
|
|
|
82
|
-
/** Cancel subscription (POST /razorpay/subscription/cancel). */
|
|
91
|
+
/** Cancel subscription (POST /razorpay/subscription/cancel). Prefers client.razorpay when available. */
|
|
83
92
|
async function cancelSubscription(
|
|
84
93
|
client: RazorpayAuthClient,
|
|
85
94
|
input: CancelSubscriptionInput
|
|
86
95
|
): Promise<CancelSubscriptionResponse['data']> {
|
|
87
|
-
const res =
|
|
88
|
-
|
|
89
|
-
|
|
96
|
+
const res = client.razorpay
|
|
97
|
+
? await client.razorpay.cancelSubscription(input)
|
|
98
|
+
: await client.api.post(`${BASE}/subscription/cancel`, {
|
|
99
|
+
body: input as unknown as Record<string, unknown>,
|
|
100
|
+
})
|
|
90
101
|
assertSuccess<CancelSubscriptionResponse['data']>(res)
|
|
91
102
|
return res.data
|
|
92
103
|
}
|
|
93
104
|
|
|
94
|
-
/** Restore subscription (POST /razorpay/subscription/restore). */
|
|
105
|
+
/** Restore subscription (POST /razorpay/subscription/restore). Prefers client.razorpay when available. */
|
|
95
106
|
async function restoreSubscription(
|
|
96
107
|
client: RazorpayAuthClient,
|
|
97
108
|
input: RestoreSubscriptionInput
|
|
98
109
|
): Promise<RestoreSubscriptionResponse['data']> {
|
|
99
|
-
const res =
|
|
100
|
-
|
|
101
|
-
|
|
110
|
+
const res = client.razorpay
|
|
111
|
+
? await client.razorpay.restoreSubscription(input)
|
|
112
|
+
: await client.api.post(`${BASE}/subscription/restore`, {
|
|
113
|
+
body: input as unknown as Record<string, unknown>,
|
|
114
|
+
})
|
|
102
115
|
assertSuccess<RestoreSubscriptionResponse['data']>(res)
|
|
103
116
|
return res.data
|
|
104
117
|
}
|
|
105
118
|
|
|
119
|
+
/** Verify payment (POST /razorpay/verify-payment). Prefers client.razorpay when available. */
|
|
120
|
+
async function verifyPayment(
|
|
121
|
+
client: RazorpayAuthClient,
|
|
122
|
+
input: VerifyPaymentInput
|
|
123
|
+
): Promise<VerifyPaymentResponse['data']> {
|
|
124
|
+
const res = client.razorpay
|
|
125
|
+
? await client.razorpay.verifyPayment(input)
|
|
126
|
+
: await client.api.post(`${BASE}/verify-payment`, {
|
|
127
|
+
body: input as unknown as Record<string, unknown>,
|
|
128
|
+
})
|
|
129
|
+
assertSuccess<VerifyPaymentResponse['data']>(res)
|
|
130
|
+
return res.data
|
|
131
|
+
}
|
|
132
|
+
|
|
106
133
|
export type UsePlansOptions = Omit<
|
|
107
134
|
UseQueryOptions<PlanSummary[], Error, PlanSummary[], readonly string[]>,
|
|
108
135
|
'queryKey' | 'queryFn'
|
|
@@ -214,16 +241,19 @@ export type UseRestoreSubscriptionOptions = UseMutationOptions<
|
|
|
214
241
|
// Re-export client types for convenience when importing from this entry
|
|
215
242
|
export type {
|
|
216
243
|
RazorpayAuthClient,
|
|
244
|
+
RazorpayClientActions,
|
|
217
245
|
PlanSummary,
|
|
218
246
|
CreateOrUpdateSubscriptionInput,
|
|
219
247
|
CancelSubscriptionInput,
|
|
220
248
|
RestoreSubscriptionInput,
|
|
221
249
|
ListSubscriptionsInput,
|
|
250
|
+
VerifyPaymentInput,
|
|
222
251
|
GetPlansResponse,
|
|
223
252
|
ListSubscriptionsResponse,
|
|
224
253
|
CreateOrUpdateSubscriptionResponse,
|
|
225
254
|
CancelSubscriptionResponse,
|
|
226
255
|
RestoreSubscriptionResponse,
|
|
256
|
+
VerifyPaymentResponse,
|
|
227
257
|
RazorpayApiError,
|
|
228
258
|
RazorpayApiResult,
|
|
229
259
|
} from './types'
|
|
@@ -246,3 +276,30 @@ export function useRestoreSubscription(
|
|
|
246
276
|
},
|
|
247
277
|
})
|
|
248
278
|
}
|
|
279
|
+
|
|
280
|
+
export type UseVerifyPaymentOptions = UseMutationOptions<
|
|
281
|
+
VerifyPaymentResponse['data'],
|
|
282
|
+
Error,
|
|
283
|
+
VerifyPaymentInput,
|
|
284
|
+
unknown
|
|
285
|
+
>
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Verify payment signature after Razorpay checkout success.
|
|
289
|
+
* Call with the payload from the Razorpay success handler (razorpay_payment_id, razorpay_subscription_id, razorpay_signature).
|
|
290
|
+
* Invalidates subscriptions list on success.
|
|
291
|
+
*/
|
|
292
|
+
export function useVerifyPayment(
|
|
293
|
+
client: RazorpayAuthClient | null | undefined,
|
|
294
|
+
options?: UseVerifyPaymentOptions
|
|
295
|
+
) {
|
|
296
|
+
const queryClient = useQueryClient()
|
|
297
|
+
return useMutation({
|
|
298
|
+
mutationFn: (input: VerifyPaymentInput) => verifyPayment(client!, input),
|
|
299
|
+
...options,
|
|
300
|
+
onSuccess: (data, variables, onMutateResult, context) => {
|
|
301
|
+
queryClient.invalidateQueries({ queryKey: razorpayQueryKeys.subscriptions() })
|
|
302
|
+
options?.onSuccess?.(data, variables, onMutateResult, context)
|
|
303
|
+
},
|
|
304
|
+
})
|
|
305
|
+
}
|
package/client/types.ts
CHANGED
|
@@ -64,9 +64,34 @@ export type RazorpayApiResult<T = unknown> =
|
|
|
64
64
|
| { success: true; data: T }
|
|
65
65
|
| RazorpayApiError
|
|
66
66
|
|
|
67
|
+
/** Razorpay API actions from the client plugin (authClient.razorpay). Use these so requests hit the correct paths. */
|
|
68
|
+
export interface RazorpayClientActions {
|
|
69
|
+
getPlans: (fetchOptions?: { query?: Record<string, string> }) => Promise<RazorpayApiResult<PlanSummary[]>>
|
|
70
|
+
listSubscriptions: (
|
|
71
|
+
input?: ListSubscriptionsInput,
|
|
72
|
+
fetchOptions?: { query?: Record<string, string> }
|
|
73
|
+
) => Promise<RazorpayApiResult<ListSubscriptionsResponse['data']>>
|
|
74
|
+
createOrUpdateSubscription: (
|
|
75
|
+
input: CreateOrUpdateSubscriptionInput,
|
|
76
|
+
fetchOptions?: { body?: Record<string, unknown> }
|
|
77
|
+
) => Promise<RazorpayApiResult<CreateOrUpdateSubscriptionResponse['data']>>
|
|
78
|
+
cancelSubscription: (
|
|
79
|
+
input: CancelSubscriptionInput,
|
|
80
|
+
fetchOptions?: { body?: Record<string, unknown> }
|
|
81
|
+
) => Promise<RazorpayApiResult<CancelSubscriptionResponse['data']>>
|
|
82
|
+
restoreSubscription: (
|
|
83
|
+
input: RestoreSubscriptionInput,
|
|
84
|
+
fetchOptions?: { body?: Record<string, unknown> }
|
|
85
|
+
) => Promise<RazorpayApiResult<RestoreSubscriptionResponse['data']>>
|
|
86
|
+
verifyPayment: (
|
|
87
|
+
input: VerifyPaymentInput,
|
|
88
|
+
fetchOptions?: { body?: Record<string, unknown> }
|
|
89
|
+
) => Promise<RazorpayApiResult<VerifyPaymentResponse['data']>>
|
|
90
|
+
}
|
|
91
|
+
|
|
67
92
|
/**
|
|
68
93
|
* Minimal auth client interface for Razorpay hooks.
|
|
69
|
-
*
|
|
94
|
+
* When using the client plugin (razorpayClientPlugin()), authClient.razorpay is set and hooks use it so requests hit the correct paths (avoids 404s from api.get/post).
|
|
70
95
|
*/
|
|
71
96
|
export interface RazorpayAuthClient {
|
|
72
97
|
api: {
|
|
@@ -79,6 +104,8 @@ export interface RazorpayAuthClient {
|
|
|
79
104
|
options?: { body?: Record<string, unknown> }
|
|
80
105
|
) => Promise<RazorpayApiResult<unknown>>
|
|
81
106
|
}
|
|
107
|
+
/** Set when razorpayClientPlugin() is used in createAuthClient({ plugins: [razorpayClientPlugin()] }). Prefer these methods over api.get/post. */
|
|
108
|
+
razorpay?: RazorpayClientActions
|
|
82
109
|
}
|
|
83
110
|
|
|
84
111
|
/** Input for create-or-update subscription. */
|
|
@@ -106,3 +133,20 @@ export interface RestoreSubscriptionInput {
|
|
|
106
133
|
export interface ListSubscriptionsInput {
|
|
107
134
|
referenceId?: string
|
|
108
135
|
}
|
|
136
|
+
|
|
137
|
+
/** Input for verify-payment (Razorpay checkout success callback payload). */
|
|
138
|
+
export interface VerifyPaymentInput {
|
|
139
|
+
razorpay_payment_id: string
|
|
140
|
+
razorpay_subscription_id: string
|
|
141
|
+
razorpay_signature: string
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Response shape for verify-payment (success). */
|
|
145
|
+
export interface VerifyPaymentResponse {
|
|
146
|
+
success: true
|
|
147
|
+
data: {
|
|
148
|
+
message: string
|
|
149
|
+
payment_id: string
|
|
150
|
+
subscription_id: string
|
|
151
|
+
}
|
|
152
|
+
}
|
package/client.ts
CHANGED
|
@@ -1,8 +1,105 @@
|
|
|
1
1
|
import type { BetterAuthClientPlugin } from 'better-auth/client'
|
|
2
2
|
import type { razorpayPlugin } from './index'
|
|
3
|
+
import type {
|
|
4
|
+
PlanSummary,
|
|
5
|
+
CreateOrUpdateSubscriptionInput,
|
|
6
|
+
CancelSubscriptionInput,
|
|
7
|
+
RestoreSubscriptionInput,
|
|
8
|
+
ListSubscriptionsInput,
|
|
9
|
+
VerifyPaymentInput,
|
|
10
|
+
ListSubscriptionsResponse,
|
|
11
|
+
CreateOrUpdateSubscriptionResponse,
|
|
12
|
+
CancelSubscriptionResponse,
|
|
13
|
+
RestoreSubscriptionResponse,
|
|
14
|
+
VerifyPaymentResponse,
|
|
15
|
+
RazorpayApiResult,
|
|
16
|
+
} from './client/types'
|
|
3
17
|
|
|
18
|
+
type FetchFn = (
|
|
19
|
+
path: string,
|
|
20
|
+
options?: {
|
|
21
|
+
method?: string
|
|
22
|
+
body?: Record<string, unknown>
|
|
23
|
+
query?: Record<string, string>
|
|
24
|
+
}
|
|
25
|
+
) => Promise<RazorpayApiResult<unknown>>
|
|
26
|
+
|
|
27
|
+
const PATHS = {
|
|
28
|
+
getPlans: '/razorpay/get-plans',
|
|
29
|
+
listSubscriptions: '/razorpay/subscription/list',
|
|
30
|
+
createOrUpdateSubscription: '/razorpay/subscription/create-or-update',
|
|
31
|
+
cancelSubscription: '/razorpay/subscription/cancel',
|
|
32
|
+
restoreSubscription: '/razorpay/subscription/restore',
|
|
33
|
+
verifyPayment: '/razorpay/verify-payment',
|
|
34
|
+
} as const
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Razorpay client plugin for Better Auth.
|
|
38
|
+
* Exposes authClient.razorpay.* so requests use the correct paths and avoid 404s from api.get/post.
|
|
39
|
+
* Add to createAuthClient: plugins: [razorpayClientPlugin()]
|
|
40
|
+
*/
|
|
4
41
|
export const razorpayClientPlugin = () =>
|
|
5
42
|
({
|
|
6
43
|
id: 'razorpay-plugin',
|
|
7
44
|
$InferServerPlugin: {} as ReturnType<typeof razorpayPlugin>,
|
|
45
|
+
getActions: ($fetch: FetchFn) => ({
|
|
46
|
+
razorpay: {
|
|
47
|
+
getPlans: (fetchOptions?: Parameters<FetchFn>[1]) =>
|
|
48
|
+
$fetch(PATHS.getPlans, { method: 'GET', ...fetchOptions }) as Promise<
|
|
49
|
+
RazorpayApiResult<PlanSummary[]>
|
|
50
|
+
>,
|
|
51
|
+
|
|
52
|
+
listSubscriptions: (
|
|
53
|
+
input?: ListSubscriptionsInput,
|
|
54
|
+
fetchOptions?: Parameters<FetchFn>[1]
|
|
55
|
+
) =>
|
|
56
|
+
$fetch(PATHS.listSubscriptions, {
|
|
57
|
+
method: 'GET',
|
|
58
|
+
query: input?.referenceId ? { referenceId: input.referenceId } : undefined,
|
|
59
|
+
...fetchOptions,
|
|
60
|
+
}) as Promise<RazorpayApiResult<ListSubscriptionsResponse['data']>>,
|
|
61
|
+
|
|
62
|
+
createOrUpdateSubscription: (
|
|
63
|
+
input: CreateOrUpdateSubscriptionInput,
|
|
64
|
+
fetchOptions?: Parameters<FetchFn>[1]
|
|
65
|
+
) =>
|
|
66
|
+
$fetch(PATHS.createOrUpdateSubscription, {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
body: input as unknown as Record<string, unknown>,
|
|
69
|
+
...fetchOptions,
|
|
70
|
+
}) as Promise<
|
|
71
|
+
RazorpayApiResult<CreateOrUpdateSubscriptionResponse['data']>
|
|
72
|
+
>,
|
|
73
|
+
|
|
74
|
+
cancelSubscription: (
|
|
75
|
+
input: CancelSubscriptionInput,
|
|
76
|
+
fetchOptions?: Parameters<FetchFn>[1]
|
|
77
|
+
) =>
|
|
78
|
+
$fetch(PATHS.cancelSubscription, {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
body: input as unknown as Record<string, unknown>,
|
|
81
|
+
...fetchOptions,
|
|
82
|
+
}) as Promise<RazorpayApiResult<CancelSubscriptionResponse['data']>>,
|
|
83
|
+
|
|
84
|
+
restoreSubscription: (
|
|
85
|
+
input: RestoreSubscriptionInput,
|
|
86
|
+
fetchOptions?: Parameters<FetchFn>[1]
|
|
87
|
+
) =>
|
|
88
|
+
$fetch(PATHS.restoreSubscription, {
|
|
89
|
+
method: 'POST',
|
|
90
|
+
body: input as unknown as Record<string, unknown>,
|
|
91
|
+
...fetchOptions,
|
|
92
|
+
}) as Promise<RazorpayApiResult<RestoreSubscriptionResponse['data']>>,
|
|
93
|
+
|
|
94
|
+
verifyPayment: (
|
|
95
|
+
input: VerifyPaymentInput,
|
|
96
|
+
fetchOptions?: Parameters<FetchFn>[1]
|
|
97
|
+
) =>
|
|
98
|
+
$fetch(PATHS.verifyPayment, {
|
|
99
|
+
method: 'POST',
|
|
100
|
+
body: input as unknown as Record<string, unknown>,
|
|
101
|
+
...fetchOptions,
|
|
102
|
+
}) as Promise<RazorpayApiResult<VerifyPaymentResponse['data']>>,
|
|
103
|
+
},
|
|
104
|
+
}),
|
|
8
105
|
}) satisfies BetterAuthClientPlugin
|
package/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
getPlans,
|
|
6
6
|
listSubscriptions,
|
|
7
7
|
restoreSubscription,
|
|
8
|
+
verifyPayment,
|
|
8
9
|
webhook,
|
|
9
10
|
} from './api'
|
|
10
11
|
import type { RazorpayPluginOptions, RazorpayUserRecord } from './lib'
|
|
@@ -21,6 +22,7 @@ import type { RazorpayPluginOptions, RazorpayUserRecord } from './lib'
|
|
|
21
22
|
* @param options - Plugin configuration
|
|
22
23
|
* @param options.razorpayClient - Initialized Razorpay instance (key_id, key_secret)
|
|
23
24
|
* @param options.razorpayWebhookSecret - Webhook secret for signature verification
|
|
25
|
+
* @param options.razorpayKeySecret - API key secret for payment signature verification (optional; when set, enables POST /razorpay/verify-payment)
|
|
24
26
|
* @param options.createCustomerOnSignUp - Create Razorpay customer when user signs up (default: false)
|
|
25
27
|
* @param options.onCustomerCreate - Callback after customer is created
|
|
26
28
|
* @param options.getCustomerCreateParams - Custom params when creating customer
|
|
@@ -31,6 +33,7 @@ export const razorpayPlugin = (options: RazorpayPluginOptions) => {
|
|
|
31
33
|
const {
|
|
32
34
|
razorpayClient,
|
|
33
35
|
razorpayWebhookSecret,
|
|
36
|
+
razorpayKeySecret,
|
|
34
37
|
createCustomerOnSignUp = false,
|
|
35
38
|
onCustomerCreate,
|
|
36
39
|
getCustomerCreateParams,
|
|
@@ -83,6 +86,9 @@ export const razorpayPlugin = (options: RazorpayPluginOptions) => {
|
|
|
83
86
|
'subscription/restore': restoreSubscription(razorpay),
|
|
84
87
|
'subscription/list': listSubscriptions({ subscription: subOpts }),
|
|
85
88
|
'get-plans': getPlans({ subscription: subOpts }),
|
|
89
|
+
...(razorpayKeySecret
|
|
90
|
+
? { 'verify-payment': verifyPayment(razorpayKeySecret) }
|
|
91
|
+
: {}),
|
|
86
92
|
webhook: webhook(razorpayWebhookSecret, options.onWebhookEvent ?? undefined, {
|
|
87
93
|
subscription: subOpts,
|
|
88
94
|
onEvent,
|
package/lib/types.ts
CHANGED
|
@@ -169,6 +169,8 @@ export interface RazorpayPluginOptions {
|
|
|
169
169
|
razorpayClient: import('razorpay')
|
|
170
170
|
/** Webhook secret for signature verification. */
|
|
171
171
|
razorpayWebhookSecret?: string
|
|
172
|
+
/** API key secret for payment signature verification. When set, enables POST /razorpay/verify-payment (same secret as Razorpay client, not webhook secret). */
|
|
173
|
+
razorpayKeySecret?: string
|
|
172
174
|
/** Create Razorpay customer when user signs up. Default: false. */
|
|
173
175
|
createCustomerOnSignUp?: boolean
|
|
174
176
|
/** Called after a Razorpay customer is created. */
|