@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/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 { OnWebhookEventCallback } from '../lib/types'
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 WebhookContext {
19
- adapter: {
20
- findOne: (params: {
21
- model: string
22
- where: { field: string; value: string }[]
23
- }) => Promise<unknown>
24
- update: (params: {
25
- model: string
26
- where: { field: string; value: string }[]
27
- update: Record<string, unknown>
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: WebhookContext['adapter'],
34
- subscriptionId: string,
35
- userId: string,
36
+ adapter: WebhookAdapter,
37
+ razorpaySubscriptionId: string,
38
+ record: SubscriptionRecord,
36
39
  subscription: SubscriptionEntity
37
40
  ) => Promise<void>
38
41
 
39
- const updateSubscriptionAndUser = async (
40
- adapter: WebhookContext['adapter'],
41
- subscriptionId: string,
42
- userId: string,
43
- status: string,
44
- extraUserFields?: Record<string, unknown>
45
- ): Promise<void> => {
46
- await adapter.update({
47
- model: 'razorpaySubscription',
48
- where: [{ field: 'subscriptionId', value: subscriptionId }],
49
- update: { status },
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: 'user',
54
- where: [{ field: 'id', value: userId }],
55
- update: { subscriptionStatus: status, ...extraUserFields },
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
- status: string,
62
- extraUserFields?: (sub: SubscriptionEntity) => Record<string, unknown>
63
- ): EventHandler =>
64
- async (adapter, subscriptionId, userId, subscription) => {
65
- const extra = extraUserFields ? extraUserFields(subscription) : {}
66
- await updateSubscriptionAndUser(adapter, subscriptionId, userId, status, extra)
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('authenticated', (sub) => ({
71
- subscriptionId: sub.id,
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
- lastPaymentDate: new Date(),
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('paused'),
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: WebhookContext['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: string; name: string } | null
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 || 'INR',
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 management.
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 = (webhookSecret?: string, onWebhookEvent?: OnWebhookEventCallback) =>
173
- createAuthEndpoint('/razorpay/webhook', { method: 'POST' }, async (_ctx) => {
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
- return processWebhookEvent(_ctx.context.adapter, rawBody, _ctx.body, onWebhookEvent)
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
- const processWebhookEvent = async (
193
- adapter: WebhookContext['adapter'],
182
+ async function processWebhookEvent(
183
+ adapter: WebhookAdapter,
194
184
  rawBody: string,
195
185
  fallbackBody: unknown,
196
- onWebhookEvent?: OnWebhookEventCallback
197
- ): Promise<WebhookResult> => {
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 subscription = payload.subscription?.entity as SubscriptionEntity | undefined
214
- if (!subscription) {
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 subscriptionRecord = (await adapter.findOne({
224
- model: 'razorpaySubscription',
225
- where: [{ field: 'subscriptionId', value: subscription.id }],
226
- })) as { userId?: string } | null
208
+ const record = (await adapter.findOne({
209
+ model: 'subscription',
210
+ where: [{ field: 'razorpaySubscriptionId', value: subscriptionEntity.id }],
211
+ })) as SubscriptionRecord | null
227
212
 
228
- if (!subscriptionRecord) {
213
+ if (!record) {
229
214
  return {
230
215
  success: false,
231
216
  message: isDev
232
- ? `Subscription record not found for subscription ${subscription.id}`
217
+ ? `Subscription record not found for ${subscriptionEntity.id}`
233
218
  : 'Subscription record not found',
234
219
  }
235
220
  }
236
221
 
237
- const userId = subscriptionRecord.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, subscription.id, userId, subscription)
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(onWebhookEvent, adapter, event, subscription, payload, userId)
285
+ await invokeCallback(
286
+ onWebhookEvent,
287
+ adapter,
288
+ event,
289
+ subscriptionEntity,
290
+ payload,
291
+ userId
292
+ )
260
293
  } catch {
261
- // Silently handle callback errors - they shouldn't break webhook processing
262
- // The callback is for custom logic and failures there shouldn't affect core functionality
294
+ // ignore
263
295
  }
264
296
  }
265
297
 
@@ -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
+ }