@deiondz/better-auth-razorpay 2.0.4 → 2.0.7

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