convex-flutterwave 0.0.1

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.
Files changed (52) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +504 -0
  3. package/dist/client/_generated/_ignore.d.ts +1 -0
  4. package/dist/client/_generated/_ignore.d.ts.map +1 -0
  5. package/dist/client/_generated/_ignore.js +3 -0
  6. package/dist/client/_generated/_ignore.js.map +1 -0
  7. package/dist/client/index.d.ts +192 -0
  8. package/dist/client/index.d.ts.map +1 -0
  9. package/dist/client/index.js +364 -0
  10. package/dist/client/index.js.map +1 -0
  11. package/dist/component/_generated/api.d.ts +34 -0
  12. package/dist/component/_generated/api.d.ts.map +1 -0
  13. package/dist/component/_generated/api.js +31 -0
  14. package/dist/component/_generated/api.js.map +1 -0
  15. package/dist/component/_generated/component.d.ts +141 -0
  16. package/dist/component/_generated/component.d.ts.map +1 -0
  17. package/dist/component/_generated/component.js +11 -0
  18. package/dist/component/_generated/component.js.map +1 -0
  19. package/dist/component/_generated/dataModel.d.ts +46 -0
  20. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  21. package/dist/component/_generated/dataModel.js +11 -0
  22. package/dist/component/_generated/dataModel.js.map +1 -0
  23. package/dist/component/_generated/server.d.ts +133 -0
  24. package/dist/component/_generated/server.d.ts.map +1 -0
  25. package/dist/component/_generated/server.js +80 -0
  26. package/dist/component/_generated/server.js.map +1 -0
  27. package/dist/component/convex.config.d.ts +3 -0
  28. package/dist/component/convex.config.d.ts.map +1 -0
  29. package/dist/component/convex.config.js +4 -0
  30. package/dist/component/convex.config.js.map +1 -0
  31. package/dist/component/lib.d.ts +117 -0
  32. package/dist/component/lib.d.ts.map +1 -0
  33. package/dist/component/lib.js +223 -0
  34. package/dist/component/lib.js.map +1 -0
  35. package/dist/component/schema.d.ts +73 -0
  36. package/dist/component/schema.d.ts.map +1 -0
  37. package/dist/component/schema.js +43 -0
  38. package/dist/component/schema.js.map +1 -0
  39. package/package.json +106 -0
  40. package/src/client/_generated/_ignore.ts +1 -0
  41. package/src/client/index.ts +565 -0
  42. package/src/client/setup.test.ts +26 -0
  43. package/src/component/_generated/api.ts +50 -0
  44. package/src/component/_generated/component.ts +183 -0
  45. package/src/component/_generated/dataModel.ts +60 -0
  46. package/src/component/_generated/server.ts +169 -0
  47. package/src/component/convex.config.ts +5 -0
  48. package/src/component/lib.test.ts +110 -0
  49. package/src/component/lib.ts +251 -0
  50. package/src/component/schema.ts +50 -0
  51. package/src/component/setup.test.ts +11 -0
  52. package/src/test.ts +18 -0
@@ -0,0 +1,565 @@
1
+ import { httpActionGeneric } from "convex/server";
2
+ import type { GenericActionCtx, GenericDataModel } from "convex/server";
3
+ import type { ComponentApi } from "../component/_generated/component.js";
4
+
5
+ const FLUTTERWAVE_API_BASE = "https://api.flutterwave.com/v3";
6
+
7
+ export type FlutterwaveOptions = {
8
+ secretKey: string;
9
+ webhookSecretHash: string;
10
+ };
11
+
12
+ export type FlutterwavePaymentOption =
13
+ | "card"
14
+ | "account"
15
+ | "banktransfer"
16
+ | "ussd"
17
+ | "nqr"
18
+ | "mpesa"
19
+ | "mobilemoneyghana"
20
+ | "mobilemoneyuganda"
21
+ | "mobilemoneyrwanda"
22
+ | "mobilemoneyzambia"
23
+ | "barter"
24
+ | "credit"
25
+ | "opay"
26
+ | "fawrypay";
27
+
28
+ export type InitializeTransactionArgs = {
29
+ email: string;
30
+ amount: number;
31
+ currency?: string;
32
+ redirectUrl: string;
33
+ txRef?: string;
34
+ customerName?: string;
35
+ customerPhoneNumber?: string;
36
+ /** Restricts which payment methods Flutterwave's hosted page offers. Omit to offer everything enabled on your account. */
37
+ paymentOptions?: FlutterwavePaymentOption[];
38
+ /** Links this checkout to a payment plan, turning it into a subscription. See createPaymentPlan(). */
39
+ paymentPlan?: string;
40
+ metadata?: Record<string, unknown>;
41
+ };
42
+
43
+ export type InitializeTransactionResult = {
44
+ paymentLink: string;
45
+ txRef: string;
46
+ };
47
+
48
+ export type VerifyTransactionResult = {
49
+ status: string;
50
+ txRef: string;
51
+ flwRef?: string;
52
+ transactionId: string;
53
+ amount: number;
54
+ currency: string;
55
+ paymentType?: string;
56
+ narration?: string;
57
+ paidAt?: number;
58
+ customerEmail: string;
59
+ };
60
+
61
+ export type CreatePaymentPlanArgs = {
62
+ name: string;
63
+ amount: number;
64
+ interval: "hourly" | "daily" | "weekly" | "monthly" | "quarterly" | "yearly" | "bi-annually";
65
+ currency?: string;
66
+ duration?: number;
67
+ };
68
+
69
+ export type PaymentPlanResult = {
70
+ planId: string;
71
+ name: string;
72
+ amount: number;
73
+ interval: string;
74
+ currency: string;
75
+ duration?: number;
76
+ status: string;
77
+ };
78
+
79
+ // Flutterwave does not sign webhook payloads. Instead, it echoes back the
80
+ // secret hash you configured in the Dashboard, verbatim, in a `verif-hash`
81
+ // header — verification is a direct (constant-time) string comparison, not
82
+ // an HMAC digest of the request body. See:
83
+ // https://developer.flutterwave.com/docs/integration-guides/webhooks
84
+ function timingSafeEqual(a: string, b: string): boolean {
85
+ if (a.length !== b.length) return false;
86
+ let mismatch = 0;
87
+ for (let i = 0; i < a.length; i++) {
88
+ mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
89
+ }
90
+ return mismatch === 0;
91
+ }
92
+
93
+ export class Flutterwave {
94
+ webhookHandler: ReturnType<typeof httpActionGeneric>;
95
+
96
+ constructor(
97
+ private component: ComponentApi,
98
+ private options: FlutterwaveOptions,
99
+ ) {
100
+ const component_ = component;
101
+ const webhookSecretHash = options.webhookSecretHash;
102
+
103
+ this.webhookHandler = httpActionGeneric(async (ctx, request) => {
104
+ const rawBody = await request.text();
105
+ const signature = request.headers.get("verif-hash");
106
+
107
+ if (!signature) {
108
+ return new Response(JSON.stringify({ error: "Missing signature" }), {
109
+ status: 400,
110
+ headers: { "Content-Type": "application/json" },
111
+ });
112
+ }
113
+
114
+ if (!timingSafeEqual(webhookSecretHash, signature)) {
115
+ console.error("convex-flutterwave: webhook signature mismatch");
116
+ return new Response(JSON.stringify({ error: "Invalid signature" }), {
117
+ status: 401,
118
+ headers: { "Content-Type": "application/json" },
119
+ });
120
+ }
121
+
122
+ let event: { event: string; data: Record<string, unknown> };
123
+ try {
124
+ event = JSON.parse(rawBody);
125
+ } catch {
126
+ return new Response(JSON.stringify({ error: "Invalid JSON" }), {
127
+ status: 400,
128
+ headers: { "Content-Type": "application/json" },
129
+ });
130
+ }
131
+
132
+ const data = event.data ?? {};
133
+ const eventId =
134
+ data.id !== undefined
135
+ ? `${event.event}:${data.id}`
136
+ : `${event.event}:${(data.tx_ref as string) ?? crypto.randomUUID()}`;
137
+
138
+ const { alreadyProcessed } = await ctx.runMutation(component_.lib.checkAndRecordEvent, {
139
+ eventId,
140
+ eventType: event.event,
141
+ txRef: (data.tx_ref as string) ?? undefined,
142
+ payload: rawBody,
143
+ });
144
+
145
+ if (alreadyProcessed) {
146
+ return new Response(JSON.stringify({ success: true, duplicate: true }), {
147
+ status: 200,
148
+ headers: { "Content-Type": "application/json" },
149
+ });
150
+ }
151
+
152
+ switch (event.event) {
153
+ case "charge.completed": {
154
+ const customer = data.customer as Record<string, unknown> | undefined;
155
+ const status = (data.status as string) === "successful" ? "successful" : "failed";
156
+ await ctx.runMutation(component_.lib.recordTransaction, {
157
+ txRef: data.tx_ref as string,
158
+ flwRef: (data.flw_ref as string) ?? undefined,
159
+ transactionId: data.id !== undefined ? String(data.id) : undefined,
160
+ customerEmail: (customer?.email as string) ?? "",
161
+ amount: data.amount as number,
162
+ currency: (data.currency as string) ?? "NGN",
163
+ status,
164
+ paymentType: (data.payment_type as string) ?? undefined,
165
+ narration: (data.narration as string) ?? undefined,
166
+ paidAt: data.created_at ? new Date(data.created_at as string).getTime() : undefined,
167
+ metadata: data.meta ? JSON.stringify(data.meta) : undefined,
168
+ });
169
+ break;
170
+ }
171
+ case "subscription.cancelled": {
172
+ const planId = (data.plan as string) ?? undefined;
173
+ const customer = data.customer as Record<string, unknown> | undefined;
174
+ if (data.id !== undefined) {
175
+ await ctx.runMutation(component_.lib.recordSubscriptionEvent, {
176
+ subscriptionId: String(data.id),
177
+ customerEmail: (customer?.email as string) ?? "",
178
+ planId,
179
+ amount: (data.amount as number) ?? undefined,
180
+ status: "cancelled",
181
+ });
182
+ }
183
+ break;
184
+ }
185
+ default:
186
+ break;
187
+ }
188
+
189
+ return new Response(JSON.stringify({ success: true }), {
190
+ status: 200,
191
+ headers: { "Content-Type": "application/json" },
192
+ });
193
+ });
194
+ }
195
+
196
+ async initializeTransaction(
197
+ ctx: GenericActionCtx<GenericDataModel>,
198
+ args: InitializeTransactionArgs,
199
+ ): Promise<InitializeTransactionResult> {
200
+ const txRef = args.txRef ?? crypto.randomUUID();
201
+
202
+ const res = await fetch(`${FLUTTERWAVE_API_BASE}/payments`, {
203
+ method: "POST",
204
+ headers: {
205
+ Authorization: `Bearer ${this.options.secretKey}`,
206
+ "Content-Type": "application/json",
207
+ },
208
+ body: JSON.stringify({
209
+ tx_ref: txRef,
210
+ amount: args.amount,
211
+ currency: args.currency ?? "NGN",
212
+ redirect_url: args.redirectUrl,
213
+ // Flutterwave requires payment_plan as a numeric id, not a string —
214
+ // this component's own PaymentPlanResult.planId is a string (for
215
+ // consistency with every other id in this API), so it's converted
216
+ // here. Sending it as a string doesn't error; Flutterwave just
217
+ // silently processes the charge as a normal one-time payment
218
+ // instead of linking it to the plan, so no subscription is created.
219
+ payment_plan: args.paymentPlan !== undefined ? Number(args.paymentPlan) : undefined,
220
+ payment_options: args.paymentOptions?.join(", "),
221
+ customer: {
222
+ email: args.email,
223
+ name: args.customerName,
224
+ phonenumber: args.customerPhoneNumber,
225
+ },
226
+ meta: args.metadata,
227
+ }),
228
+ });
229
+ const json = (await res.json()) as {
230
+ status: string;
231
+ message?: string;
232
+ data: { link: string };
233
+ };
234
+ if (json.status !== "success") {
235
+ throw new Error(json.message ?? "Failed to initialize Flutterwave transaction");
236
+ }
237
+
238
+ await ctx.runMutation(this.component.lib.recordTransaction, {
239
+ txRef,
240
+ customerEmail: args.email,
241
+ amount: args.amount,
242
+ currency: args.currency ?? "NGN",
243
+ status: "pending",
244
+ metadata: args.metadata ? JSON.stringify(args.metadata) : undefined,
245
+ });
246
+
247
+ return { paymentLink: json.data.link, txRef };
248
+ }
249
+
250
+ async verifyTransaction(
251
+ ctx: GenericActionCtx<GenericDataModel>,
252
+ args: { transactionId: string },
253
+ ): Promise<VerifyTransactionResult> {
254
+ const res = await fetch(
255
+ `${FLUTTERWAVE_API_BASE}/transactions/${encodeURIComponent(args.transactionId)}/verify`,
256
+ { headers: { Authorization: `Bearer ${this.options.secretKey}` } },
257
+ );
258
+ const json = (await res.json()) as {
259
+ status: string;
260
+ message?: string;
261
+ data: {
262
+ id: number;
263
+ tx_ref: string;
264
+ flw_ref?: string;
265
+ status: string;
266
+ amount: number;
267
+ currency: string;
268
+ payment_type?: string;
269
+ narration?: string;
270
+ created_at?: string;
271
+ customer?: { email?: string };
272
+ };
273
+ };
274
+ if (json.status !== "success") {
275
+ throw new Error(json.message ?? "Failed to verify Flutterwave transaction");
276
+ }
277
+ const data = json.data;
278
+ const status = data.status === "successful" ? "successful" : "failed";
279
+
280
+ await ctx.runMutation(this.component.lib.recordTransaction, {
281
+ txRef: data.tx_ref,
282
+ flwRef: data.flw_ref ?? undefined,
283
+ transactionId: String(data.id),
284
+ customerEmail: data.customer?.email ?? "",
285
+ amount: data.amount,
286
+ currency: data.currency,
287
+ status,
288
+ paymentType: data.payment_type ?? undefined,
289
+ narration: data.narration ?? undefined,
290
+ paidAt: data.created_at ? new Date(data.created_at).getTime() : undefined,
291
+ });
292
+
293
+ // recordTransaction keeps the email the transaction was first created
294
+ // under rather than whatever Flutterwave just echoed back (see its
295
+ // comment — test mode rewrites it). Read that back so callers — and
296
+ // anything they render, like a result screen — see the real address
297
+ // too, not just what's in the database.
298
+ const stored = await ctx.runQuery(this.component.lib.getTransaction, { txRef: data.tx_ref });
299
+
300
+ return {
301
+ status: data.status,
302
+ txRef: data.tx_ref,
303
+ flwRef: data.flw_ref ?? undefined,
304
+ transactionId: String(data.id),
305
+ amount: data.amount,
306
+ currency: data.currency,
307
+ paymentType: data.payment_type ?? undefined,
308
+ narration: data.narration ?? undefined,
309
+ paidAt: data.created_at ? new Date(data.created_at).getTime() : undefined,
310
+ customerEmail: stored?.customerEmail ?? data.customer?.email ?? "",
311
+ };
312
+ }
313
+
314
+ async cancelSubscription(
315
+ ctx: GenericActionCtx<GenericDataModel>,
316
+ args: { subscriptionId: string; customerEmail: string },
317
+ ): Promise<void> {
318
+ const res = await fetch(
319
+ `${FLUTTERWAVE_API_BASE}/subscriptions/${encodeURIComponent(args.subscriptionId)}/cancel`,
320
+ { method: "PUT", headers: { Authorization: `Bearer ${this.options.secretKey}` } },
321
+ );
322
+ const json = (await res.json()) as { status: string; message?: string };
323
+ if (json.status !== "success") {
324
+ throw new Error(json.message ?? "Failed to cancel Flutterwave subscription");
325
+ }
326
+ await ctx.runMutation(this.component.lib.recordSubscriptionEvent, {
327
+ subscriptionId: args.subscriptionId,
328
+ customerEmail: args.customerEmail,
329
+ status: "cancelled",
330
+ });
331
+ }
332
+
333
+ async enableSubscription(
334
+ ctx: GenericActionCtx<GenericDataModel>,
335
+ args: { subscriptionId: string; customerEmail: string },
336
+ ): Promise<void> {
337
+ const res = await fetch(
338
+ `${FLUTTERWAVE_API_BASE}/subscriptions/${encodeURIComponent(args.subscriptionId)}/activate`,
339
+ { method: "PUT", headers: { Authorization: `Bearer ${this.options.secretKey}` } },
340
+ );
341
+ const json = (await res.json()) as { status: string; message?: string };
342
+ if (json.status !== "success") {
343
+ throw new Error(json.message ?? "Failed to activate Flutterwave subscription");
344
+ }
345
+ await ctx.runMutation(this.component.lib.recordSubscriptionEvent, {
346
+ subscriptionId: args.subscriptionId,
347
+ customerEmail: args.customerEmail,
348
+ status: "active",
349
+ });
350
+ }
351
+
352
+ async createPaymentPlan(
353
+ ctx: GenericActionCtx<GenericDataModel>,
354
+ args: CreatePaymentPlanArgs,
355
+ ): Promise<PaymentPlanResult> {
356
+ const res = await fetch(`${FLUTTERWAVE_API_BASE}/payment-plans`, {
357
+ method: "POST",
358
+ headers: {
359
+ Authorization: `Bearer ${this.options.secretKey}`,
360
+ "Content-Type": "application/json",
361
+ },
362
+ body: JSON.stringify({
363
+ name: args.name,
364
+ amount: args.amount,
365
+ interval: args.interval,
366
+ currency: args.currency ?? "NGN",
367
+ duration: args.duration,
368
+ }),
369
+ });
370
+ const json = (await res.json()) as {
371
+ status: string;
372
+ message?: string;
373
+ data: {
374
+ id: number;
375
+ name: string;
376
+ amount: number;
377
+ interval: string;
378
+ currency: string;
379
+ duration?: number;
380
+ status: string;
381
+ };
382
+ };
383
+ if (json.status !== "success") {
384
+ throw new Error(json.message ?? "Failed to create Flutterwave payment plan");
385
+ }
386
+ return {
387
+ planId: String(json.data.id),
388
+ name: json.data.name,
389
+ amount: json.data.amount,
390
+ interval: json.data.interval,
391
+ currency: json.data.currency,
392
+ duration: json.data.duration,
393
+ status: json.data.status,
394
+ };
395
+ }
396
+
397
+ async listPaymentPlans(
398
+ _ctx: GenericActionCtx<GenericDataModel>,
399
+ ): Promise<PaymentPlanResult[]> {
400
+ const res = await fetch(`${FLUTTERWAVE_API_BASE}/payment-plans`, {
401
+ headers: { Authorization: `Bearer ${this.options.secretKey}` },
402
+ });
403
+ const json = (await res.json()) as {
404
+ status: string;
405
+ message?: string;
406
+ data: Array<{
407
+ id: number;
408
+ name: string;
409
+ amount: number;
410
+ interval: string;
411
+ currency: string;
412
+ duration?: number;
413
+ status: string;
414
+ }>;
415
+ };
416
+ if (json.status !== "success") {
417
+ throw new Error(json.message ?? "Failed to list Flutterwave payment plans");
418
+ }
419
+ return json.data.map((plan) => ({
420
+ planId: String(plan.id),
421
+ name: plan.name,
422
+ amount: plan.amount,
423
+ interval: plan.interval,
424
+ currency: plan.currency,
425
+ duration: plan.duration,
426
+ status: plan.status,
427
+ }));
428
+ }
429
+
430
+ /**
431
+ * Flutterwave has no "subscription created" webhook — a subscription is
432
+ * created implicitly the first time a customer pays through a checkout
433
+ * initialized with `paymentPlan`, and that charge webhook doesn't carry
434
+ * the subscription's own id (only the plan id). This reads the customer's
435
+ * subscriptions straight from Flutterwave's Subscriptions API and upserts
436
+ * them locally — call it after a plan-linked checkout returns, or from a
437
+ * manual "sync" action, as a reliable way to populate subscription state.
438
+ */
439
+ private async fetchFlutterwaveSubscriptions(email?: string): Promise<{
440
+ status: string;
441
+ message?: string;
442
+ data?: Array<{
443
+ id: number;
444
+ amount?: number;
445
+ plan?: number;
446
+ status: string;
447
+ customer?: { customer_email?: string };
448
+ }>;
449
+ }> {
450
+ const url = email
451
+ ? `${FLUTTERWAVE_API_BASE}/subscriptions?email=${encodeURIComponent(email)}`
452
+ : `${FLUTTERWAVE_API_BASE}/subscriptions`;
453
+ const res = await fetch(url, { headers: { Authorization: `Bearer ${this.options.secretKey}` } });
454
+ return await res.json();
455
+ }
456
+
457
+ async syncCustomerSubscriptions(
458
+ ctx: GenericActionCtx<GenericDataModel>,
459
+ args: { email: string },
460
+ ): Promise<number> {
461
+ const json = await this.fetchFlutterwaveSubscriptions(args.email);
462
+ if (json.status !== "success") {
463
+ throw new Error(json.message ?? "Failed to fetch Flutterwave subscriptions");
464
+ }
465
+ let subscriptions = json.data ?? [];
466
+
467
+ // Flutterwave's ?email= filter matches against whatever email it has on
468
+ // file for the subscription — in test mode that's the same "ravesb_
469
+ // <hash>_" rewritten address seen elsewhere, not the real one, so
470
+ // filtering by the real email can come back empty even when a matching
471
+ // subscription exists. Fall back to an unfiltered list and match after
472
+ // stripping that rewrite, rather than trusting the filter alone.
473
+ if (subscriptions.length === 0) {
474
+ const all = await this.fetchFlutterwaveSubscriptions();
475
+ if (all.status === "success") {
476
+ subscriptions = (all.data ?? []).filter(
477
+ (sub) =>
478
+ sub.customer?.customer_email?.replace(/^ravesb_[0-9a-f]+_/i, "") === args.email,
479
+ );
480
+ }
481
+ }
482
+
483
+ for (const sub of subscriptions) {
484
+ await ctx.runMutation(this.component.lib.recordSubscriptionEvent, {
485
+ subscriptionId: String(sub.id),
486
+ // Use the email the caller queried by, not Flutterwave's own
487
+ // customer_email field — in test mode Flutterwave rewrites it (e.g.
488
+ // "ravesb_<hash>_real@email.com"), which would make the record
489
+ // unfindable by the email a merchant actually knows the customer as.
490
+ customerEmail: args.email,
491
+ planId: sub.plan !== undefined ? String(sub.plan) : undefined,
492
+ amount: sub.amount ?? undefined,
493
+ status: sub.status === "active" ? "active" : "cancelled",
494
+ });
495
+ }
496
+ return subscriptions.length;
497
+ }
498
+
499
+ /**
500
+ * Attributes every subscription on a given plan to one email, regardless
501
+ * of what Flutterwave itself has on file as that subscription's customer.
502
+ * This is unsafe to reach for by default — a real plan can have many
503
+ * subscribers — but it's exactly what's needed when Flutterwave's
504
+ * test-mode sandbox has substituted its own fixed customer identity for
505
+ * every subscription (see the README's Subscriptions section), making
506
+ * email-based sync unable to find anything at all. Reach for it only
507
+ * where you already know (or, in a single-developer test/demo context,
508
+ * can safely assume) there's one subscriber to claim — the example app
509
+ * uses it as a fallback for exactly that reason.
510
+ */
511
+ async syncSubscriptionsByPlan(
512
+ ctx: GenericActionCtx<GenericDataModel>,
513
+ args: { planId: string; email: string },
514
+ ): Promise<number> {
515
+ const json = await this.fetchFlutterwaveSubscriptions();
516
+ if (json.status !== "success") {
517
+ throw new Error(json.message ?? "Failed to fetch Flutterwave subscriptions");
518
+ }
519
+ const planId = Number(args.planId);
520
+ const subscriptions = (json.data ?? []).filter((sub) => sub.plan === planId);
521
+
522
+ for (const sub of subscriptions) {
523
+ await ctx.runMutation(this.component.lib.recordSubscriptionEvent, {
524
+ subscriptionId: String(sub.id),
525
+ customerEmail: args.email,
526
+ planId: String(planId),
527
+ amount: sub.amount ?? undefined,
528
+ status: sub.status === "active" ? "active" : "cancelled",
529
+ });
530
+ }
531
+ return subscriptions.length;
532
+ }
533
+
534
+ async getTransaction(ctx: RunQueryCtx, args: { txRef: string }) {
535
+ return await ctx.runQuery(this.component.lib.getTransaction, args);
536
+ }
537
+
538
+ async listTransactions(ctx: RunQueryCtx, args: { customerEmail: string; limit?: number }) {
539
+ return await ctx.runQuery(this.component.lib.listTransactions, args);
540
+ }
541
+
542
+ async getSubscription(ctx: RunQueryCtx, args: { subscriptionId: string }) {
543
+ return await ctx.runQuery(this.component.lib.getSubscription, args);
544
+ }
545
+
546
+ async listSubscriptions(ctx: RunQueryCtx, args: { customerEmail: string }) {
547
+ return await ctx.runQuery(this.component.lib.listSubscriptions, args);
548
+ }
549
+
550
+ async hasActiveSubscription(ctx: RunQueryCtx, args: { customerEmail: string }): Promise<boolean> {
551
+ return await ctx.runQuery(this.component.lib.hasActiveSubscription, args);
552
+ }
553
+
554
+ async listRecentEvents(ctx: RunQueryCtx, args?: { limit?: number }) {
555
+ return await ctx.runQuery(this.component.lib.listRecentEvents, args ?? {});
556
+ }
557
+
558
+ async getStats(ctx: RunQueryCtx) {
559
+ return await ctx.runQuery(this.component.lib.getStats, {});
560
+ }
561
+ }
562
+
563
+ type RunQueryCtx = {
564
+ runQuery: GenericActionCtx<GenericDataModel>["runQuery"];
565
+ };
@@ -0,0 +1,26 @@
1
+ /// <reference types="vite/client" />
2
+ import { test } from "vitest";
3
+ import { convexTest } from "convex-test";
4
+ export const modules = import.meta.glob("./**/*.*s");
5
+
6
+ import {
7
+ defineSchema,
8
+ type GenericSchema,
9
+ type SchemaDefinition,
10
+ } from "convex/server";
11
+ import { type ComponentApi } from "../component/_generated/component.js";
12
+ import { componentsGeneric } from "convex/server";
13
+ import { register } from "../test.js";
14
+
15
+ export function initConvexTest<
16
+ Schema extends SchemaDefinition<GenericSchema, boolean>,
17
+ >(schema?: Schema) {
18
+ const t = convexTest(schema ?? defineSchema({}), modules);
19
+ register(t);
20
+ return t;
21
+ }
22
+ export const components = componentsGeneric() as unknown as {
23
+ convexFlutterwave: ComponentApi;
24
+ };
25
+
26
+ test("setup", () => {});
@@ -0,0 +1,50 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * Generated `api` utility.
4
+ *
5
+ * THIS CODE IS AUTOMATICALLY GENERATED.
6
+ *
7
+ * To regenerate, run `npx convex dev`.
8
+ * @module
9
+ */
10
+
11
+ import type * as lib from "../lib.js";
12
+
13
+ import type {
14
+ ApiFromModules,
15
+ FilterApi,
16
+ FunctionReference,
17
+ } from "convex/server";
18
+ import { anyApi, componentsGeneric } from "convex/server";
19
+
20
+ const fullApi: ApiFromModules<{
21
+ lib: typeof lib;
22
+ }> = anyApi as any;
23
+
24
+ /**
25
+ * A utility for referencing Convex functions in your app's public API.
26
+ *
27
+ * Usage:
28
+ * ```js
29
+ * const myFunctionReference = api.myModule.myFunction;
30
+ * ```
31
+ */
32
+ export const api: FilterApi<
33
+ typeof fullApi,
34
+ FunctionReference<any, "public">
35
+ > = anyApi as any;
36
+
37
+ /**
38
+ * A utility for referencing Convex functions in your app's internal API.
39
+ *
40
+ * Usage:
41
+ * ```js
42
+ * const myFunctionReference = internal.myModule.myFunction;
43
+ * ```
44
+ */
45
+ export const internal: FilterApi<
46
+ typeof fullApi,
47
+ FunctionReference<any, "internal">
48
+ > = anyApi as any;
49
+
50
+ export const components = componentsGeneric() as unknown as {};