@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/lib/types.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  /**
2
- * Razorpay subscription response interface from the Razorpay API.
3
- * This represents the full subscription object returned by Razorpay API calls.
2
+ * Razorpay subscription response from the Razorpay API.
4
3
  */
5
4
  export interface RazorpaySubscription {
6
5
  id: string
@@ -29,33 +28,103 @@ export interface RazorpaySubscription {
29
28
  remaining_count: string
30
29
  }
31
30
 
32
- /**
33
- * Subscription record stored in the auth adapter.
34
- */
35
- export interface RazorpaySubscriptionRecord {
36
- userId: string
37
- subscriptionId: string
38
- planId: string
39
- status: string
31
+ /** Local subscription status aligned with Razorpay and plugin lifecycle. */
32
+ export type SubscriptionStatus =
33
+ | 'created'
34
+ | 'active'
35
+ | 'pending'
36
+ | 'halted'
37
+ | 'cancelled'
38
+ | 'completed'
39
+ | 'expired'
40
+ | 'trialing'
41
+
42
+ /** Local subscription record stored in the auth adapter. */
43
+ export interface SubscriptionRecord {
44
+ id: string
45
+ plan: string
46
+ referenceId: string
47
+ razorpayCustomerId?: string | null
48
+ razorpaySubscriptionId?: string | null
49
+ status: SubscriptionStatus
50
+ trialStart?: Date | null
51
+ trialEnd?: Date | null
52
+ periodStart?: Date | null
53
+ periodEnd?: Date | null
54
+ cancelAtPeriodEnd: boolean
55
+ seats: number
56
+ groupId?: string | null
57
+ createdAt: Date
58
+ updatedAt: Date
40
59
  }
41
60
 
42
- /**
43
- * User record shape used by the Razorpay plugin.
44
- */
61
+ /** Plan limits (customizable per plan). */
62
+ export interface PlanLimits {
63
+ [key: string]: number
64
+ }
65
+
66
+ /** Free trial configuration for a plan. */
67
+ export interface PlanFreeTrial {
68
+ days: number
69
+ onTrialStart?: (subscription: SubscriptionRecord) => Promise<void>
70
+ onTrialEnd?: (args: { subscription: SubscriptionRecord }) => Promise<void>
71
+ }
72
+
73
+ /** Named plan with monthly/annual Razorpay plan IDs and optional trial. */
74
+ export interface RazorpayPlan {
75
+ name: string
76
+ monthlyPlanId: string
77
+ annualPlanId?: string
78
+ limits?: PlanLimits
79
+ freeTrial?: PlanFreeTrial
80
+ }
81
+
82
+ /** Subscription plugin options (plans, callbacks, authorization). */
83
+ export interface SubscriptionOptions {
84
+ enabled: boolean
85
+ plans: RazorpayPlan[] | (() => Promise<RazorpayPlan[]>)
86
+ requireEmailVerification?: boolean
87
+ authorizeReference?: (args: {
88
+ user: { id: string; email?: string; name?: string; [key: string]: unknown }
89
+ referenceId: string
90
+ action: string
91
+ }) => Promise<boolean>
92
+ getSubscriptionCreateParams?: (args: {
93
+ user: { id: string; email?: string; name?: string; [key: string]: unknown }
94
+ session: unknown
95
+ plan: RazorpayPlan
96
+ subscription: SubscriptionRecord
97
+ }) => Promise<{ params?: Record<string, unknown> }>
98
+ onSubscriptionCreated?: (args: {
99
+ razorpaySubscription: RazorpaySubscription
100
+ subscription: SubscriptionRecord
101
+ plan: RazorpayPlan
102
+ }) => Promise<void>
103
+ onSubscriptionActivated?: (args: {
104
+ event: string
105
+ razorpaySubscription: RazorpaySubscription
106
+ subscription: SubscriptionRecord
107
+ plan: RazorpayPlan
108
+ }) => Promise<void>
109
+ onSubscriptionUpdate?: (args: { event: string; subscription: SubscriptionRecord }) => Promise<void>
110
+ onSubscriptionCancel?: (args: {
111
+ event: string
112
+ razorpaySubscription: RazorpaySubscription
113
+ subscription: SubscriptionRecord
114
+ }) => Promise<void>
115
+ }
116
+
117
+ /** User record shape used by the Razorpay plugin (customer ID on user). */
45
118
  export interface RazorpayUserRecord {
46
119
  id: string
47
120
  email?: string
48
121
  name?: string
49
- subscriptionId?: string
50
- subscriptionPlanId?: string
51
- subscriptionStatus?: string
52
- subscriptionCurrentPeriodEnd?: Date | null
53
- cancelAtPeriodEnd?: boolean
54
- lastPaymentDate?: Date | null
55
- nextBillingDate?: Date | null
122
+ razorpayCustomerId?: string | null
123
+ [key: string]: unknown
56
124
  }
57
125
 
58
- type RazorpayWebhookEvent =
126
+ /** Razorpay webhook event types. */
127
+ export type RazorpayWebhookEvent =
59
128
  | 'subscription.authenticated'
60
129
  | 'subscription.activated'
61
130
  | 'subscription.charged'
@@ -64,8 +133,9 @@ type RazorpayWebhookEvent =
64
133
  | 'subscription.resumed'
65
134
  | 'subscription.pending'
66
135
  | 'subscription.halted'
136
+ | 'subscription.expired'
67
137
 
68
- interface RazorpayWebhookPayload {
138
+ export interface RazorpayWebhookPayload {
69
139
  event: RazorpayWebhookEvent
70
140
  subscription: {
71
141
  id: string
@@ -73,79 +143,62 @@ interface RazorpayWebhookPayload {
73
143
  status: string
74
144
  current_start?: number
75
145
  current_end?: number
76
- [key: string]: unknown // Allow other Razorpay subscription fields
146
+ [key: string]: unknown
77
147
  }
78
148
  payment?: {
79
149
  id: string
80
150
  amount: number
81
151
  currency: string
82
- [key: string]: unknown // Allow other Razorpay payment fields
152
+ [key: string]: unknown
83
153
  }
84
154
  }
85
155
 
86
- interface RazorpayWebhookContext {
156
+ export interface RazorpayWebhookContext {
87
157
  userId: string
88
- user: {
89
- id: string
90
- email: string
91
- name: string
92
- [key: string]: unknown // Allow other user fields
93
- }
158
+ user: { id: string; email?: string; name?: string; [key: string]: unknown }
94
159
  }
95
160
 
96
- /**
97
- * Callback function invoked after webhook events are processed.
98
- * Can be used for any custom logic: emails, notifications, analytics, integrations, etc.
99
- */
100
- type OnWebhookEventCallback = (
161
+ export type OnWebhookEventCallback = (
101
162
  payload: RazorpayWebhookPayload,
102
163
  context: RazorpayWebhookContext
103
164
  ) => Promise<void>
104
165
 
105
- interface RazorpayPluginOptions {
106
- keyId: string
107
- keySecret: string
108
- webhookSecret?: string
109
- plans: string[] // Array of plan IDs from Razorpay dashboard
110
- /**
111
- * Optional callback function invoked after webhook events are processed.
112
- * Use this for any custom logic: sending emails, updating external systems,
113
- * analytics tracking, integrations, or any other business logic.
114
- */
166
+ /** Main plugin options: client, webhook secret, customer creation, subscription config, callbacks. */
167
+ export interface RazorpayPluginOptions {
168
+ /** Initialized Razorpay client instance. */
169
+ razorpayClient: import('razorpay')
170
+ /** Webhook secret for signature verification. */
171
+ razorpayWebhookSecret?: string
172
+ /** Create Razorpay customer when user signs up. Default: false. */
173
+ createCustomerOnSignUp?: boolean
174
+ /** Called after a Razorpay customer is created. */
175
+ onCustomerCreate?: (args: {
176
+ user: RazorpayUserRecord
177
+ razorpayCustomer: { id: string; [key: string]: unknown }
178
+ }) => Promise<void>
179
+ /** Custom params (e.g. notes) when creating Razorpay customer. */
180
+ getCustomerCreateParams?: (args: {
181
+ user: RazorpayUserRecord
182
+ session: unknown
183
+ }) => Promise<{ params?: Record<string, unknown> }>
184
+ /** Subscription feature config (plans, callbacks). */
185
+ subscription?: SubscriptionOptions
186
+ /** Global callback for all processed webhook events. */
187
+ onEvent?: (event: { event: string; [key: string]: unknown }) => Promise<void>
188
+ /** Legacy: callback after webhook events are processed (payload + context). */
115
189
  onWebhookEvent?: OnWebhookEventCallback
116
190
  }
117
191
 
118
- /**
119
- * Standard success response structure for Razorpay API endpoints.
120
- */
121
192
  export interface RazorpaySuccessResponse<T = unknown> {
122
193
  success: true
123
194
  data: T
124
195
  }
125
196
 
126
- /**
127
- * Standard error response structure for Razorpay API endpoints.
128
- */
129
197
  export interface RazorpayErrorResponse {
130
198
  success: false
131
- error: {
132
- code: string
133
- description: string
134
- [key: string]: unknown // Allow additional error metadata
135
- }
199
+ error: { code: string; description: string; [key: string]: unknown }
136
200
  }
137
201
 
138
- /**
139
- * Union type for all Razorpay API responses.
140
- */
141
202
  export type RazorpayApiResponse<T = unknown> =
142
203
  | RazorpaySuccessResponse<T>
143
204
  | RazorpayErrorResponse
144
-
145
- export type {
146
- RazorpayPluginOptions,
147
- RazorpayWebhookEvent,
148
- RazorpayWebhookPayload,
149
- RazorpayWebhookContext,
150
- OnWebhookEventCallback,
151
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deiondz/better-auth-razorpay",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "Better Auth plugin for Razorpay subscriptions and payments",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -15,11 +15,17 @@
15
15
  "types": "./client.ts",
16
16
  "import": "./client.ts",
17
17
  "default": "./client.ts"
18
+ },
19
+ "./hooks": {
20
+ "types": "./client/hooks.ts",
21
+ "import": "./client/hooks.ts",
22
+ "default": "./client/hooks.ts"
18
23
  }
19
24
  },
20
25
  "files": [
21
26
  "index.ts",
22
27
  "client.ts",
28
+ "client",
23
29
  "api",
24
30
  "lib",
25
31
  "README.md",
@@ -43,13 +49,17 @@
43
49
  "url": "git+https://github.com/deiondz/better-auth-razorpay.git"
44
50
  },
45
51
  "peerDependencies": {
46
- "better-auth": "^1.0.0"
52
+ "@tanstack/react-query": "^5.0.0",
53
+ "better-auth": "^1.0.0",
54
+ "react": "^18.0.0 || ^19.0.0"
47
55
  },
48
56
  "dependencies": {
49
57
  "razorpay": "^2.9.2",
50
58
  "zod": "^3.23.0"
51
59
  },
52
60
  "devDependencies": {
61
+ "@tanstack/react-query": "^5.0.0",
62
+ "react": "^18.0.0",
53
63
  "typescript": "^5.0.0"
54
64
  },
55
65
  "engines": {
@@ -1,174 +0,0 @@
1
- import { createAuthEndpoint, sessionMiddleware } from 'better-auth/api'
2
- import type Razorpay from 'razorpay'
3
- import {
4
- handleRazorpayError,
5
- type RazorpaySubscription,
6
- type RazorpayUserRecord,
7
- } from '../lib'
8
-
9
- /**
10
- * Retrieves the current subscription details for the authenticated user.
11
- *
12
- * @param razorpay - The Razorpay instance initialized with API credentials
13
- * @returns A Better Auth endpoint handler
14
- *
15
- * @remarks
16
- * This endpoint:
17
- * - Requires user authentication via session
18
- * - Fetches subscription details from Razorpay API
19
- * - Includes cancellation status and period end information
20
- * - Returns null if user has no active subscription
21
- * - Provides detailed error messages in development mode
22
- *
23
- * @example
24
- * Response (success):
25
- * ```json
26
- * {
27
- * "success": true,
28
- * "data": {
29
- * "id": "sub_1234567890",
30
- * "status": "active",
31
- * "plan_id": "plan_1234567890",
32
- * "cancel_at_period_end": false,
33
- * ...
34
- * }
35
- * }
36
- * ```
37
- */
38
- export const getSubscription = (razorpay: Razorpay) =>
39
- createAuthEndpoint(
40
- '/razorpay/get-subscription',
41
- { method: 'GET', use: [sessionMiddleware] },
42
- async (_ctx) => {
43
- try {
44
- // Get user ID from session
45
- const userId = _ctx.context.session?.user?.id
46
-
47
- if (!userId) {
48
- return {
49
- success: false,
50
- error: {
51
- code: 'UNAUTHORIZED',
52
- description: 'User not authenticated',
53
- },
54
- }
55
- }
56
-
57
- // Get user record to check subscription status
58
- const user = (await _ctx.context.adapter.findOne({
59
- model: 'user',
60
- where: [{ field: 'id', value: userId }],
61
- })) as RazorpayUserRecord | null
62
-
63
- if (!user) {
64
- return {
65
- success: false,
66
- error: {
67
- code: 'USER_NOT_FOUND',
68
- description: 'User not found',
69
- },
70
- }
71
- }
72
-
73
- // Check subscription status from user table
74
- const subscriptionId = user.subscriptionId
75
-
76
- if (!subscriptionId) {
77
- return {
78
- success: true,
79
- data: null,
80
- }
81
- }
82
-
83
- // Fetch full subscription details from Razorpay API
84
- try {
85
- const subscription = (await razorpay.subscriptions.fetch(
86
- subscriptionId
87
- )) as RazorpaySubscription
88
-
89
- // Read cancellation status from user table
90
- const cancelAtPeriodEnd = user.cancelAtPeriodEnd ?? false
91
- const subscriptionCurrentPeriodEnd = user.subscriptionCurrentPeriodEnd
92
-
93
- return {
94
- success: true,
95
- data: {
96
- id: subscription.id,
97
- entity: subscription.entity,
98
- plan_id: subscription.plan_id,
99
- status: subscription.status,
100
- current_start: subscription.current_start,
101
- current_end: subscription.current_end,
102
- ended_at: subscription.ended_at,
103
- quantity: subscription.quantity,
104
- notes: subscription.notes,
105
- charge_at: subscription.charge_at,
106
- start_at: subscription.start_at,
107
- end_at: subscription.end_at,
108
- auth_attempts: subscription.auth_attempts,
109
- total_count: subscription.total_count,
110
- paid_count: subscription.paid_count,
111
- customer_notify: subscription.customer_notify,
112
- created_at: subscription.created_at,
113
- expire_by: subscription.expire_by,
114
- short_url: subscription.short_url,
115
- has_scheduled_changes: subscription.has_scheduled_changes,
116
- change_scheduled_at: subscription.change_scheduled_at,
117
- source: subscription.source,
118
- offer_id: subscription.offer_id,
119
- remaining_count: subscription.remaining_count,
120
- cancel_at_period_end: cancelAtPeriodEnd,
121
- subscription_current_period_end: subscriptionCurrentPeriodEnd
122
- ? Math.floor(new Date(subscriptionCurrentPeriodEnd).getTime() / 1000)
123
- : null,
124
- },
125
- }
126
- } catch (razorpayError) {
127
- // Handle Razorpay-specific errors with subscription ID context
128
- const isDev = process.env.NODE_ENV === 'development'
129
-
130
- // Extract error message from Razorpay error
131
- let errorMessage = 'Failed to fetch subscription'
132
- let errorCode = 'SUBSCRIPTION_FETCH_FAILED'
133
-
134
- if (razorpayError && typeof razorpayError === 'object') {
135
- // Razorpay error format: { error: { code: string, description: string } }
136
- if ('error' in razorpayError) {
137
- const razorpayErr = razorpayError as {
138
- error?: { code?: string; description?: string }
139
- }
140
- errorCode = razorpayErr.error?.code || errorCode
141
- errorMessage = razorpayErr.error?.description || errorMessage
142
- } else if ('message' in razorpayError) {
143
- errorMessage = (razorpayError as { message: string }).message
144
- }
145
- }
146
-
147
- // Check if error is specifically about subscription not existing
148
- const isNotFoundError =
149
- errorMessage.toLowerCase().includes('does not exist') ||
150
- errorMessage.toLowerCase().includes('not found') ||
151
- errorCode === 'BAD_REQUEST_ERROR'
152
-
153
- // Include subscription ID in error for debugging
154
- const description = isDev
155
- ? `${errorMessage} (Subscription ID: ${subscriptionId})`
156
- : isNotFoundError
157
- ? 'The subscription could not be found. This may indicate the subscription was deleted or the ID is invalid. Please contact support if this issue persists.'
158
- : 'Unable to retrieve subscription information. Please try again or contact support if this issue persists.'
159
-
160
- return {
161
- success: false,
162
- error: {
163
- code: errorCode,
164
- description,
165
- // Include subscription ID in metadata for development
166
- ...(isDev && { subscriptionId }),
167
- },
168
- }
169
- }
170
- } catch (error) {
171
- return handleRazorpayError(error)
172
- }
173
- }
174
- )
@@ -1,138 +0,0 @@
1
- import { createAuthEndpoint, sessionMiddleware } from 'better-auth/api'
2
- import type Razorpay from 'razorpay'
3
- import {
4
- handleRazorpayError,
5
- pauseSubscriptionSchema,
6
- type RazorpaySubscription,
7
- type RazorpaySubscriptionRecord,
8
- } from '../lib'
9
-
10
- /**
11
- * Pauses an active subscription.
12
- *
13
- * @param razorpay - The Razorpay instance initialized with API credentials
14
- * @returns A Better Auth endpoint handler
15
- *
16
- * @remarks
17
- * This endpoint:
18
- * - Requires user authentication via session
19
- * - Verifies subscription ownership before pausing
20
- * - Pauses the subscription via Razorpay API
21
- * - Updates subscription and user records with paused status
22
- * - Paused subscriptions can be resumed later
23
- *
24
- * @example
25
- * Request body:
26
- * ```json
27
- * {
28
- * "subscription_id": "sub_1234567890"
29
- * }
30
- * ```
31
- */
32
- export const pauseSubscription = (razorpay: Razorpay) =>
33
- createAuthEndpoint(
34
- '/razorpay/pause-subscription',
35
- { method: 'POST', use: [sessionMiddleware] },
36
- async (_ctx) => {
37
- try {
38
- // Validate input using Zod schema
39
- const validatedInput = pauseSubscriptionSchema.parse(_ctx.body)
40
-
41
- // Get user ID from session
42
- const userId = _ctx.context.session?.user?.id
43
-
44
- if (!userId) {
45
- return {
46
- success: false,
47
- error: {
48
- code: 'UNAUTHORIZED',
49
- description: 'User not authenticated',
50
- },
51
- }
52
- }
53
-
54
- // Get subscription record to verify it belongs to the user
55
- const subscriptionRecord = (await _ctx.context.adapter.findOne({
56
- model: 'razorpaySubscription',
57
- where: [{ field: 'subscriptionId', value: validatedInput.subscription_id }],
58
- })) as RazorpaySubscriptionRecord | null
59
-
60
- if (!subscriptionRecord) {
61
- return {
62
- success: false,
63
- error: {
64
- code: 'SUBSCRIPTION_NOT_FOUND',
65
- description: 'Subscription not found',
66
- },
67
- }
68
- }
69
-
70
- // Verify that the subscription belongs to the authenticated user
71
- const subscriptionUserId = subscriptionRecord.userId
72
- if (subscriptionUserId !== userId) {
73
- return {
74
- success: false,
75
- error: {
76
- code: 'UNAUTHORIZED',
77
- description: 'Subscription does not belong to authenticated user',
78
- },
79
- }
80
- }
81
-
82
- // Pause subscription via Razorpay API
83
- const subscription = (await razorpay.subscriptions.pause(
84
- validatedInput.subscription_id
85
- )) as RazorpaySubscription
86
-
87
- // Update subscription status in database
88
- await _ctx.context.adapter.update({
89
- model: 'razorpaySubscription',
90
- where: [{ field: 'subscriptionId', value: validatedInput.subscription_id }],
91
- update: { status: subscription.status },
92
- })
93
-
94
- // Update user table with subscription status
95
- await _ctx.context.adapter.update({
96
- model: 'user',
97
- where: [{ field: 'id', value: userId }],
98
- update: {
99
- data: {
100
- subscriptionStatus: subscription.status,
101
- },
102
- },
103
- })
104
-
105
- return {
106
- success: true,
107
- data: {
108
- id: subscription.id,
109
- entity: subscription.entity,
110
- plan_id: subscription.plan_id,
111
- status: subscription.status,
112
- current_start: subscription.current_start,
113
- current_end: subscription.current_end,
114
- ended_at: subscription.ended_at,
115
- quantity: subscription.quantity,
116
- notes: subscription.notes,
117
- charge_at: subscription.charge_at,
118
- start_at: subscription.start_at,
119
- end_at: subscription.end_at,
120
- auth_attempts: subscription.auth_attempts,
121
- total_count: subscription.total_count,
122
- paid_count: subscription.paid_count,
123
- customer_notify: subscription.customer_notify,
124
- created_at: subscription.created_at,
125
- expire_by: subscription.expire_by,
126
- short_url: subscription.short_url,
127
- has_scheduled_changes: subscription.has_scheduled_changes,
128
- change_scheduled_at: subscription.change_scheduled_at,
129
- source: subscription.source,
130
- offer_id: subscription.offer_id,
131
- remaining_count: subscription.remaining_count,
132
- },
133
- }
134
- } catch (error) {
135
- return handleRazorpayError(error)
136
- }
137
- }
138
- )