@deiondz/better-auth-razorpay 2.0.3 → 2.0.5

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 CHANGED
@@ -725,7 +725,7 @@ type User = typeof authClient.$Infer.Session.user
725
725
 
726
726
  ## TanStack Query Hooks
727
727
 
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.
728
+ The plugin works with **TanStack Query**. We provide optional pre-built hooks that get the auth client from React context; wrap your app once with **`<RazorpayAuthProvider client={authClient}>`** and use **`usePlans()`**, **`useSubscriptions()`**, etc. with **no client argument**. 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.
729
729
 
730
730
  To use our pre-built hooks, install peer dependencies:
731
731
 
@@ -734,13 +734,14 @@ npm install @tanstack/react-query react
734
734
  # or yarn / pnpm / bun
735
735
  ```
736
736
 
737
- Import from `@deiondz/better-auth-razorpay/hooks` and pass your Better Auth client as the first argument:
737
+ Import from `@deiondz/better-auth-razorpay/hooks` and wrap your app with **`RazorpayAuthProvider`** so hooks can read the client from context:
738
738
 
739
739
  ```tsx
740
740
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
741
741
  import { createAuthClient } from 'better-auth/react'
742
742
  import { razorpayClientPlugin } from '@deiondz/better-auth-razorpay/client'
743
743
  import {
744
+ RazorpayAuthProvider,
744
745
  usePlans,
745
746
  useSubscriptions,
746
747
  useCreateOrUpdateSubscription,
@@ -759,21 +760,23 @@ const authClient = createAuthClient<typeof auth>({
759
760
  function App() {
760
761
  return (
761
762
  <QueryClientProvider client={queryClient}>
762
- <SubscriptionUI />
763
+ <RazorpayAuthProvider client={authClient}>
764
+ <SubscriptionUI />
765
+ </RazorpayAuthProvider>
763
766
  </QueryClientProvider>
764
767
  )
765
768
  }
766
769
 
767
770
  function SubscriptionUI() {
768
771
  // Plans (no auth required)
769
- const { data: plans, isLoading: plansLoading } = usePlans(authClient)
772
+ const { data: plans, isLoading: plansLoading } = usePlans()
770
773
 
771
774
  // Current user's subscriptions (requires session)
772
- const { data: subscriptions, isLoading: subsLoading } = useSubscriptions(authClient)
775
+ const { data: subscriptions, isLoading: subsLoading } = useSubscriptions()
773
776
 
774
- const createOrUpdate = useCreateOrUpdateSubscription(authClient)
775
- const cancel = useCancelSubscription(authClient)
776
- const restore = useRestoreSubscription(authClient)
777
+ const createOrUpdate = useCreateOrUpdateSubscription()
778
+ const cancel = useCancelSubscription()
779
+ const restore = useRestoreSubscription()
777
780
 
778
781
  const handleSubscribe = () => {
779
782
  createOrUpdate.mutate(
@@ -821,11 +824,11 @@ function SubscriptionUI() {
821
824
 
822
825
  | Hook | Type | Description |
823
826
  |------|------|-------------|
824
- | `usePlans(client, options?)` | `useQuery` | Fetches configured plans (GET `/razorpay/get-plans`). |
825
- | `useSubscriptions(client, input?, options?)` | `useQuery` | Lists active/trialing subscriptions (GET `/razorpay/subscription/list`). Optional `referenceId` in input or options. |
826
- | `useCreateOrUpdateSubscription(client, options?)` | `useMutation` | Creates or updates subscription; returns `checkoutUrl`. Invalidates subscriptions list on success. |
827
- | `useCancelSubscription(client, options?)` | `useMutation` | Cancels by local subscription ID; optional `immediately`. Invalidates subscriptions list on success. |
828
- | `useRestoreSubscription(client, options?)` | `useMutation` | Restores a subscription scheduled to cancel. Invalidates subscriptions list on success. |
827
+ | `usePlans(options?)` | `useQuery` | Fetches configured plans (GET `/razorpay/get-plans`). Requires `RazorpayAuthProvider` above in the tree. |
828
+ | `useSubscriptions(input?, options?)` | `useQuery` | Lists active/trialing subscriptions (GET `/razorpay/subscription/list`). Optional `referenceId` in input or options. Requires `RazorpayAuthProvider`. |
829
+ | `useCreateOrUpdateSubscription(options?)` | `useMutation` | Creates or updates subscription; returns `checkoutUrl`. Invalidates subscriptions list on success. Requires `RazorpayAuthProvider`. |
830
+ | `useCancelSubscription(options?)` | `useMutation` | Cancels by local subscription ID; optional `immediately`. Invalidates subscriptions list on success. Requires `RazorpayAuthProvider`. |
831
+ | `useRestoreSubscription(options?)` | `useMutation` | Restores a subscription scheduled to cancel. Invalidates subscriptions list on success. Requires `RazorpayAuthProvider`. |
829
832
 
830
833
  **Query keys** (for manual invalidation or prefetching):
831
834
 
@@ -1381,7 +1384,10 @@ async function initializeRazorpayCheckout(subscriptionId: string) {
1381
1384
  **1. "POST /api/auth/api/get 404" or Razorpay requests returning 404**
1382
1385
  - Add the **client plugin** to your auth client: `createAuthClient({ plugins: [razorpayClientPlugin(), ...] })`
1383
1386
  - 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
1387
+ - Wrap your app with **`<RazorpayAuthProvider client={authClient}>`** so hooks get the client from context; use **`usePlans()`**, **`useSubscriptions()`**, etc. with no client argument
1388
+ - The TanStack hooks use `authClient.razorpay` when present, so they work correctly once the client plugin is added
1389
+
1390
+ **For maintainers:** See [Razorpay plugin × Better Auth client (problem and solution)](docs/BETTER_AUTH_CLIENT_CONTEXT.md) for the 404/TypeScript root cause and plugin-side fixes.
1385
1391
 
1386
1392
  **2. "Plan not found in configured plans"**
1387
1393
  - Ensure the plan ID exists in Razorpay dashboard
package/client/hooks.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * React hooks for Razorpay subscription features using TanStack Query.
3
- * Use with your Better Auth client: usePlans(authClient), useSubscriptions(authClient), etc.
3
+ * Wrap your app with <RazorpayAuthProvider client={authClient}> and use usePlans(), useSubscriptions(), etc. with no client argument.
4
4
  */
5
5
 
6
+ import { createContext, useContext, createElement, type ReactNode } from 'react'
6
7
  import {
7
8
  useQuery,
8
9
  useMutation,
@@ -29,6 +30,37 @@ import type {
29
30
 
30
31
  const BASE = '/razorpay'
31
32
 
33
+ const RAZORPAY_NO_CLIENT_MESSAGE =
34
+ 'Razorpay hooks require wrapping your app with <RazorpayAuthProvider client={authClient}>.'
35
+
36
+ const RAZORPAY_NO_RAZORPAY_OR_API_MESSAGE =
37
+ 'Razorpay hooks require a client created with razorpayClientPlugin() in createAuthClient({ plugins: [...] }).'
38
+
39
+ /** Context holding the Razorpay-capable auth client. Default is null. */
40
+ export const RazorpayAuthContext = createContext<RazorpayAuthClient | null>(null)
41
+
42
+ /** Provider that supplies the auth client to Razorpay hooks. Wrap your app once with client={authClient}. */
43
+ export function RazorpayAuthProvider({
44
+ client,
45
+ children,
46
+ }: {
47
+ client: RazorpayAuthClient | null
48
+ children: ReactNode
49
+ }) {
50
+ return createElement(RazorpayAuthContext.Provider, { value: client }, children)
51
+ }
52
+
53
+ /** Returns the Razorpay-capable auth client from context, or null if not wrapped with RazorpayAuthProvider. */
54
+ export function useRazorpayAuthClient(): RazorpayAuthClient | null {
55
+ return useContext(RazorpayAuthContext)
56
+ }
57
+
58
+ function requireRazorpayOrApi(client: RazorpayAuthClient): void {
59
+ if (!client.razorpay && !client.api) {
60
+ throw new Error(RAZORPAY_NO_RAZORPAY_OR_API_MESSAGE)
61
+ }
62
+ }
63
+
32
64
  /** Query keys for cache invalidation. */
33
65
  export const razorpayQueryKeys = {
34
66
  all: ['razorpay'] as const,
@@ -48,9 +80,10 @@ function assertSuccess<T>(res: unknown): asserts res is { success: true; data: T
48
80
 
49
81
  /** Fetch plans (GET /razorpay/get-plans). Prefers client.razorpay when available to avoid 404s. */
50
82
  async function fetchPlans(client: RazorpayAuthClient): Promise<PlanSummary[]> {
83
+ requireRazorpayOrApi(client)
51
84
  const res = client.razorpay
52
85
  ? await client.razorpay.getPlans()
53
- : await client.api.get(`${BASE}/get-plans`)
86
+ : await client.api!.get(`${BASE}/get-plans`)
54
87
  assertSuccess<PlanSummary[]>(res)
55
88
  return res.data
56
89
  }
@@ -60,16 +93,17 @@ async function fetchSubscriptions(
60
93
  client: RazorpayAuthClient,
61
94
  input?: ListSubscriptionsInput
62
95
  ): Promise<ListSubscriptionsResponse['data']> {
96
+ requireRazorpayOrApi(client)
63
97
  const res = client.razorpay
64
98
  ? await client.razorpay.listSubscriptions(input)
65
99
  : (() => {
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
- })()
100
+ const query: Record<string, string> = {}
101
+ if (input?.referenceId) query.referenceId = input.referenceId
102
+ const path = `${BASE}/subscription/list`
103
+ return Object.keys(query).length > 0
104
+ ? client.api!.get(path, { query })
105
+ : client.api!.get(path)
106
+ })()
73
107
  assertSuccess<ListSubscriptionsResponse['data']>(res)
74
108
  return res.data
75
109
  }
@@ -79,11 +113,12 @@ async function createOrUpdateSubscription(
79
113
  client: RazorpayAuthClient,
80
114
  input: CreateOrUpdateSubscriptionInput
81
115
  ): Promise<CreateOrUpdateSubscriptionResponse['data']> {
116
+ requireRazorpayOrApi(client)
82
117
  const res = client.razorpay
83
118
  ? 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
- })
119
+ : await client.api!.post(`${BASE}/subscription/create-or-update`, {
120
+ body: input as unknown as Record<string, unknown>,
121
+ })
87
122
  assertSuccess<CreateOrUpdateSubscriptionResponse['data']>(res)
88
123
  return res.data
89
124
  }
@@ -93,11 +128,12 @@ async function cancelSubscription(
93
128
  client: RazorpayAuthClient,
94
129
  input: CancelSubscriptionInput
95
130
  ): Promise<CancelSubscriptionResponse['data']> {
131
+ requireRazorpayOrApi(client)
96
132
  const res = client.razorpay
97
133
  ? await client.razorpay.cancelSubscription(input)
98
- : await client.api.post(`${BASE}/subscription/cancel`, {
99
- body: input as unknown as Record<string, unknown>,
100
- })
134
+ : await client.api!.post(`${BASE}/subscription/cancel`, {
135
+ body: input as unknown as Record<string, unknown>,
136
+ })
101
137
  assertSuccess<CancelSubscriptionResponse['data']>(res)
102
138
  return res.data
103
139
  }
@@ -107,11 +143,12 @@ async function restoreSubscription(
107
143
  client: RazorpayAuthClient,
108
144
  input: RestoreSubscriptionInput
109
145
  ): Promise<RestoreSubscriptionResponse['data']> {
146
+ requireRazorpayOrApi(client)
110
147
  const res = client.razorpay
111
148
  ? await client.razorpay.restoreSubscription(input)
112
- : await client.api.post(`${BASE}/subscription/restore`, {
113
- body: input as unknown as Record<string, unknown>,
114
- })
149
+ : await client.api!.post(`${BASE}/subscription/restore`, {
150
+ body: input as unknown as Record<string, unknown>,
151
+ })
115
152
  assertSuccess<RestoreSubscriptionResponse['data']>(res)
116
153
  return res.data
117
154
  }
@@ -121,11 +158,12 @@ async function verifyPayment(
121
158
  client: RazorpayAuthClient,
122
159
  input: VerifyPaymentInput
123
160
  ): Promise<VerifyPaymentResponse['data']> {
161
+ requireRazorpayOrApi(client)
124
162
  const res = client.razorpay
125
163
  ? await client.razorpay.verifyPayment(input)
126
- : await client.api.post(`${BASE}/verify-payment`, {
127
- body: input as unknown as Record<string, unknown>,
128
- })
164
+ : await client.api!.post(`${BASE}/verify-payment`, {
165
+ body: input as unknown as Record<string, unknown>,
166
+ })
129
167
  assertSuccess<VerifyPaymentResponse['data']>(res)
130
168
  return res.data
131
169
  }
@@ -137,11 +175,10 @@ export type UsePlansOptions = Omit<
137
175
 
138
176
  /**
139
177
  * Fetch configured subscription plans (no auth required).
178
+ * Requires RazorpayAuthProvider above in the tree.
140
179
  */
141
- export function usePlans(
142
- client: RazorpayAuthClient | null | undefined,
143
- options?: UsePlansOptions
144
- ) {
180
+ export function usePlans(options?: UsePlansOptions) {
181
+ const client = useRazorpayAuthClient()
145
182
  return useQuery({
146
183
  queryKey: razorpayQueryKeys.plans(),
147
184
  queryFn: () => fetchPlans(client!),
@@ -162,12 +199,13 @@ export type UseSubscriptionsOptions = Omit<
162
199
 
163
200
  /**
164
201
  * List active/trialing subscriptions for the current user (or referenceId).
202
+ * Requires RazorpayAuthProvider above in the tree.
165
203
  */
166
204
  export function useSubscriptions(
167
- client: RazorpayAuthClient | null | undefined,
168
205
  input?: ListSubscriptionsInput,
169
206
  options?: UseSubscriptionsOptions
170
207
  ) {
208
+ const client = useRazorpayAuthClient()
171
209
  const { referenceId, ...queryOptions } = options ?? {}
172
210
  const refId = input?.referenceId ?? referenceId
173
211
  return useQuery({
@@ -188,15 +226,18 @@ export type UseCreateOrUpdateSubscriptionOptions = UseMutationOptions<
188
226
  /**
189
227
  * Create or update a subscription. Returns checkoutUrl for Razorpay payment page.
190
228
  * Invalidates subscriptions list on success.
229
+ * Requires RazorpayAuthProvider above in the tree.
191
230
  */
192
231
  export function useCreateOrUpdateSubscription(
193
- client: RazorpayAuthClient | null | undefined,
194
232
  options?: UseCreateOrUpdateSubscriptionOptions
195
233
  ) {
234
+ const client = useRazorpayAuthClient()
196
235
  const queryClient = useQueryClient()
197
236
  return useMutation({
198
- mutationFn: (input: CreateOrUpdateSubscriptionInput) =>
199
- createOrUpdateSubscription(client!, input),
237
+ mutationFn: (input: CreateOrUpdateSubscriptionInput) => {
238
+ if (!client) throw new Error(RAZORPAY_NO_CLIENT_MESSAGE)
239
+ return createOrUpdateSubscription(client, input)
240
+ },
200
241
  ...options,
201
242
  onSuccess: (data, variables, onMutateResult, context) => {
202
243
  queryClient.invalidateQueries({ queryKey: razorpayQueryKeys.subscriptions() })
@@ -215,14 +256,16 @@ export type UseCancelSubscriptionOptions = UseMutationOptions<
215
256
  /**
216
257
  * Cancel a subscription by local subscription ID (at period end or immediately).
217
258
  * Invalidates subscriptions list on success.
259
+ * Requires RazorpayAuthProvider above in the tree.
218
260
  */
219
- export function useCancelSubscription(
220
- client: RazorpayAuthClient | null | undefined,
221
- options?: UseCancelSubscriptionOptions
222
- ) {
261
+ export function useCancelSubscription(options?: UseCancelSubscriptionOptions) {
262
+ const client = useRazorpayAuthClient()
223
263
  const queryClient = useQueryClient()
224
264
  return useMutation({
225
- mutationFn: (input: CancelSubscriptionInput) => cancelSubscription(client!, input),
265
+ mutationFn: (input: CancelSubscriptionInput) => {
266
+ if (!client) throw new Error(RAZORPAY_NO_CLIENT_MESSAGE)
267
+ return cancelSubscription(client, input)
268
+ },
226
269
  ...options,
227
270
  onSuccess: (data, variables, onMutateResult, context) => {
228
271
  queryClient.invalidateQueries({ queryKey: razorpayQueryKeys.subscriptions() })
@@ -261,14 +304,16 @@ export type {
261
304
  /**
262
305
  * Restore a subscription that was scheduled to cancel at period end.
263
306
  * Invalidates subscriptions list on success.
307
+ * Requires RazorpayAuthProvider above in the tree.
264
308
  */
265
- export function useRestoreSubscription(
266
- client: RazorpayAuthClient | null | undefined,
267
- options?: UseRestoreSubscriptionOptions
268
- ) {
309
+ export function useRestoreSubscription(options?: UseRestoreSubscriptionOptions) {
310
+ const client = useRazorpayAuthClient()
269
311
  const queryClient = useQueryClient()
270
312
  return useMutation({
271
- mutationFn: (input: RestoreSubscriptionInput) => restoreSubscription(client!, input),
313
+ mutationFn: (input: RestoreSubscriptionInput) => {
314
+ if (!client) throw new Error(RAZORPAY_NO_CLIENT_MESSAGE)
315
+ return restoreSubscription(client, input)
316
+ },
272
317
  ...options,
273
318
  onSuccess: (data, variables, onMutateResult, context) => {
274
319
  queryClient.invalidateQueries({ queryKey: razorpayQueryKeys.subscriptions() })
@@ -288,14 +333,16 @@ export type UseVerifyPaymentOptions = UseMutationOptions<
288
333
  * Verify payment signature after Razorpay checkout success.
289
334
  * Call with the payload from the Razorpay success handler (razorpay_payment_id, razorpay_subscription_id, razorpay_signature).
290
335
  * Invalidates subscriptions list on success.
336
+ * Requires RazorpayAuthProvider above in the tree.
291
337
  */
292
- export function useVerifyPayment(
293
- client: RazorpayAuthClient | null | undefined,
294
- options?: UseVerifyPaymentOptions
295
- ) {
338
+ export function useVerifyPayment(options?: UseVerifyPaymentOptions) {
339
+ const client = useRazorpayAuthClient()
296
340
  const queryClient = useQueryClient()
297
341
  return useMutation({
298
- mutationFn: (input: VerifyPaymentInput) => verifyPayment(client!, input),
342
+ mutationFn: (input: VerifyPaymentInput) => {
343
+ if (!client) throw new Error(RAZORPAY_NO_CLIENT_MESSAGE)
344
+ return verifyPayment(client, input)
345
+ },
299
346
  ...options,
300
347
  onSuccess: (data, variables, onMutateResult, context) => {
301
348
  queryClient.invalidateQueries({ queryKey: razorpayQueryKeys.subscriptions() })
package/client/types.ts CHANGED
@@ -91,10 +91,14 @@ export interface RazorpayClientActions {
91
91
 
92
92
  /**
93
93
  * Minimal auth client interface for Razorpay hooks.
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).
94
+ * Primary: razorpay is set when razorpayClientPlugin() is used in createAuthClient({ plugins: [...] }); prefer it so requests hit the correct paths.
95
+ * Fallback: api is optional for custom clients that implement path-based api.get/api.post.
95
96
  */
96
97
  export interface RazorpayAuthClient {
97
- api: {
98
+ /** Set when razorpayClientPlugin() is used in createAuthClient({ plugins: [razorpayClientPlugin()] }). Prefer these methods over api.get/post. */
99
+ razorpay?: RazorpayClientActions
100
+ /** Optional; for custom clients that implement path-based api.get/post. */
101
+ api?: {
98
102
  get: (
99
103
  path: string,
100
104
  options?: { query?: Record<string, string> }
@@ -104,8 +108,6 @@ export interface RazorpayAuthClient {
104
108
  options?: { body?: Record<string, unknown> }
105
109
  ) => Promise<RazorpayApiResult<unknown>>
106
110
  }
107
- /** Set when razorpayClientPlugin() is used in createAuthClient({ plugins: [razorpayClientPlugin()] }). Prefer these methods over api.get/post. */
108
- razorpay?: RazorpayClientActions
109
111
  }
110
112
 
111
113
  /** Input for create-or-update subscription. */
package/lib/types.ts CHANGED
@@ -75,6 +75,7 @@ export interface RazorpayPlan {
75
75
  name: string
76
76
  monthlyPlanId: string
77
77
  annualPlanId?: string
78
+ description?: string
78
79
  limits?: PlanLimits
79
80
  freeTrial?: PlanFreeTrial
80
81
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deiondz/better-auth-razorpay",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
4
4
  "description": "Better Auth plugin for Razorpay subscriptions and payments",
5
5
  "type": "module",
6
6
  "main": "./index.ts",