@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/api/webhook.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { createHmac } from 'node:crypto'
|
|
2
2
|
import { createAuthEndpoint } from 'better-auth/api'
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
OnWebhookEventCallback,
|
|
5
|
+
RazorpayPluginOptions,
|
|
6
|
+
RazorpaySubscription,
|
|
7
|
+
SubscriptionRecord,
|
|
8
|
+
} from '../lib'
|
|
4
9
|
|
|
5
10
|
export interface WebhookResult {
|
|
6
11
|
success: boolean
|
|
@@ -15,83 +20,86 @@ interface SubscriptionEntity {
|
|
|
15
20
|
current_end?: number
|
|
16
21
|
}
|
|
17
22
|
|
|
18
|
-
interface
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}) => Promise<unknown>
|
|
29
|
-
}
|
|
23
|
+
interface WebhookAdapter {
|
|
24
|
+
findOne: (params: {
|
|
25
|
+
model: string
|
|
26
|
+
where: { field: string; value: string }[]
|
|
27
|
+
}) => Promise<unknown>
|
|
28
|
+
update: (params: {
|
|
29
|
+
model: string
|
|
30
|
+
where: { field: string; value: string }[]
|
|
31
|
+
update: { data: Record<string, unknown> }
|
|
32
|
+
}) => Promise<unknown>
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
type EventHandler = (
|
|
33
|
-
adapter:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
adapter: WebhookAdapter,
|
|
37
|
+
razorpaySubscriptionId: string,
|
|
38
|
+
record: SubscriptionRecord,
|
|
36
39
|
subscription: SubscriptionEntity
|
|
37
40
|
) => Promise<void>
|
|
38
41
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
42
|
+
function toLocalStatus(razorpayStatus: string): SubscriptionRecord['status'] {
|
|
43
|
+
const map: Record<string, SubscriptionRecord['status']> = {
|
|
44
|
+
created: 'created',
|
|
45
|
+
authenticated: 'pending',
|
|
46
|
+
active: 'active',
|
|
47
|
+
pending: 'pending',
|
|
48
|
+
halted: 'halted',
|
|
49
|
+
cancelled: 'cancelled',
|
|
50
|
+
completed: 'completed',
|
|
51
|
+
expired: 'expired',
|
|
52
|
+
}
|
|
53
|
+
return map[razorpayStatus] ?? 'pending'
|
|
54
|
+
}
|
|
51
55
|
|
|
56
|
+
const updateSubscriptionRecord = async (
|
|
57
|
+
adapter: WebhookAdapter,
|
|
58
|
+
razorpaySubscriptionId: string,
|
|
59
|
+
data: Record<string, unknown>
|
|
60
|
+
): Promise<void> => {
|
|
52
61
|
await adapter.update({
|
|
53
|
-
model: '
|
|
54
|
-
where: [{ field: '
|
|
55
|
-
update: {
|
|
62
|
+
model: 'subscription',
|
|
63
|
+
where: [{ field: 'razorpaySubscriptionId', value: razorpaySubscriptionId }],
|
|
64
|
+
update: { data: { ...data, updatedAt: new Date() } },
|
|
56
65
|
})
|
|
57
66
|
}
|
|
58
67
|
|
|
59
|
-
const createStatusHandler =
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
const createStatusHandler = (
|
|
69
|
+
status: SubscriptionRecord['status'],
|
|
70
|
+
extraFields?: (sub: SubscriptionEntity) => Record<string, unknown>
|
|
71
|
+
): EventHandler =>
|
|
72
|
+
async (adapter, razorpaySubscriptionId, record, subscription) => {
|
|
73
|
+
const periodStart = subscription.current_start
|
|
74
|
+
? new Date(subscription.current_start * 1000)
|
|
75
|
+
: null
|
|
76
|
+
const periodEnd = subscription.current_end
|
|
77
|
+
? new Date(subscription.current_end * 1000)
|
|
78
|
+
: null
|
|
79
|
+
await updateSubscriptionRecord(adapter, razorpaySubscriptionId, {
|
|
80
|
+
status,
|
|
81
|
+
...(periodStart !== null && { periodStart }),
|
|
82
|
+
...(periodEnd !== null && { periodEnd }),
|
|
83
|
+
...(extraFields?.(subscription) ?? {}),
|
|
84
|
+
})
|
|
67
85
|
}
|
|
68
86
|
|
|
69
87
|
const eventHandlers: Record<string, EventHandler> = {
|
|
70
|
-
'subscription.authenticated': createStatusHandler('
|
|
71
|
-
|
|
72
|
-
subscriptionPlanId: sub.plan_id,
|
|
73
|
-
})),
|
|
74
|
-
'subscription.activated': createStatusHandler('active', (sub) => ({
|
|
75
|
-
subscriptionId: sub.id,
|
|
76
|
-
subscriptionPlanId: sub.plan_id,
|
|
77
|
-
})),
|
|
88
|
+
'subscription.authenticated': createStatusHandler('pending'),
|
|
89
|
+
'subscription.activated': createStatusHandler('active'),
|
|
78
90
|
'subscription.charged': createStatusHandler('active', (sub) => ({
|
|
79
|
-
|
|
80
|
-
nextBillingDate: sub.current_end ? new Date(sub.current_end * 1000) : null,
|
|
81
|
-
subscriptionCurrentPeriodEnd: sub.current_end ? new Date(sub.current_end * 1000) : null,
|
|
91
|
+
periodEnd: sub.current_end ? new Date(sub.current_end * 1000) : undefined,
|
|
82
92
|
})),
|
|
83
93
|
'subscription.cancelled': createStatusHandler('cancelled', () => ({ cancelAtPeriodEnd: false })),
|
|
84
|
-
'subscription.paused': createStatusHandler('
|
|
94
|
+
'subscription.paused': createStatusHandler('halted'),
|
|
85
95
|
'subscription.resumed': createStatusHandler('active'),
|
|
86
96
|
'subscription.pending': createStatusHandler('pending'),
|
|
87
97
|
'subscription.halted': createStatusHandler('halted'),
|
|
98
|
+
'subscription.expired': createStatusHandler('expired'),
|
|
88
99
|
}
|
|
89
100
|
|
|
90
101
|
const getRawBody = async (request: Request | undefined, fallbackBody: unknown): Promise<string> => {
|
|
91
|
-
if (!request)
|
|
92
|
-
return JSON.stringify(fallbackBody)
|
|
93
|
-
}
|
|
94
|
-
|
|
102
|
+
if (!request) return JSON.stringify(fallbackBody)
|
|
95
103
|
try {
|
|
96
104
|
const clonedRequest = request.clone()
|
|
97
105
|
const text = await clonedRequest.text()
|
|
@@ -108,7 +116,7 @@ const verifySignature = (rawBody: string, signature: string, secret: string): bo
|
|
|
108
116
|
|
|
109
117
|
const invokeCallback = async (
|
|
110
118
|
onWebhookEvent: OnWebhookEventCallback,
|
|
111
|
-
adapter:
|
|
119
|
+
adapter: WebhookAdapter,
|
|
112
120
|
event: string,
|
|
113
121
|
subscription: SubscriptionEntity,
|
|
114
122
|
payload: { payment?: { entity?: { id: string; amount: number; currency?: string } } },
|
|
@@ -117,10 +125,8 @@ const invokeCallback = async (
|
|
|
117
125
|
const user = (await adapter.findOne({
|
|
118
126
|
model: 'user',
|
|
119
127
|
where: [{ field: 'id', value: userId }],
|
|
120
|
-
})) as { id: string; email
|
|
121
|
-
|
|
128
|
+
})) as { id: string; email?: string; name?: string } | null
|
|
122
129
|
if (!user) return
|
|
123
|
-
|
|
124
130
|
await onWebhookEvent(
|
|
125
131
|
{
|
|
126
132
|
event: event as Parameters<OnWebhookEventCallback>[0]['event'],
|
|
@@ -135,7 +141,7 @@ const invokeCallback = async (
|
|
|
135
141
|
? {
|
|
136
142
|
id: payload.payment.entity.id,
|
|
137
143
|
amount: payload.payment.entity.amount,
|
|
138
|
-
currency: payload.payment.entity.currency
|
|
144
|
+
currency: payload.payment.entity.currency ?? 'INR',
|
|
139
145
|
}
|
|
140
146
|
: undefined,
|
|
141
147
|
},
|
|
@@ -144,103 +150,80 @@ const invokeCallback = async (
|
|
|
144
150
|
}
|
|
145
151
|
|
|
146
152
|
/**
|
|
147
|
-
* Handles Razorpay webhook events for subscription lifecycle
|
|
148
|
-
*
|
|
149
|
-
* @param webhookSecret - Optional webhook secret for signature verification
|
|
150
|
-
* @param onWebhookEvent - Optional callback function invoked after webhook processing
|
|
151
|
-
* @returns A Better Auth endpoint handler
|
|
152
|
-
*
|
|
153
|
-
* @remarks
|
|
154
|
-
* This endpoint:
|
|
155
|
-
* - Verifies webhook signature using HMAC SHA256
|
|
156
|
-
* - Processes subscription events (authenticated, activated, charged, cancelled, paused, resumed, etc.)
|
|
157
|
-
* - Updates subscription and user records based on event type
|
|
158
|
-
* - Invokes optional callback for custom business logic
|
|
159
|
-
* - Does not require authentication (webhook endpoint)
|
|
160
|
-
*
|
|
161
|
-
* @example
|
|
162
|
-
* Supported events:
|
|
163
|
-
* - subscription.authenticated
|
|
164
|
-
* - subscription.activated
|
|
165
|
-
* - subscription.charged
|
|
166
|
-
* - subscription.cancelled
|
|
167
|
-
* - subscription.paused
|
|
168
|
-
* - subscription.resumed
|
|
169
|
-
* - subscription.pending
|
|
170
|
-
* - subscription.halted
|
|
153
|
+
* Handles Razorpay webhook events for subscription lifecycle.
|
|
154
|
+
* Updates the subscription model only (no user subscription fields).
|
|
171
155
|
*/
|
|
172
|
-
export const webhook = (
|
|
173
|
-
|
|
156
|
+
export const webhook = (
|
|
157
|
+
webhookSecret: string | undefined,
|
|
158
|
+
onWebhookEvent: OnWebhookEventCallback | undefined,
|
|
159
|
+
pluginOptions: Pick<RazorpayPluginOptions, 'subscription' | 'onEvent'>
|
|
160
|
+
) =>
|
|
161
|
+
createAuthEndpoint('/razorpay/webhook', { method: 'POST' }, async (ctx) => {
|
|
174
162
|
if (!webhookSecret) {
|
|
175
163
|
return { success: false, message: 'Webhook secret not configured' }
|
|
176
164
|
}
|
|
177
|
-
|
|
178
|
-
const signature = _ctx.request?.headers.get('x-razorpay-signature')
|
|
165
|
+
const signature = ctx.request?.headers.get('x-razorpay-signature')
|
|
179
166
|
if (!signature) {
|
|
180
167
|
return { success: false, message: 'Missing webhook signature' }
|
|
181
168
|
}
|
|
182
|
-
|
|
183
|
-
const rawBody = await getRawBody(_ctx.request, _ctx.body)
|
|
184
|
-
|
|
169
|
+
const rawBody = await getRawBody(ctx.request, ctx.body)
|
|
185
170
|
if (!verifySignature(rawBody, signature, webhookSecret)) {
|
|
186
171
|
return { success: false, message: 'Invalid webhook signature' }
|
|
187
172
|
}
|
|
188
|
-
|
|
189
|
-
|
|
173
|
+
return processWebhookEvent(
|
|
174
|
+
ctx.context.adapter as unknown as WebhookAdapter,
|
|
175
|
+
rawBody,
|
|
176
|
+
ctx.body,
|
|
177
|
+
onWebhookEvent,
|
|
178
|
+
pluginOptions
|
|
179
|
+
)
|
|
190
180
|
})
|
|
191
181
|
|
|
192
|
-
|
|
193
|
-
adapter:
|
|
182
|
+
async function processWebhookEvent(
|
|
183
|
+
adapter: WebhookAdapter,
|
|
194
184
|
rawBody: string,
|
|
195
185
|
fallbackBody: unknown,
|
|
196
|
-
onWebhookEvent?: OnWebhookEventCallback
|
|
197
|
-
|
|
186
|
+
onWebhookEvent?: OnWebhookEventCallback,
|
|
187
|
+
pluginOptions?: Pick<RazorpayPluginOptions, 'subscription' | 'onEvent'>
|
|
188
|
+
): Promise<WebhookResult> {
|
|
198
189
|
const isDev = process.env.NODE_ENV === 'development'
|
|
199
|
-
|
|
200
190
|
try {
|
|
201
191
|
const webhookData = rawBody ? JSON.parse(rawBody) : fallbackBody
|
|
202
192
|
const { event, payload } = webhookData
|
|
203
|
-
|
|
204
193
|
if (!event || !payload) {
|
|
205
194
|
return {
|
|
206
195
|
success: false,
|
|
207
|
-
message: isDev
|
|
208
|
-
? 'Invalid webhook payload: missing event or payload'
|
|
209
|
-
: 'Invalid webhook payload',
|
|
196
|
+
message: isDev ? 'Invalid webhook payload: missing event or payload' : 'Invalid webhook payload',
|
|
210
197
|
}
|
|
211
198
|
}
|
|
212
199
|
|
|
213
|
-
const
|
|
214
|
-
if (!
|
|
200
|
+
const subscriptionEntity = payload.subscription?.entity as SubscriptionEntity | undefined
|
|
201
|
+
if (!subscriptionEntity) {
|
|
215
202
|
return {
|
|
216
203
|
success: false,
|
|
217
|
-
message: isDev
|
|
218
|
-
? 'Invalid webhook payload: missing subscription data'
|
|
219
|
-
: 'Invalid webhook payload',
|
|
204
|
+
message: isDev ? 'Invalid webhook payload: missing subscription data' : 'Invalid webhook payload',
|
|
220
205
|
}
|
|
221
206
|
}
|
|
222
207
|
|
|
223
|
-
const
|
|
224
|
-
model: '
|
|
225
|
-
where: [{ field: '
|
|
226
|
-
})) as
|
|
208
|
+
const record = (await adapter.findOne({
|
|
209
|
+
model: 'subscription',
|
|
210
|
+
where: [{ field: 'razorpaySubscriptionId', value: subscriptionEntity.id }],
|
|
211
|
+
})) as SubscriptionRecord | null
|
|
227
212
|
|
|
228
|
-
if (!
|
|
213
|
+
if (!record) {
|
|
229
214
|
return {
|
|
230
215
|
success: false,
|
|
231
216
|
message: isDev
|
|
232
|
-
? `Subscription record not found for
|
|
217
|
+
? `Subscription record not found for ${subscriptionEntity.id}`
|
|
233
218
|
: 'Subscription record not found',
|
|
234
219
|
}
|
|
235
220
|
}
|
|
236
221
|
|
|
237
|
-
const userId =
|
|
222
|
+
const userId = record.referenceId
|
|
238
223
|
if (!userId) {
|
|
239
224
|
return {
|
|
240
225
|
success: false,
|
|
241
|
-
message: isDev
|
|
242
|
-
? `User ID not found in subscription record for subscription ${subscription.id}`
|
|
243
|
-
: 'User ID not found in subscription record',
|
|
226
|
+
message: isDev ? 'referenceId not found on subscription record' : 'Invalid subscription record',
|
|
244
227
|
}
|
|
245
228
|
}
|
|
246
229
|
|
|
@@ -252,14 +235,63 @@ const processWebhookEvent = async (
|
|
|
252
235
|
}
|
|
253
236
|
}
|
|
254
237
|
|
|
255
|
-
await handler(adapter,
|
|
238
|
+
await handler(adapter, subscriptionEntity.id, record, subscriptionEntity)
|
|
239
|
+
|
|
240
|
+
if (pluginOptions?.onEvent) {
|
|
241
|
+
try {
|
|
242
|
+
await pluginOptions.onEvent({ event, ...payload })
|
|
243
|
+
} catch {
|
|
244
|
+
// ignore
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (pluginOptions?.subscription) {
|
|
249
|
+
const sub = pluginOptions.subscription
|
|
250
|
+
const rpSub = {
|
|
251
|
+
id: subscriptionEntity.id,
|
|
252
|
+
plan_id: subscriptionEntity.plan_id,
|
|
253
|
+
status: subscriptionEntity.status,
|
|
254
|
+
current_start: subscriptionEntity.current_start,
|
|
255
|
+
current_end: subscriptionEntity.current_end,
|
|
256
|
+
} as RazorpaySubscription
|
|
257
|
+
const updatedRecord = { ...record, status: toLocalStatus(subscriptionEntity.status) }
|
|
258
|
+
try {
|
|
259
|
+
if (event === 'subscription.activated' && sub.onSubscriptionActivated) {
|
|
260
|
+
await sub.onSubscriptionActivated({
|
|
261
|
+
event,
|
|
262
|
+
razorpaySubscription: rpSub,
|
|
263
|
+
subscription: updatedRecord as SubscriptionRecord,
|
|
264
|
+
plan: { name: record.plan, monthlyPlanId: subscriptionEntity.plan_id },
|
|
265
|
+
})
|
|
266
|
+
} else if (
|
|
267
|
+
['subscription.cancelled', 'subscription.expired'].includes(event) &&
|
|
268
|
+
sub.onSubscriptionCancel
|
|
269
|
+
) {
|
|
270
|
+
await sub.onSubscriptionCancel({
|
|
271
|
+
event,
|
|
272
|
+
razorpaySubscription: rpSub,
|
|
273
|
+
subscription: updatedRecord as SubscriptionRecord,
|
|
274
|
+
})
|
|
275
|
+
} else if (sub.onSubscriptionUpdate) {
|
|
276
|
+
await sub.onSubscriptionUpdate({ event, subscription: updatedRecord as SubscriptionRecord })
|
|
277
|
+
}
|
|
278
|
+
} catch {
|
|
279
|
+
// ignore callback errors
|
|
280
|
+
}
|
|
281
|
+
}
|
|
256
282
|
|
|
257
283
|
if (onWebhookEvent) {
|
|
258
284
|
try {
|
|
259
|
-
await invokeCallback(
|
|
285
|
+
await invokeCallback(
|
|
286
|
+
onWebhookEvent,
|
|
287
|
+
adapter,
|
|
288
|
+
event,
|
|
289
|
+
subscriptionEntity,
|
|
290
|
+
payload,
|
|
291
|
+
userId
|
|
292
|
+
)
|
|
260
293
|
} catch {
|
|
261
|
-
//
|
|
262
|
-
// The callback is for custom logic and failures there shouldn't affect core functionality
|
|
294
|
+
// ignore
|
|
263
295
|
}
|
|
264
296
|
}
|
|
265
297
|
|
package/client/hooks.ts
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React hooks for Razorpay subscription features using TanStack Query.
|
|
3
|
+
* Use with your Better Auth client: usePlans(authClient), useSubscriptions(authClient), etc.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
useQuery,
|
|
8
|
+
useMutation,
|
|
9
|
+
useQueryClient,
|
|
10
|
+
type UseQueryOptions,
|
|
11
|
+
type UseMutationOptions,
|
|
12
|
+
} from '@tanstack/react-query'
|
|
13
|
+
import type {
|
|
14
|
+
RazorpayAuthClient,
|
|
15
|
+
PlanSummary,
|
|
16
|
+
CreateOrUpdateSubscriptionInput,
|
|
17
|
+
CancelSubscriptionInput,
|
|
18
|
+
RestoreSubscriptionInput,
|
|
19
|
+
ListSubscriptionsInput,
|
|
20
|
+
GetPlansResponse,
|
|
21
|
+
ListSubscriptionsResponse,
|
|
22
|
+
CreateOrUpdateSubscriptionResponse,
|
|
23
|
+
CancelSubscriptionResponse,
|
|
24
|
+
RestoreSubscriptionResponse,
|
|
25
|
+
RazorpayApiError,
|
|
26
|
+
} from './types'
|
|
27
|
+
|
|
28
|
+
const BASE = '/razorpay'
|
|
29
|
+
|
|
30
|
+
/** Query keys for cache invalidation. */
|
|
31
|
+
export const razorpayQueryKeys = {
|
|
32
|
+
all: ['razorpay'] as const,
|
|
33
|
+
plans: () => [...razorpayQueryKeys.all, 'plans'] as const,
|
|
34
|
+
subscriptions: (referenceId?: string) =>
|
|
35
|
+
[...razorpayQueryKeys.all, 'subscriptions', referenceId ?? 'me'] as const,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function assertSuccess<T>(res: unknown): asserts res is { success: true; data: T } {
|
|
39
|
+
if (res && typeof res === 'object' && 'success' in res) {
|
|
40
|
+
if ((res as { success: boolean }).success) return
|
|
41
|
+
const err = res as RazorpayApiError
|
|
42
|
+
throw new Error(err.error?.description ?? err.error?.code ?? 'Request failed')
|
|
43
|
+
}
|
|
44
|
+
throw new Error('Invalid response')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Fetch plans (GET /razorpay/get-plans). */
|
|
48
|
+
async function fetchPlans(client: RazorpayAuthClient): Promise<PlanSummary[]> {
|
|
49
|
+
const res = await client.api.get(`${BASE}/get-plans`)
|
|
50
|
+
assertSuccess<PlanSummary[]>(res)
|
|
51
|
+
return res.data
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Fetch subscriptions list (GET /razorpay/subscription/list). */
|
|
55
|
+
async function fetchSubscriptions(
|
|
56
|
+
client: RazorpayAuthClient,
|
|
57
|
+
input?: ListSubscriptionsInput
|
|
58
|
+
): Promise<ListSubscriptionsResponse['data']> {
|
|
59
|
+
const query: Record<string, string> = {}
|
|
60
|
+
if (input?.referenceId) query.referenceId = input.referenceId
|
|
61
|
+
const path = `${BASE}/subscription/list`
|
|
62
|
+
const res =
|
|
63
|
+
Object.keys(query).length > 0
|
|
64
|
+
? await client.api.get(path, { query })
|
|
65
|
+
: await client.api.get(path)
|
|
66
|
+
assertSuccess<ListSubscriptionsResponse['data']>(res)
|
|
67
|
+
return res.data
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Create or update subscription (POST /razorpay/subscription/create-or-update). */
|
|
71
|
+
async function createOrUpdateSubscription(
|
|
72
|
+
client: RazorpayAuthClient,
|
|
73
|
+
input: CreateOrUpdateSubscriptionInput
|
|
74
|
+
): Promise<CreateOrUpdateSubscriptionResponse['data']> {
|
|
75
|
+
const res = await client.api.post(`${BASE}/subscription/create-or-update`, {
|
|
76
|
+
body: input as unknown as Record<string, unknown>,
|
|
77
|
+
})
|
|
78
|
+
assertSuccess<CreateOrUpdateSubscriptionResponse['data']>(res)
|
|
79
|
+
return res.data
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Cancel subscription (POST /razorpay/subscription/cancel). */
|
|
83
|
+
async function cancelSubscription(
|
|
84
|
+
client: RazorpayAuthClient,
|
|
85
|
+
input: CancelSubscriptionInput
|
|
86
|
+
): Promise<CancelSubscriptionResponse['data']> {
|
|
87
|
+
const res = await client.api.post(`${BASE}/subscription/cancel`, {
|
|
88
|
+
body: input as unknown as Record<string, unknown>,
|
|
89
|
+
})
|
|
90
|
+
assertSuccess<CancelSubscriptionResponse['data']>(res)
|
|
91
|
+
return res.data
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Restore subscription (POST /razorpay/subscription/restore). */
|
|
95
|
+
async function restoreSubscription(
|
|
96
|
+
client: RazorpayAuthClient,
|
|
97
|
+
input: RestoreSubscriptionInput
|
|
98
|
+
): Promise<RestoreSubscriptionResponse['data']> {
|
|
99
|
+
const res = await client.api.post(`${BASE}/subscription/restore`, {
|
|
100
|
+
body: input as unknown as Record<string, unknown>,
|
|
101
|
+
})
|
|
102
|
+
assertSuccess<RestoreSubscriptionResponse['data']>(res)
|
|
103
|
+
return res.data
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export type UsePlansOptions = Omit<
|
|
107
|
+
UseQueryOptions<PlanSummary[], Error, PlanSummary[], readonly string[]>,
|
|
108
|
+
'queryKey' | 'queryFn'
|
|
109
|
+
>
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Fetch configured subscription plans (no auth required).
|
|
113
|
+
*/
|
|
114
|
+
export function usePlans(
|
|
115
|
+
client: RazorpayAuthClient | null | undefined,
|
|
116
|
+
options?: UsePlansOptions
|
|
117
|
+
) {
|
|
118
|
+
return useQuery({
|
|
119
|
+
queryKey: razorpayQueryKeys.plans(),
|
|
120
|
+
queryFn: () => fetchPlans(client!),
|
|
121
|
+
enabled: !!client,
|
|
122
|
+
...options,
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export type UseSubscriptionsOptions = Omit<
|
|
127
|
+
UseQueryOptions<
|
|
128
|
+
ListSubscriptionsResponse['data'],
|
|
129
|
+
Error,
|
|
130
|
+
ListSubscriptionsResponse['data'],
|
|
131
|
+
readonly (string | undefined)[]
|
|
132
|
+
>,
|
|
133
|
+
'queryKey' | 'queryFn'
|
|
134
|
+
> & { referenceId?: string }
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* List active/trialing subscriptions for the current user (or referenceId).
|
|
138
|
+
*/
|
|
139
|
+
export function useSubscriptions(
|
|
140
|
+
client: RazorpayAuthClient | null | undefined,
|
|
141
|
+
input?: ListSubscriptionsInput,
|
|
142
|
+
options?: UseSubscriptionsOptions
|
|
143
|
+
) {
|
|
144
|
+
const { referenceId, ...queryOptions } = options ?? {}
|
|
145
|
+
const refId = input?.referenceId ?? referenceId
|
|
146
|
+
return useQuery({
|
|
147
|
+
queryKey: razorpayQueryKeys.subscriptions(refId),
|
|
148
|
+
queryFn: () => fetchSubscriptions(client!, input),
|
|
149
|
+
enabled: !!client,
|
|
150
|
+
...queryOptions,
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export type UseCreateOrUpdateSubscriptionOptions = UseMutationOptions<
|
|
155
|
+
CreateOrUpdateSubscriptionResponse['data'],
|
|
156
|
+
Error,
|
|
157
|
+
CreateOrUpdateSubscriptionInput,
|
|
158
|
+
unknown
|
|
159
|
+
>
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Create or update a subscription. Returns checkoutUrl for Razorpay payment page.
|
|
163
|
+
* Invalidates subscriptions list on success.
|
|
164
|
+
*/
|
|
165
|
+
export function useCreateOrUpdateSubscription(
|
|
166
|
+
client: RazorpayAuthClient | null | undefined,
|
|
167
|
+
options?: UseCreateOrUpdateSubscriptionOptions
|
|
168
|
+
) {
|
|
169
|
+
const queryClient = useQueryClient()
|
|
170
|
+
return useMutation({
|
|
171
|
+
mutationFn: (input: CreateOrUpdateSubscriptionInput) =>
|
|
172
|
+
createOrUpdateSubscription(client!, input),
|
|
173
|
+
...options,
|
|
174
|
+
onSuccess: (data, variables, onMutateResult, context) => {
|
|
175
|
+
queryClient.invalidateQueries({ queryKey: razorpayQueryKeys.subscriptions() })
|
|
176
|
+
options?.onSuccess?.(data, variables, onMutateResult, context)
|
|
177
|
+
},
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export type UseCancelSubscriptionOptions = UseMutationOptions<
|
|
182
|
+
CancelSubscriptionResponse['data'],
|
|
183
|
+
Error,
|
|
184
|
+
CancelSubscriptionInput,
|
|
185
|
+
unknown
|
|
186
|
+
>
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Cancel a subscription by local subscription ID (at period end or immediately).
|
|
190
|
+
* Invalidates subscriptions list on success.
|
|
191
|
+
*/
|
|
192
|
+
export function useCancelSubscription(
|
|
193
|
+
client: RazorpayAuthClient | null | undefined,
|
|
194
|
+
options?: UseCancelSubscriptionOptions
|
|
195
|
+
) {
|
|
196
|
+
const queryClient = useQueryClient()
|
|
197
|
+
return useMutation({
|
|
198
|
+
mutationFn: (input: CancelSubscriptionInput) => cancelSubscription(client!, input),
|
|
199
|
+
...options,
|
|
200
|
+
onSuccess: (data, variables, onMutateResult, context) => {
|
|
201
|
+
queryClient.invalidateQueries({ queryKey: razorpayQueryKeys.subscriptions() })
|
|
202
|
+
options?.onSuccess?.(data, variables, onMutateResult, context)
|
|
203
|
+
},
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export type UseRestoreSubscriptionOptions = UseMutationOptions<
|
|
208
|
+
RestoreSubscriptionResponse['data'],
|
|
209
|
+
Error,
|
|
210
|
+
RestoreSubscriptionInput,
|
|
211
|
+
unknown
|
|
212
|
+
>
|
|
213
|
+
|
|
214
|
+
// Re-export client types for convenience when importing from this entry
|
|
215
|
+
export type {
|
|
216
|
+
RazorpayAuthClient,
|
|
217
|
+
PlanSummary,
|
|
218
|
+
CreateOrUpdateSubscriptionInput,
|
|
219
|
+
CancelSubscriptionInput,
|
|
220
|
+
RestoreSubscriptionInput,
|
|
221
|
+
ListSubscriptionsInput,
|
|
222
|
+
GetPlansResponse,
|
|
223
|
+
ListSubscriptionsResponse,
|
|
224
|
+
CreateOrUpdateSubscriptionResponse,
|
|
225
|
+
CancelSubscriptionResponse,
|
|
226
|
+
RestoreSubscriptionResponse,
|
|
227
|
+
RazorpayApiError,
|
|
228
|
+
RazorpayApiResult,
|
|
229
|
+
} from './types'
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Restore a subscription that was scheduled to cancel at period end.
|
|
233
|
+
* Invalidates subscriptions list on success.
|
|
234
|
+
*/
|
|
235
|
+
export function useRestoreSubscription(
|
|
236
|
+
client: RazorpayAuthClient | null | undefined,
|
|
237
|
+
options?: UseRestoreSubscriptionOptions
|
|
238
|
+
) {
|
|
239
|
+
const queryClient = useQueryClient()
|
|
240
|
+
return useMutation({
|
|
241
|
+
mutationFn: (input: RestoreSubscriptionInput) => restoreSubscription(client!, input),
|
|
242
|
+
...options,
|
|
243
|
+
onSuccess: (data, variables, onMutateResult, context) => {
|
|
244
|
+
queryClient.invalidateQueries({ queryKey: razorpayQueryKeys.subscriptions() })
|
|
245
|
+
options?.onSuccess?.(data, variables, onMutateResult, context)
|
|
246
|
+
},
|
|
247
|
+
})
|
|
248
|
+
}
|