convex-paystack 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.
- package/LICENSE +201 -0
- package/README.md +490 -0
- package/dist/client/_generated/_ignore.d.ts +1 -0
- package/dist/client/_generated/_ignore.d.ts.map +1 -0
- package/dist/client/_generated/_ignore.js +3 -0
- package/dist/client/_generated/_ignore.js.map +1 -0
- package/dist/client/index.d.ts +201 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +383 -0
- package/dist/client/index.js.map +1 -0
- package/dist/component/_generated/api.d.ts +34 -0
- package/dist/component/_generated/api.d.ts.map +1 -0
- package/dist/component/_generated/api.js +31 -0
- package/dist/component/_generated/api.js.map +1 -0
- package/dist/component/_generated/component.d.ts +147 -0
- package/dist/component/_generated/component.d.ts.map +1 -0
- package/dist/component/_generated/component.js +11 -0
- package/dist/component/_generated/component.js.map +1 -0
- package/dist/component/_generated/dataModel.d.ts +46 -0
- package/dist/component/_generated/dataModel.d.ts.map +1 -0
- package/dist/component/_generated/dataModel.js +11 -0
- package/dist/component/_generated/dataModel.js.map +1 -0
- package/dist/component/_generated/server.d.ts +133 -0
- package/dist/component/_generated/server.d.ts.map +1 -0
- package/dist/component/_generated/server.js +80 -0
- package/dist/component/_generated/server.js.map +1 -0
- package/dist/component/convex.config.d.ts +3 -0
- package/dist/component/convex.config.d.ts.map +1 -0
- package/dist/component/convex.config.js +4 -0
- package/dist/component/convex.config.js.map +1 -0
- package/dist/component/lib.d.ts +137 -0
- package/dist/component/lib.d.ts.map +1 -0
- package/dist/component/lib.js +241 -0
- package/dist/component/lib.js.map +1 -0
- package/dist/component/schema.d.ts +77 -0
- package/dist/component/schema.d.ts.map +1 -0
- package/dist/component/schema.js +45 -0
- package/dist/component/schema.js.map +1 -0
- package/package.json +106 -0
- package/src/client/_generated/_ignore.ts +1 -0
- package/src/client/index.ts +570 -0
- package/src/client/setup.test.ts +26 -0
- package/src/component/_generated/api.ts +50 -0
- package/src/component/_generated/component.ts +217 -0
- package/src/component/_generated/dataModel.ts +60 -0
- package/src/component/_generated/server.ts +169 -0
- package/src/component/convex.config.ts +5 -0
- package/src/component/lib.test.ts +110 -0
- package/src/component/lib.ts +278 -0
- package/src/component/schema.ts +58 -0
- package/src/component/setup.test.ts +11 -0
- package/src/test.ts +18 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright [yyyy] [name of copyright owner]
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
# convex-paystack
|
|
2
|
+
|
|
3
|
+
**Accept payments and subscriptions with Paystack in your Convex app.** Reactive transactions, subscription state, and webhook ingestion.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/convex-paystack)
|
|
6
|
+
[](https://www.convex.dev/components/sholajegede/convex-paystack)
|
|
7
|
+
[](https://www.npmjs.com/package/convex-paystack)
|
|
8
|
+
[](./LICENSE)
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
const paystack = new Paystack(components.convexPaystack, {
|
|
12
|
+
secretKey: process.env.PAYSTACK_SECRET_KEY!,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Start a checkout
|
|
16
|
+
const { authorizationUrl } = await paystack.initializeTransaction(ctx, {
|
|
17
|
+
email: "customer@example.com",
|
|
18
|
+
amount: 500000, // kobo
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Confirm it server-side after redirect
|
|
22
|
+
const result = await paystack.verifyTransaction(ctx, { reference });
|
|
23
|
+
|
|
24
|
+
// Is this customer on an active subscription?
|
|
25
|
+
const active = await paystack.hasActiveSubscription(ctx, { customerEmail: "customer@example.com" });
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## What this does
|
|
29
|
+
|
|
30
|
+
Paystack fires webhook events every time a payment or subscription action happens — a charge succeeds, a subscription is created, an invoice is billed. Without this component, you have to write and maintain your own webhook receiver, HMAC signature verification, database schema, and reactive queries.
|
|
31
|
+
|
|
32
|
+
This component owns all of that. Drop it in, mount the webhook, and your Convex app immediately has:
|
|
33
|
+
|
|
34
|
+
- **Reactive transaction state** — every transaction, live in Convex, keyed by reference
|
|
35
|
+
- **Reactive subscription state** — subscription status, plan, next payment date, live in Convex
|
|
36
|
+
- **Checkout** — `initializeTransaction()` generates a Paystack-hosted checkout link for one-time payments or, with a `plan` code, for subscriptions
|
|
37
|
+
- **Plan management** — `createPlan()` / `listPlans()` manage the billing plans subscriptions are built on
|
|
38
|
+
- **Server-side verification** — `verifyTransaction()` confirms a transaction directly with Paystack
|
|
39
|
+
- **Subscription management** — `cancelSubscription()` / `enableSubscription()` call Paystack directly and keep local state in sync
|
|
40
|
+
- **Webhook idempotency** — duplicate deliveries of the same event are detected and skipped
|
|
41
|
+
|
|
42
|
+
> **Webhook timing:** After a payment or subscription action occurs in Paystack, there is a short delay — usually a few seconds — before the webhook arrives and your Convex data updates. Once the webhook arrives, Convex's real-time reactivity propagates the change to all subscribers instantly.
|
|
43
|
+
|
|
44
|
+
## Table of Contents
|
|
45
|
+
|
|
46
|
+
- [Install](#install)
|
|
47
|
+
- [Quick Start](#quick-start)
|
|
48
|
+
- [Setup](#setup)
|
|
49
|
+
- [Usage](#usage)
|
|
50
|
+
- [Checkout](#checkout)
|
|
51
|
+
- [Plans](#plans)
|
|
52
|
+
- [Subscriptions](#subscriptions)
|
|
53
|
+
- [API Reference](#api-reference)
|
|
54
|
+
- [Type Reference](#type-reference)
|
|
55
|
+
- [Webhook Events](#webhook-events)
|
|
56
|
+
- [Database Schema](#database-schema)
|
|
57
|
+
- [Customer IDs](#customer-ids)
|
|
58
|
+
- [Testing](#testing)
|
|
59
|
+
- [Example App](#example-app)
|
|
60
|
+
- [Limitations](#limitations)
|
|
61
|
+
- [Troubleshooting](#troubleshooting)
|
|
62
|
+
- [Contributing](#contributing)
|
|
63
|
+
- [Changelog](#changelog)
|
|
64
|
+
|
|
65
|
+
## Install
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npm install convex-paystack
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Requirements:** Convex v1.34.1 or later, Node.js 18+, a [Paystack](https://paystack.com) account
|
|
72
|
+
|
|
73
|
+
## Quick Start
|
|
74
|
+
|
|
75
|
+
Five steps to add Paystack to your Convex app.
|
|
76
|
+
|
|
77
|
+
### 1. Add the component
|
|
78
|
+
|
|
79
|
+
In `convex/convex.config.ts`:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { defineApp } from "convex/server";
|
|
83
|
+
import convexPaystack from "convex-paystack/convex.config";
|
|
84
|
+
|
|
85
|
+
const app = defineApp();
|
|
86
|
+
app.use(convexPaystack);
|
|
87
|
+
|
|
88
|
+
export default app;
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 2. Set environment variables
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
npx convex env set PAYSTACK_SECRET_KEY sk_live_xxxxxxxxxxxx
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 3. Mount the webhook handler
|
|
98
|
+
|
|
99
|
+
In `convex/http.ts`:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import { httpRouter } from "convex/server";
|
|
103
|
+
import { components } from "./_generated/api";
|
|
104
|
+
import { Paystack } from "convex-paystack";
|
|
105
|
+
|
|
106
|
+
const paystack = new Paystack(components.convexPaystack, {
|
|
107
|
+
secretKey: process.env.PAYSTACK_SECRET_KEY!,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const http = httpRouter();
|
|
111
|
+
|
|
112
|
+
http.route({
|
|
113
|
+
path: "/webhooks/paystack",
|
|
114
|
+
method: "POST",
|
|
115
|
+
handler: paystack.webhookHandler,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
export default http;
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 4. Register the webhook in Paystack
|
|
122
|
+
|
|
123
|
+
1. In Paystack Dashboard → **Settings → API Keys & Webhooks**
|
|
124
|
+
2. Set the webhook URL: `https://your-deployment.convex.site/webhooks/paystack`
|
|
125
|
+
3. Save. Paystack sends every event to this URL — the handler ignores events it doesn't recognize.
|
|
126
|
+
|
|
127
|
+
Your Convex site URL is in the Convex dashboard under **Settings → URL & Deploy Key** — it ends in `.convex.site`.
|
|
128
|
+
|
|
129
|
+
### 5. Initialize the client
|
|
130
|
+
|
|
131
|
+
In `convex/payments.ts`:
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import { components } from "./_generated/api";
|
|
135
|
+
import { Paystack } from "convex-paystack";
|
|
136
|
+
|
|
137
|
+
export const paystack = new Paystack(components.convexPaystack, {
|
|
138
|
+
secretKey: process.env.PAYSTACK_SECRET_KEY!,
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Import `paystack` from this file in any Convex function that needs payments.
|
|
143
|
+
|
|
144
|
+
## Setup
|
|
145
|
+
|
|
146
|
+
**`convex/payments.ts`** — your central payments module:
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
import { components } from "./_generated/api";
|
|
150
|
+
import { Paystack } from "convex-paystack";
|
|
151
|
+
import { action } from "./_generated/server";
|
|
152
|
+
import { v } from "convex/values";
|
|
153
|
+
|
|
154
|
+
export const paystack = new Paystack(components.convexPaystack, {
|
|
155
|
+
secretKey: process.env.PAYSTACK_SECRET_KEY!,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
export const checkout = action({
|
|
159
|
+
args: { email: v.string(), amount: v.number() },
|
|
160
|
+
handler: async (ctx, args) => paystack.initializeTransaction(ctx, args),
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**`convex/http.ts`** — webhook entry point (shown in [Quick Start](#quick-start)).
|
|
165
|
+
|
|
166
|
+
## Usage
|
|
167
|
+
|
|
168
|
+
### Start a checkout
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
export const checkout = action({
|
|
172
|
+
args: { email: v.string(), amount: v.number() },
|
|
173
|
+
handler: async (ctx, args) => {
|
|
174
|
+
return await paystack.initializeTransaction(ctx, {
|
|
175
|
+
email: args.email,
|
|
176
|
+
amount: args.amount, // subunit — kobo for NGN, pesewas for GHS, cents for USD
|
|
177
|
+
callbackUrl: "https://yourapp.com/payment/callback",
|
|
178
|
+
});
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
// Returns: { authorizationUrl, accessCode, reference }
|
|
182
|
+
// Redirect the customer to authorizationUrl.
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Verify a transaction
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
export const confirmPayment = action({
|
|
189
|
+
args: { reference: v.string() },
|
|
190
|
+
handler: async (ctx, args) => {
|
|
191
|
+
return await paystack.verifyTransaction(ctx, args);
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
// Returns: { status, reference, amount, currency, channel, paidAt, customerEmail, ... }
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Read a transaction reactively
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
export const getTransaction = query({
|
|
201
|
+
args: { reference: v.string() },
|
|
202
|
+
handler: async (ctx, args) => {
|
|
203
|
+
return await paystack.getTransaction(ctx, args);
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
// Returns: Transaction | null
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### List a customer's transactions
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
export const getHistory = query({
|
|
213
|
+
args: { customerEmail: v.string() },
|
|
214
|
+
handler: async (ctx, args) => {
|
|
215
|
+
return await paystack.listTransactions(ctx, {
|
|
216
|
+
customerEmail: args.customerEmail,
|
|
217
|
+
limit: 20,
|
|
218
|
+
});
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
// Returns: Transaction[] ordered newest first
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Checkout
|
|
225
|
+
|
|
226
|
+
`initializeTransaction()` calls Paystack's [Initialize Transaction](https://paystack.com/docs/payments/accept-payments/) endpoint, records a `pending` transaction locally, and returns the hosted checkout URL. Send the customer to `authorizationUrl`; Paystack redirects them back to `callbackUrl` after payment.
|
|
227
|
+
|
|
228
|
+
Call `verifyTransaction()` from your callback route (or rely on the `charge.success` webhook) to confirm the final status — never trust the client-side redirect alone.
|
|
229
|
+
|
|
230
|
+
Two optional arguments shape what the customer sees at checkout:
|
|
231
|
+
|
|
232
|
+
- **`channels`** restricts which payment methods Paystack's hosted page offers — any of `"card"`, `"bank"`, `"apple_pay"`, `"ussd"`, `"qr"`, `"mobile_money"`, `"bank_transfer"`, `"eft"`, `"capitec_pay"`, `"payattitude"`. Omit it to let Paystack offer everything enabled on your account.
|
|
233
|
+
- **`plan`** turns a one-time checkout into a subscription — see [Plans](#plans).
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
await paystack.initializeTransaction(ctx, {
|
|
237
|
+
email: "customer@example.com",
|
|
238
|
+
amount: 500000,
|
|
239
|
+
channels: ["card", "bank_transfer", "ussd"],
|
|
240
|
+
});
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Pass `currency` to charge in something other than your account's default (Paystack test accounts are usually NGN-only until you enable more in **Dashboard → Settings → Preferences**). Rather than guessing, check what's actually enabled with `listBalances()`:
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
const balances = await paystack.listBalances(ctx);
|
|
247
|
+
// [{ currency: "NGN", balance: 0 }]
|
|
248
|
+
const enabledCurrencies = balances.map((b) => b.currency);
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Charging a currency that isn't enabled throws `Currency not supported by merchant` — this component does not silently convert or substitute currencies for you.
|
|
252
|
+
|
|
253
|
+
## Plans
|
|
254
|
+
|
|
255
|
+
Plans are the billing schedule a subscription is built on — an amount, a currency, and an interval (`daily`, `weekly`, `monthly`, `quarterly`, `biannually`, `annually`). Create one, then pass its `planCode` to `initializeTransaction()`: the customer's first successful payment automatically starts the subscription, and this component records it as soon as the `subscription.create` webhook arrives.
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
export const createProPlan = action({
|
|
259
|
+
args: {},
|
|
260
|
+
handler: async (ctx) => {
|
|
261
|
+
return await paystack.createPlan(ctx, {
|
|
262
|
+
name: "Pro Monthly",
|
|
263
|
+
amount: 500000, // ₦5,000.00 in kobo
|
|
264
|
+
interval: "monthly",
|
|
265
|
+
currency: "NGN",
|
|
266
|
+
});
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
// Returns: { planCode, name, amount, interval, currency, description? }
|
|
270
|
+
|
|
271
|
+
export const startSubscription = action({
|
|
272
|
+
args: { email: v.string(), planCode: v.string(), amount: v.number() },
|
|
273
|
+
handler: async (ctx, args) => {
|
|
274
|
+
return await paystack.initializeTransaction(ctx, {
|
|
275
|
+
email: args.email,
|
|
276
|
+
amount: args.amount,
|
|
277
|
+
plan: args.planCode,
|
|
278
|
+
});
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
`listPlans()` returns every plan already created on your Paystack account, so you can check for an existing plan by name before creating a duplicate — this is exactly the pattern the [example app](#example-app) uses to bootstrap its demo plans on first run.
|
|
284
|
+
|
|
285
|
+
## Subscriptions
|
|
286
|
+
|
|
287
|
+
Once a plan exists, a subscription is created automatically the first time a customer pays through a checkout initialized with that `plan` code (see [Plans](#plans) above). This component mirrors subscription state reactively as webhooks arrive, and exposes cancel/enable:
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
export const cancelPlan = action({
|
|
291
|
+
args: { code: v.string(), token: v.string() },
|
|
292
|
+
handler: async (ctx, args) => {
|
|
293
|
+
await paystack.cancelSubscription(ctx, args);
|
|
294
|
+
return null;
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
`code` and `token` are the subscription's `subscription_code` and `email_token`, both delivered on the `subscription.create` webhook and available via `getSubscription()`.
|
|
300
|
+
|
|
301
|
+
If a subscription isn't showing up locally, it's almost always a webhook that hasn't reached your deployment yet (a fresh Convex project whose webhook URL was never registered in Paystack, most commonly). `syncCustomerSubscriptions()` is a fallback for exactly that — it reads the customer's live subscriptions from Paystack's [Fetch Customer](https://paystack.com/docs/api/customer/#fetch) endpoint and upserts them, no webhook required:
|
|
302
|
+
|
|
303
|
+
```ts
|
|
304
|
+
export const syncSubscriptions = action({
|
|
305
|
+
args: { email: v.string() },
|
|
306
|
+
handler: async (ctx, args) => {
|
|
307
|
+
return await paystack.syncCustomerSubscriptions(ctx, { email: args.email });
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
// Returns: number of subscriptions synced
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## API Reference
|
|
314
|
+
|
|
315
|
+
| Method | Kind | Description |
|
|
316
|
+
| --- | --- | --- |
|
|
317
|
+
| `initializeTransaction(ctx, args)` | action | Starts a Paystack checkout, returns the hosted payment link — pass `plan` to start a subscription |
|
|
318
|
+
| `verifyTransaction(ctx, args)` | action | Confirms a transaction's final status with Paystack |
|
|
319
|
+
| `createPlan(ctx, args)` | action | Creates a billing plan on Paystack |
|
|
320
|
+
| `listPlans(ctx)` | action | Lists every billing plan on your Paystack account |
|
|
321
|
+
| `cancelSubscription(ctx, args)` | action | Disables a subscription on Paystack and locally |
|
|
322
|
+
| `enableSubscription(ctx, args)` | action | Re-enables a non-renewing subscription |
|
|
323
|
+
| `getTransaction(ctx, args)` | query | Fetch one transaction by reference |
|
|
324
|
+
| `listTransactions(ctx, args)` | query | List a customer's transactions, newest first |
|
|
325
|
+
| `getSubscription(ctx, args)` | query | Fetch one subscription by subscription code |
|
|
326
|
+
| `listSubscriptions(ctx, args)` | query | List a customer's subscriptions |
|
|
327
|
+
| `hasActiveSubscription(ctx, args)` | query | `true` if the customer has an active or non-renewing subscription |
|
|
328
|
+
| `listRecentEvents(ctx, args?)` | query | Raw webhook event log, newest first — audit trail or a live console |
|
|
329
|
+
| `getStats(ctx)` | query | Aggregate transaction/subscription/event counts for a small dashboard |
|
|
330
|
+
| `listBalances(ctx)` | action | Currencies actually enabled on your Paystack account, with their available balance |
|
|
331
|
+
| `syncCustomerSubscriptions(ctx, args)` | action | Pulls a customer's subscriptions straight from Paystack and upserts them locally — a fallback for when the webhook hasn't arrived (or isn't registered) yet |
|
|
332
|
+
|
|
333
|
+
## Type Reference
|
|
334
|
+
|
|
335
|
+
```ts
|
|
336
|
+
type InitializeTransactionArgs = {
|
|
337
|
+
email: string;
|
|
338
|
+
amount: number;
|
|
339
|
+
currency?: string;
|
|
340
|
+
callbackUrl?: string;
|
|
341
|
+
reference?: string;
|
|
342
|
+
channels?: Array<
|
|
343
|
+
| "card" | "bank" | "apple_pay" | "ussd" | "qr"
|
|
344
|
+
| "mobile_money" | "bank_transfer" | "eft" | "capitec_pay" | "payattitude"
|
|
345
|
+
>;
|
|
346
|
+
plan?: string;
|
|
347
|
+
metadata?: Record<string, unknown>;
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
type CreatePlanArgs = {
|
|
351
|
+
name: string;
|
|
352
|
+
amount: number;
|
|
353
|
+
interval: "daily" | "weekly" | "monthly" | "quarterly" | "biannually" | "annually";
|
|
354
|
+
currency?: string;
|
|
355
|
+
description?: string;
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
type PlanResult = {
|
|
359
|
+
planCode: string;
|
|
360
|
+
name: string;
|
|
361
|
+
amount: number;
|
|
362
|
+
interval: string;
|
|
363
|
+
currency: string;
|
|
364
|
+
description?: string;
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
type Transaction = {
|
|
368
|
+
reference: string;
|
|
369
|
+
customerEmail: string;
|
|
370
|
+
amount: number;
|
|
371
|
+
currency: string;
|
|
372
|
+
status: "pending" | "success" | "failed" | "abandoned";
|
|
373
|
+
channel?: string;
|
|
374
|
+
gatewayResponse?: string;
|
|
375
|
+
authorizationCode?: string;
|
|
376
|
+
paidAt?: number;
|
|
377
|
+
metadata?: string;
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
type Subscription = {
|
|
381
|
+
subscriptionCode: string;
|
|
382
|
+
emailToken?: string;
|
|
383
|
+
customerEmail: string;
|
|
384
|
+
customerCode?: string;
|
|
385
|
+
planCode: string;
|
|
386
|
+
status: "active" | "non-renewing" | "attention" | "completed" | "cancelled";
|
|
387
|
+
amount?: number;
|
|
388
|
+
nextPaymentDate?: number;
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
type Balance = {
|
|
392
|
+
currency: string;
|
|
393
|
+
balance: number;
|
|
394
|
+
};
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
## Webhook Events
|
|
398
|
+
|
|
399
|
+
The webhook handler verifies the `x-paystack-signature` header (hex-encoded HMAC-SHA512 of the raw request body, keyed with your secret key) before processing anything, and de-duplicates by event id so retried deliveries are safe. It currently acts on:
|
|
400
|
+
|
|
401
|
+
| Event | Effect |
|
|
402
|
+
| --- | --- |
|
|
403
|
+
| `charge.success` | Upserts the transaction as `success` |
|
|
404
|
+
| `subscription.create` | Upserts the subscription record |
|
|
405
|
+
| `subscription.disable` | Marks the subscription `cancelled` |
|
|
406
|
+
| `subscription.not_renew` | Marks the subscription `non-renewing` |
|
|
407
|
+
| `invoice.update` | Marks the subscription `active` or `attention` depending on invoice status |
|
|
408
|
+
| `invoice.payment_failed` | Marks the subscription `attention` |
|
|
409
|
+
|
|
410
|
+
All other event types are accepted (HTTP 200) but ignored, so you can register every event on one endpoint without errors.
|
|
411
|
+
|
|
412
|
+
## Database Schema
|
|
413
|
+
|
|
414
|
+
```ts
|
|
415
|
+
transactions: {
|
|
416
|
+
reference, customerEmail, amount, currency, status,
|
|
417
|
+
channel?, gatewayResponse?, authorizationCode?, paidAt?, metadata?,
|
|
418
|
+
createdAt, updatedAt,
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
subscriptions: {
|
|
422
|
+
subscriptionCode, emailToken?, customerEmail, customerCode?, planCode, status,
|
|
423
|
+
amount?, nextPaymentDate?, createdAt, updatedAt,
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
webhookEvents: {
|
|
427
|
+
eventId, eventType, reference?, payload, receivedAt,
|
|
428
|
+
}
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
Plans are not stored locally — Paystack is the source of truth for them, the same way it is for verified transactions. `listPlans()` reads live from Paystack.
|
|
432
|
+
|
|
433
|
+
`listRecentEvents()` reads `webhookEvents` directly — every event this component's webhook handler has ever received, whether or not it changed a transaction or subscription. `getStats()` returns row counts across all three tables with a full scan, intended for a small dashboard rather than a high-volume production metric.
|
|
434
|
+
|
|
435
|
+
## Customer IDs
|
|
436
|
+
|
|
437
|
+
This component keys everything on `customerEmail` — the email Paystack has on file for the transaction or subscription. If your app identifies customers a different way (e.g. an internal user id), keep a mapping from your own id to the email you pass into this component.
|
|
438
|
+
|
|
439
|
+
## Testing
|
|
440
|
+
|
|
441
|
+
```bash
|
|
442
|
+
npm run test
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
Component logic is tested with [`convex-test`](https://www.npmjs.com/package/convex-test) in `src/component/lib.test.ts`. Import `convex-paystack/test` in your own app to register this component's schema against your test instance.
|
|
446
|
+
|
|
447
|
+
## Example App
|
|
448
|
+
|
|
449
|
+
`example/` is a full Vite + React demo, styled with Paystack's and Convex's own brand colors, that exercises the entire component end to end against your own Paystack test-mode account:
|
|
450
|
+
|
|
451
|
+
- **One-time payment** — pick an amount and currency, choose which checkout channels to offer, pay, and land on a result screen driven by `verifyTransaction()`.
|
|
452
|
+
- **Subscriptions** — the app bootstraps two demo plans via `createPlan()` / `listPlans()` on first load and lets you subscribe to either.
|
|
453
|
+
- **Retry flow** — a declined or abandoned payment surfaces a "Try again" action that returns you to the same flow with your details preserved.
|
|
454
|
+
- **Transaction history** — a live Convex query over `listTransactions()` / `listSubscriptions()` that updates the instant a webhook lands, no refresh needed.
|
|
455
|
+
- **Live developer console** — a pinned panel (bottom of the page) that interleaves client-side actions (checkout started, verifying…) with the real webhook log from `listRecentEvents()`, reactively, so you can watch the entire lifecycle of a payment or subscription as it happens. Click any webhook row to see its raw payload.
|
|
456
|
+
|
|
457
|
+
Run it with:
|
|
458
|
+
|
|
459
|
+
```bash
|
|
460
|
+
cd example
|
|
461
|
+
npm install
|
|
462
|
+
npx convex dev
|
|
463
|
+
# in another terminal
|
|
464
|
+
npm run dev
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
Use Paystack's [test cards](https://paystack.com/docs/payments/test-payments/) to exercise both outcomes — `4084 0840 8408 4081` always succeeds, `4084 0800 0000 5408` always declines so you can see the retry flow.
|
|
468
|
+
|
|
469
|
+
## Limitations
|
|
470
|
+
|
|
471
|
+
- Amounts are in Paystack's subunit for the currency (kobo, pesewas, cents) — this component does not convert them. The example app converts major-unit input (e.g. Naira) to subunits before calling `initializeTransaction`.
|
|
472
|
+
- Only the webhook events listed above update local state; other events are received but not persisted beyond the raw idempotency record.
|
|
473
|
+
- `cancelSubscription` / `enableSubscription` require the subscription's `code` and `token`, both only available after a `subscription.create` webhook has been received.
|
|
474
|
+
- `createPlan` / `listPlans` talk to Paystack directly on every call — this component does not cache plans locally.
|
|
475
|
+
|
|
476
|
+
## Troubleshooting
|
|
477
|
+
|
|
478
|
+
**Webhook returns 401** — the `x-paystack-signature` header didn't match. Confirm `PAYSTACK_SECRET_KEY` is your live/test secret key (not the public key) and matches the key used in the Paystack Dashboard for this webhook endpoint.
|
|
479
|
+
|
|
480
|
+
**Transaction stays `pending`** — `initializeTransaction` only records `pending`; it becomes `success`/`failed` once the `charge.success` webhook arrives or you call `verifyTransaction`.
|
|
481
|
+
|
|
482
|
+
**Subscription never appears** — subscriptions are created on Paystack, not by this component. Confirm the `subscription.create` webhook is registered and reaching your endpoint, and that the checkout was initialized with a `plan` code.
|
|
483
|
+
|
|
484
|
+
## Contributing
|
|
485
|
+
|
|
486
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md).
|
|
487
|
+
|
|
488
|
+
## Changelog
|
|
489
|
+
|
|
490
|
+
See [CHANGELOG.md](./CHANGELOG.md).
|