paymongo-cli 1.2.0 → 1.4.2
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/.github/copilot-instructions.md +95 -0
- package/.github/workflows/ci-cd.yml +2 -46
- package/.github/workflows/ci.yml +1 -1
- package/.github/workflows/release.yml +16 -1
- package/AGENTS.md +418 -0
- package/CHANGELOG.md +331 -185
- package/README.md +94 -13
- package/TESTING.md +222 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +281 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +281 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov.info +5053 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/commands/config.d.ts +17 -0
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +268 -55
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/dev.d.ts +13 -1
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +40 -56
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/env.d.ts +4 -0
- package/dist/commands/env.d.ts.map +1 -0
- package/dist/commands/env.js +106 -0
- package/dist/commands/env.js.map +1 -0
- package/dist/commands/generate.js +1184 -0
- package/dist/commands/init.d.ts +11 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +33 -33
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/login.d.ts +17 -1
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +2 -17
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/payments.d.ts +37 -0
- package/dist/commands/payments.d.ts.map +1 -1
- package/dist/commands/payments.js +367 -32
- package/dist/commands/payments.js.map +1 -1
- package/dist/commands/team/index.d.ts.map +1 -1
- package/dist/commands/team/index.js +192 -95
- package/dist/commands/team/index.js.map +1 -1
- package/dist/commands/trigger.d.ts.map +1 -1
- package/dist/commands/trigger.js +239 -75
- package/dist/commands/trigger.js.map +1 -1
- package/dist/commands/webhooks.d.ts +19 -0
- package/dist/commands/webhooks.d.ts.map +1 -1
- package/dist/commands/webhooks.js +246 -70
- package/dist/commands/webhooks.js.map +1 -1
- package/dist/index.js +56 -32
- package/dist/index.js.map +1 -1
- package/dist/services/analytics/service.js +6 -8
- package/dist/services/api/client.d.ts +6 -8
- package/dist/services/api/client.d.ts.map +1 -1
- package/dist/services/api/client.js +20 -131
- package/dist/services/api/client.js.map +1 -1
- package/dist/services/api/rate-limiter.d.ts +64 -0
- package/dist/services/api/rate-limiter.d.ts.map +1 -0
- package/dist/services/api/rate-limiter.js +83 -0
- package/dist/services/api/rate-limiter.js.map +1 -0
- package/dist/services/api/undici-client.d.ts +39 -0
- package/dist/services/api/undici-client.d.ts.map +1 -0
- package/dist/services/api/undici-client.js +294 -0
- package/dist/services/api/undici-client.js.map +1 -0
- package/dist/services/config/manager.js +1 -16
- package/dist/services/dev/process-manager.js +0 -32
- package/dist/services/github/client.d.ts +41 -0
- package/dist/services/github/client.d.ts.map +1 -1
- package/dist/services/github/client.js +28 -0
- package/dist/services/github/client.js.map +1 -1
- package/dist/services/payments/simulator.d.ts +28 -0
- package/dist/services/payments/simulator.d.ts.map +1 -0
- package/dist/services/payments/simulator.js +115 -0
- package/dist/services/payments/simulator.js.map +1 -0
- package/dist/services/team/service.d.ts +44 -0
- package/dist/services/team/service.d.ts.map +1 -0
- package/dist/services/team/service.js +153 -0
- package/dist/services/team/service.js.map +1 -0
- package/dist/types/paymongo.d.ts +36 -3
- package/dist/types/paymongo.d.ts.map +1 -1
- package/dist/types/paymongo.js +0 -1
- package/dist/types/schemas.js +0 -8
- package/dist/utils/bulk.d.ts +62 -0
- package/dist/utils/bulk.d.ts.map +1 -0
- package/dist/utils/bulk.js +123 -0
- package/dist/utils/bulk.js.map +1 -0
- package/dist/utils/cache.js +4 -16
- package/dist/utils/constants.js +2 -13
- package/dist/utils/errors.d.ts.map +1 -1
- package/dist/utils/errors.js +22 -7
- package/dist/utils/errors.js.map +1 -1
- package/dist/utils/logger.d.ts +3 -1
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +38 -25
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/spinner.js +0 -1
- package/dist/utils/validator.js +0 -3
- package/dist/utils/webhook-store.d.ts +22 -0
- package/dist/utils/webhook-store.d.ts.map +1 -0
- package/dist/utils/webhook-store.js +57 -0
- package/dist/utils/webhook-store.js.map +1 -0
- package/package.json +75 -76
- package/jest.config.ts +0 -30
- package/web/index.html +0 -688
|
@@ -0,0 +1,1184 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { input } from '@inquirer/prompts';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import fs from 'fs/promises';
|
|
5
|
+
import ConfigManager from '../services/config/manager.js';
|
|
6
|
+
import Spinner from '../utils/spinner.js';
|
|
7
|
+
const command = new Command('generate');
|
|
8
|
+
command
|
|
9
|
+
.description('Generate boilerplate code for PayMongo integrations')
|
|
10
|
+
.addHelpText('after', `
|
|
11
|
+
EXAMPLES
|
|
12
|
+
$ paymongo generate webhook-handler --events payment.paid,payment.failed
|
|
13
|
+
$ paymongo generate webhook-handler --language typescript --framework express
|
|
14
|
+
$ paymongo generate payment-intent --methods card,gcash --language typescript
|
|
15
|
+
$ paymongo generate checkout-page --framework react --output Checkout.jsx
|
|
16
|
+
`)
|
|
17
|
+
.addCommand(new Command('webhook-handler')
|
|
18
|
+
.description('Generate a webhook handler for specific events')
|
|
19
|
+
.option('-e, --events <events>', 'Comma-separated list of events (e.g., payment.paid,payment.failed)')
|
|
20
|
+
.option('-l, --language <language>', 'Programming language (javascript, typescript)', 'javascript')
|
|
21
|
+
.option('-f, --framework <framework>', 'Framework (express, fastify, hapi)', 'express')
|
|
22
|
+
.option('-o, --output <file>', 'Output file path')
|
|
23
|
+
.addHelpText('after', `
|
|
24
|
+
SUPPORTED EVENTS:
|
|
25
|
+
payment.paid, payment.failed, payment.refunded, payment.expired
|
|
26
|
+
source.chargeable, source.failed, source.cancelled
|
|
27
|
+
checkout.session.succeeded, checkout.session.cancelled
|
|
28
|
+
|
|
29
|
+
EXAMPLES:
|
|
30
|
+
$ paymongo generate webhook-handler
|
|
31
|
+
$ paymongo generate webhook-handler --events payment.paid,payment.failed
|
|
32
|
+
$ paymongo generate webhook-handler --language typescript --framework fastify
|
|
33
|
+
$ paymongo generate webhook-handler --output my-webhook.js
|
|
34
|
+
`)
|
|
35
|
+
.action(async (options) => {
|
|
36
|
+
await generateWebhookHandler(options);
|
|
37
|
+
}))
|
|
38
|
+
.addCommand(new Command('payment-intent')
|
|
39
|
+
.description('Generate payment intent creation code')
|
|
40
|
+
.option('-l, --language <language>', 'Programming language (javascript, typescript)', 'javascript')
|
|
41
|
+
.option('-m, --methods <methods>', 'Payment methods (card,gcash,paymaya,grab_pay,qrph)')
|
|
42
|
+
.option('-o, --output <file>', 'Output file path')
|
|
43
|
+
.addHelpText('after', `
|
|
44
|
+
PAYMENT METHODS:
|
|
45
|
+
card, gcash, paymaya, grab_pay, qrph
|
|
46
|
+
|
|
47
|
+
EXAMPLES:
|
|
48
|
+
$ paymongo generate payment-intent
|
|
49
|
+
$ paymongo generate payment-intent --methods card,gcash
|
|
50
|
+
$ paymongo generate payment-intent --language typescript --output create-payment.js
|
|
51
|
+
`)
|
|
52
|
+
.action(async (options) => {
|
|
53
|
+
await generatePaymentIntent(options);
|
|
54
|
+
}))
|
|
55
|
+
.addCommand(new Command('checkout-page')
|
|
56
|
+
.description('Generate a basic checkout page with PayMongo integration')
|
|
57
|
+
.option('-l, --language <language>', 'Frontend language/framework (html, react, vue)', 'html')
|
|
58
|
+
.option('-o, --output <file>', 'Output file path')
|
|
59
|
+
.addHelpText('after', `
|
|
60
|
+
FRAMEWORKS:
|
|
61
|
+
html (vanilla HTML/JS), react, vue
|
|
62
|
+
|
|
63
|
+
EXAMPLES:
|
|
64
|
+
$ paymongo generate checkout-page
|
|
65
|
+
$ paymongo generate checkout-page --framework react
|
|
66
|
+
$ paymongo generate checkout-page --language vue --output Checkout.vue
|
|
67
|
+
`)
|
|
68
|
+
.action(async (options) => {
|
|
69
|
+
await generateCheckoutPage(options);
|
|
70
|
+
}));
|
|
71
|
+
async function generateWebhookHandler(options) {
|
|
72
|
+
const spinner = new Spinner();
|
|
73
|
+
const configManager = new ConfigManager();
|
|
74
|
+
try {
|
|
75
|
+
spinner.start('Loading configuration...');
|
|
76
|
+
const config = await configManager.load();
|
|
77
|
+
if (!config) {
|
|
78
|
+
spinner.fail('No configuration found');
|
|
79
|
+
console.log(chalk.yellow('No PayMongo configuration found.'));
|
|
80
|
+
console.log(chalk.gray("Run 'paymongo init' to set up your project first."));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
spinner.succeed('Configuration loaded');
|
|
84
|
+
let events = [];
|
|
85
|
+
if (options.events) {
|
|
86
|
+
events = options.events.split(',').map(e => e.trim());
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
const eventInput = await input({
|
|
90
|
+
message: 'Enter webhook events (comma-separated):',
|
|
91
|
+
default: 'payment.paid,payment.failed',
|
|
92
|
+
});
|
|
93
|
+
events = eventInput.split(',').map(e => e.trim());
|
|
94
|
+
}
|
|
95
|
+
const validEvents = [
|
|
96
|
+
'payment.paid', 'payment.failed', 'payment.refunded', 'payment.expired',
|
|
97
|
+
'source.chargeable', 'source.failed', 'source.cancelled',
|
|
98
|
+
'checkout.session.succeeded', 'checkout.session.cancelled'
|
|
99
|
+
];
|
|
100
|
+
const invalidEvents = events.filter(e => !validEvents.includes(e));
|
|
101
|
+
if (invalidEvents.length > 0) {
|
|
102
|
+
console.log(chalk.yellow(`Warning: Unknown events: ${invalidEvents.join(', ')}`));
|
|
103
|
+
console.log(chalk.gray(`Valid events: ${validEvents.join(', ')}`));
|
|
104
|
+
}
|
|
105
|
+
let code;
|
|
106
|
+
if (options.language === 'typescript') {
|
|
107
|
+
code = generateTypeScriptWebhookHandler(events, options.framework);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
code = generateJavaScriptWebhookHandler(events, options.framework);
|
|
111
|
+
}
|
|
112
|
+
let outputFile = options.output;
|
|
113
|
+
if (!outputFile) {
|
|
114
|
+
const firstEvent = events[0] || 'webhook';
|
|
115
|
+
const defaultName = `webhook-handler-${firstEvent.replace('.', '-')}.${options.language === 'typescript' ? 'ts' : 'js'}`;
|
|
116
|
+
outputFile = await input({
|
|
117
|
+
message: 'Output file path:',
|
|
118
|
+
default: defaultName,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
spinner.start(`Generating webhook handler...`);
|
|
122
|
+
await fs.writeFile(outputFile, code, 'utf-8');
|
|
123
|
+
spinner.succeed(`Webhook handler generated: ${outputFile}`);
|
|
124
|
+
console.log('\n' + chalk.green('✅ Webhook handler generated successfully!'));
|
|
125
|
+
console.log(chalk.gray(`Events handled: ${events.join(', ')}`));
|
|
126
|
+
console.log(chalk.gray(`Language: ${options.language}`));
|
|
127
|
+
console.log(chalk.gray(`Framework: ${options.framework}`));
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
spinner.fail('Generation failed');
|
|
131
|
+
console.error(chalk.red('Error:'), error instanceof Error ? error.message : String(error));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async function generatePaymentIntent(options) {
|
|
135
|
+
const spinner = new Spinner();
|
|
136
|
+
try {
|
|
137
|
+
let methods = ['card', 'gcash', 'paymaya'];
|
|
138
|
+
if (options.methods) {
|
|
139
|
+
methods = options.methods.split(',').map(m => m.trim());
|
|
140
|
+
}
|
|
141
|
+
let code;
|
|
142
|
+
if (options.language === 'typescript') {
|
|
143
|
+
code = generateTypeScriptPaymentIntent(methods);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
code = generateJavaScriptPaymentIntent(methods);
|
|
147
|
+
}
|
|
148
|
+
let outputFile = options.output;
|
|
149
|
+
if (!outputFile) {
|
|
150
|
+
const defaultName = `create-payment-intent.${options.language === 'typescript' ? 'ts' : 'js'}`;
|
|
151
|
+
outputFile = await input({
|
|
152
|
+
message: 'Output file path:',
|
|
153
|
+
default: defaultName,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
spinner.start(`Generating payment intent code...`);
|
|
157
|
+
await fs.writeFile(outputFile, code, 'utf-8');
|
|
158
|
+
spinner.succeed(`Payment intent code generated: ${outputFile}`);
|
|
159
|
+
console.log('\n' + chalk.green('✅ Payment intent code generated successfully!'));
|
|
160
|
+
console.log(chalk.gray(`Payment methods: ${methods.join(', ')}`));
|
|
161
|
+
console.log(chalk.gray(`Language: ${options.language}`));
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
spinner.fail('Generation failed');
|
|
165
|
+
console.error(chalk.red('Error:'), error instanceof Error ? error.message : String(error));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async function generateCheckoutPage(options) {
|
|
169
|
+
const spinner = new Spinner();
|
|
170
|
+
try {
|
|
171
|
+
let code;
|
|
172
|
+
let fileExtension;
|
|
173
|
+
switch (options.language) {
|
|
174
|
+
case 'react':
|
|
175
|
+
code = generateReactCheckoutPage();
|
|
176
|
+
fileExtension = 'jsx';
|
|
177
|
+
break;
|
|
178
|
+
case 'vue':
|
|
179
|
+
code = generateVueCheckoutPage();
|
|
180
|
+
fileExtension = 'vue';
|
|
181
|
+
break;
|
|
182
|
+
default:
|
|
183
|
+
code = generateHTMLCheckoutPage();
|
|
184
|
+
fileExtension = 'html';
|
|
185
|
+
}
|
|
186
|
+
let outputFile = options.output;
|
|
187
|
+
if (!outputFile) {
|
|
188
|
+
const defaultName = `checkout.${fileExtension}`;
|
|
189
|
+
outputFile = await input({
|
|
190
|
+
message: 'Output file path:',
|
|
191
|
+
default: defaultName,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
spinner.start(`Generating checkout page...`);
|
|
195
|
+
await fs.writeFile(outputFile, code, 'utf-8');
|
|
196
|
+
spinner.succeed(`Checkout page generated: ${outputFile}`);
|
|
197
|
+
console.log('\n' + chalk.green('✅ Checkout page generated successfully!'));
|
|
198
|
+
console.log(chalk.gray(`Framework: ${options.language}`));
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
spinner.fail('Generation failed');
|
|
202
|
+
console.error(chalk.red('Error:'), error instanceof Error ? error.message : String(error));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function generateJavaScriptWebhookHandler(events, framework) {
|
|
206
|
+
const eventHandlers = events.map(event => `
|
|
207
|
+
case '${event}':
|
|
208
|
+
console.log('Processing ${event} event:', data);
|
|
209
|
+
// Add your ${event} handling logic here
|
|
210
|
+
break;`).join('');
|
|
211
|
+
switch (framework) {
|
|
212
|
+
case 'express':
|
|
213
|
+
return `const express = require('express');
|
|
214
|
+
const crypto = require('crypto');
|
|
215
|
+
|
|
216
|
+
const app = express();
|
|
217
|
+
app.use(express.json());
|
|
218
|
+
|
|
219
|
+
// Webhook secret from PayMongo dashboard
|
|
220
|
+
const WEBHOOK_SECRET = process.env.PAYMONGO_WEBHOOK_SECRET;
|
|
221
|
+
|
|
222
|
+
function verifySignature(payload, signature, secret) {
|
|
223
|
+
const expectedSignature = crypto
|
|
224
|
+
.createHmac('sha256', secret)
|
|
225
|
+
.update(payload, 'utf8')
|
|
226
|
+
.digest('hex');
|
|
227
|
+
|
|
228
|
+
return signature === \`sha256=\${expectedSignature}\`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
app.post('/webhooks/paymongo', (req, res) => {
|
|
232
|
+
try {
|
|
233
|
+
const signature = req.headers['paymongo-signature'];
|
|
234
|
+
const payload = JSON.stringify(req.body);
|
|
235
|
+
|
|
236
|
+
// Verify webhook signature (optional but recommended)
|
|
237
|
+
if (WEBHOOK_SECRET && !verifySignature(payload, signature, WEBHOOK_SECRET)) {
|
|
238
|
+
console.log('Invalid signature');
|
|
239
|
+
return res.status(400).json({ error: 'Invalid signature' });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const { data } = req.body;
|
|
243
|
+
const eventType = data.attributes.type;
|
|
244
|
+
|
|
245
|
+
switch (eventType) {${eventHandlers}
|
|
246
|
+
default:
|
|
247
|
+
console.log('Unhandled event type:', eventType);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
res.json({ received: true });
|
|
251
|
+
} catch (error) {
|
|
252
|
+
console.error('Webhook processing error:', error);
|
|
253
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const PORT = process.env.PORT || 3000;
|
|
258
|
+
app.listen(PORT, () => {
|
|
259
|
+
console.log(\`Webhook server running on port \${PORT}\`);
|
|
260
|
+
});`;
|
|
261
|
+
case 'fastify':
|
|
262
|
+
return `const fastify = require('fastify')({ logger: true });
|
|
263
|
+
const crypto = require('crypto');
|
|
264
|
+
|
|
265
|
+
// Webhook secret from PayMongo dashboard
|
|
266
|
+
const WEBHOOK_SECRET = process.env.PAYMONGO_WEBHOOK_SECRET;
|
|
267
|
+
|
|
268
|
+
function verifySignature(payload, signature, secret) {
|
|
269
|
+
const expectedSignature = crypto
|
|
270
|
+
.createHmac('sha256', secret)
|
|
271
|
+
.update(payload, 'utf8')
|
|
272
|
+
.digest('hex');
|
|
273
|
+
|
|
274
|
+
return signature === \`sha256=\${expectedSignature}\`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
fastify.post('/webhooks/paymongo', async (request, reply) => {
|
|
278
|
+
try {
|
|
279
|
+
const signature = request.headers['paymongo-signature'];
|
|
280
|
+
const payload = JSON.stringify(request.body);
|
|
281
|
+
|
|
282
|
+
// Verify webhook signature (optional but recommended)
|
|
283
|
+
if (WEBHOOK_SECRET && !verifySignature(payload, signature, WEBHOOK_SECRET)) {
|
|
284
|
+
console.log('Invalid signature');
|
|
285
|
+
return reply.code(400).send({ error: 'Invalid signature' });
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const { data } = request.body;
|
|
289
|
+
const eventType = data.attributes.type;
|
|
290
|
+
|
|
291
|
+
switch (eventType) {${eventHandlers}
|
|
292
|
+
default:
|
|
293
|
+
console.log('Unhandled event type:', eventType);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return { received: true };
|
|
297
|
+
} catch (error) {
|
|
298
|
+
console.error('Webhook processing error:', error);
|
|
299
|
+
return reply.code(500).send({ error: 'Internal server error' });
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const start = async () => {
|
|
304
|
+
try {
|
|
305
|
+
await fastify.listen({ port: process.env.PORT || 3000 });
|
|
306
|
+
} catch (err) {
|
|
307
|
+
fastify.log.error(err);
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
start();`;
|
|
313
|
+
default:
|
|
314
|
+
return `// Simple webhook handler for ${events.join(', ')}
|
|
315
|
+
|
|
316
|
+
const crypto = require('crypto');
|
|
317
|
+
|
|
318
|
+
// Webhook secret from PayMongo dashboard
|
|
319
|
+
const WEBHOOK_SECRET = process.env.PAYMONGO_WEBHOOK_SECRET;
|
|
320
|
+
|
|
321
|
+
function verifySignature(payload, signature, secret) {
|
|
322
|
+
const expectedSignature = crypto
|
|
323
|
+
.createHmac('sha256', secret)
|
|
324
|
+
.update(payload, 'utf8')
|
|
325
|
+
.digest('hex');
|
|
326
|
+
|
|
327
|
+
return signature === \`sha256=\${expectedSignature}\`;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function handleWebhook(request, response) {
|
|
331
|
+
try {
|
|
332
|
+
const signature = request.headers['paymongo-signature'];
|
|
333
|
+
const payload = JSON.stringify(request.body);
|
|
334
|
+
|
|
335
|
+
// Verify webhook signature (optional but recommended)
|
|
336
|
+
if (WEBHOOK_SECRET && !verifySignature(payload, signature, WEBHOOK_SECRET)) {
|
|
337
|
+
console.log('Invalid signature');
|
|
338
|
+
response.writeHead(400, { 'Content-Type': 'application/json' });
|
|
339
|
+
response.end(JSON.stringify({ error: 'Invalid signature' }));
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const { data } = request.body;
|
|
344
|
+
const eventType = data.attributes.type;
|
|
345
|
+
|
|
346
|
+
switch (eventType) {${eventHandlers}
|
|
347
|
+
default:
|
|
348
|
+
console.log('Unhandled event type:', eventType);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
response.writeHead(200, { 'Content-Type': 'application/json' });
|
|
352
|
+
response.end(JSON.stringify({ received: true }));
|
|
353
|
+
} catch (error) {
|
|
354
|
+
console.error('Webhook processing error:', error);
|
|
355
|
+
response.writeHead(500, { 'Content-Type': 'application/json' });
|
|
356
|
+
response.end(JSON.stringify({ error: 'Internal server error' }));
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
module.exports = { handleWebhook };`;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function generateTypeScriptWebhookHandler(events, framework) {
|
|
364
|
+
const eventHandlers = events.map(event => `
|
|
365
|
+
case '${event}':
|
|
366
|
+
console.log('Processing ${event} event:', data);
|
|
367
|
+
// Add your ${event} handling logic here
|
|
368
|
+
break;`).join('');
|
|
369
|
+
switch (framework) {
|
|
370
|
+
case 'express':
|
|
371
|
+
return `import express, { Request, Response } from 'express';
|
|
372
|
+
import crypto from 'crypto';
|
|
373
|
+
|
|
374
|
+
const app = express();
|
|
375
|
+
app.use(express.json());
|
|
376
|
+
|
|
377
|
+
// Webhook secret from PayMongo dashboard
|
|
378
|
+
const WEBHOOK_SECRET = process.env.PAYMONGO_WEBHOOK_SECRET;
|
|
379
|
+
|
|
380
|
+
interface PayMongoWebhookPayload {
|
|
381
|
+
data: {
|
|
382
|
+
id: string;
|
|
383
|
+
type: string;
|
|
384
|
+
attributes: {
|
|
385
|
+
type: string;
|
|
386
|
+
livemode: boolean;
|
|
387
|
+
created_at: number;
|
|
388
|
+
updated_at: number;
|
|
389
|
+
data: any;
|
|
390
|
+
};
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function verifySignature(payload: string, signature: string, secret: string): boolean {
|
|
395
|
+
const expectedSignature = crypto
|
|
396
|
+
.createHmac('sha256', secret)
|
|
397
|
+
.update(payload, 'utf8')
|
|
398
|
+
.digest('hex');
|
|
399
|
+
|
|
400
|
+
return signature === \`sha256=\${expectedSignature}\`;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
app.post('/webhooks/paymongo', (req: Request, res: Response) => {
|
|
404
|
+
try {
|
|
405
|
+
const signature = req.headers['paymongo-signature'] as string;
|
|
406
|
+
const payload = JSON.stringify(req.body);
|
|
407
|
+
|
|
408
|
+
// Verify webhook signature (optional but recommended)
|
|
409
|
+
if (WEBHOOK_SECRET && !verifySignature(payload, signature, WEBHOOK_SECRET)) {
|
|
410
|
+
console.log('Invalid signature');
|
|
411
|
+
return res.status(400).json({ error: 'Invalid signature' });
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const { data }: PayMongoWebhookPayload = req.body;
|
|
415
|
+
const eventType = data.attributes.type;
|
|
416
|
+
|
|
417
|
+
switch (eventType) {${eventHandlers}
|
|
418
|
+
default:
|
|
419
|
+
console.log('Unhandled event type:', eventType);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
res.json({ received: true });
|
|
423
|
+
} catch (error) {
|
|
424
|
+
console.error('Webhook processing error:', error);
|
|
425
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
const PORT = process.env.PORT || 3000;
|
|
430
|
+
app.listen(PORT, () => {
|
|
431
|
+
console.log(\`Webhook server running on port \${PORT}\`);
|
|
432
|
+
});`;
|
|
433
|
+
default:
|
|
434
|
+
return `// TypeScript webhook handler for ${events.join(', ')}
|
|
435
|
+
|
|
436
|
+
import crypto from 'crypto';
|
|
437
|
+
|
|
438
|
+
const WEBHOOK_SECRET = process.env.PAYMONGO_WEBHOOK_SECRET;
|
|
439
|
+
|
|
440
|
+
interface PayMongoWebhookPayload {
|
|
441
|
+
data: {
|
|
442
|
+
id: string;
|
|
443
|
+
type: string;
|
|
444
|
+
attributes: {
|
|
445
|
+
type: string;
|
|
446
|
+
livemode: boolean;
|
|
447
|
+
created_at: number;
|
|
448
|
+
updated_at: number;
|
|
449
|
+
data: any;
|
|
450
|
+
};
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function verifySignature(payload: string, signature: string, secret: string): boolean {
|
|
455
|
+
const expectedSignature = crypto
|
|
456
|
+
.createHmac('sha256', secret)
|
|
457
|
+
.update(payload, 'utf8')
|
|
458
|
+
.digest('hex');
|
|
459
|
+
|
|
460
|
+
return signature === \`sha256=\${expectedSignature}\`;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export function handleWebhook(body: PayMongoWebhookPayload, signature?: string): { received: boolean } {
|
|
464
|
+
try {
|
|
465
|
+
const payload = JSON.stringify(body);
|
|
466
|
+
|
|
467
|
+
// Verify webhook signature (optional but recommended)
|
|
468
|
+
if (WEBHOOK_SECRET && signature && !verifySignature(payload, signature, WEBHOOK_SECRET)) {
|
|
469
|
+
console.log('Invalid signature');
|
|
470
|
+
throw new Error('Invalid signature');
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const { data } = body;
|
|
474
|
+
const eventType = data.attributes.type;
|
|
475
|
+
|
|
476
|
+
switch (eventType) {${eventHandlers}
|
|
477
|
+
default:
|
|
478
|
+
console.log('Unhandled event type:', eventType);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return { received: true };
|
|
482
|
+
} catch (error) {
|
|
483
|
+
console.error('Webhook processing error:', error);
|
|
484
|
+
throw error;
|
|
485
|
+
}
|
|
486
|
+
}`;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
function generateJavaScriptPaymentIntent(methods) {
|
|
490
|
+
return `const axios = require('axios');
|
|
491
|
+
|
|
492
|
+
// PayMongo API credentials
|
|
493
|
+
const PAYMONGO_SECRET_KEY = process.env.PAYMONGO_SECRET_KEY;
|
|
494
|
+
const PAYMONGO_PUBLIC_KEY = process.env.PAYMONGO_PUBLIC_KEY;
|
|
495
|
+
|
|
496
|
+
async function createPaymentIntent(amount, currency = 'PHP', description = '') {
|
|
497
|
+
try {
|
|
498
|
+
const response = await axios.post(
|
|
499
|
+
'https://api.paymongo.com/v1/payment_intents',
|
|
500
|
+
{
|
|
501
|
+
data: {
|
|
502
|
+
attributes: {
|
|
503
|
+
amount: amount, // Amount in centavos (e.g., 10000 = ₱100.00)
|
|
504
|
+
currency: currency,
|
|
505
|
+
description: description,
|
|
506
|
+
payment_method_allowed: ${JSON.stringify(methods)},
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
headers: {
|
|
512
|
+
'Authorization': \`Basic \${Buffer.from(PAYMONGO_SECRET_KEY + ':').toString('base64')}\`,
|
|
513
|
+
'Content-Type': 'application/json',
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
const paymentIntent = response.data.data;
|
|
519
|
+
|
|
520
|
+
console.log('Payment Intent created:', paymentIntent.id);
|
|
521
|
+
console.log('Client Key:', paymentIntent.attributes.client_key);
|
|
522
|
+
console.log('Amount:', (paymentIntent.attributes.amount / 100).toFixed(2), paymentIntent.attributes.currency);
|
|
523
|
+
|
|
524
|
+
return {
|
|
525
|
+
id: paymentIntent.id,
|
|
526
|
+
clientKey: paymentIntent.attributes.client_key,
|
|
527
|
+
amount: paymentIntent.attributes.amount,
|
|
528
|
+
currency: paymentIntent.attributes.currency,
|
|
529
|
+
status: paymentIntent.attributes.status
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
} catch (error) {
|
|
533
|
+
console.error('Error creating payment intent:', error.response?.data || error.message);
|
|
534
|
+
throw error;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Example usage
|
|
539
|
+
async function example() {
|
|
540
|
+
try {
|
|
541
|
+
const paymentIntent = await createPaymentIntent(
|
|
542
|
+
10000, // ₱100.00
|
|
543
|
+
'PHP',
|
|
544
|
+
'Sample payment'
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
console.log('Use this client key in your frontend:', paymentIntent.clientKey);
|
|
548
|
+
|
|
549
|
+
} catch (error) {
|
|
550
|
+
console.error('Failed to create payment intent');
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
module.exports = { createPaymentIntent };
|
|
555
|
+
|
|
556
|
+
if (require.main === module) {
|
|
557
|
+
example();
|
|
558
|
+
}`;
|
|
559
|
+
}
|
|
560
|
+
function generateTypeScriptPaymentIntent(methods) {
|
|
561
|
+
return `import axios from 'axios';
|
|
562
|
+
|
|
563
|
+
interface PaymentIntent {
|
|
564
|
+
id: string;
|
|
565
|
+
clientKey: string;
|
|
566
|
+
amount: number;
|
|
567
|
+
currency: string;
|
|
568
|
+
status: string;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
interface PayMongoPaymentIntentResponse {
|
|
572
|
+
data: {
|
|
573
|
+
id: string;
|
|
574
|
+
attributes: {
|
|
575
|
+
amount: number;
|
|
576
|
+
currency: string;
|
|
577
|
+
description: string;
|
|
578
|
+
status: string;
|
|
579
|
+
client_key: string;
|
|
580
|
+
payment_method_allowed: string[];
|
|
581
|
+
};
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// PayMongo API credentials
|
|
586
|
+
const PAYMONGO_SECRET_KEY = process.env.PAYMONGO_SECRET_KEY!;
|
|
587
|
+
const PAYMONGO_PUBLIC_KEY = process.env.PAYMONGO_PUBLIC_KEY!;
|
|
588
|
+
|
|
589
|
+
export async function createPaymentIntent(
|
|
590
|
+
amount: number,
|
|
591
|
+
currency: string = 'PHP',
|
|
592
|
+
description: string = ''
|
|
593
|
+
): Promise<PaymentIntent> {
|
|
594
|
+
try {
|
|
595
|
+
const response = await axios.post<PayMongoPaymentIntentResponse>(
|
|
596
|
+
'https://api.paymongo.com/v1/payment_intents',
|
|
597
|
+
{
|
|
598
|
+
data: {
|
|
599
|
+
attributes: {
|
|
600
|
+
amount: amount, // Amount in centavos (e.g., 10000 = ₱100.00)
|
|
601
|
+
currency: currency,
|
|
602
|
+
description: description,
|
|
603
|
+
payment_method_allowed: ${JSON.stringify(methods)},
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
headers: {
|
|
609
|
+
'Authorization': \`Basic \${Buffer.from(PAYMONGO_SECRET_KEY + ':').toString('base64')}\`,
|
|
610
|
+
'Content-Type': 'application/json',
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
const paymentIntent = response.data.data;
|
|
616
|
+
|
|
617
|
+
console.log('Payment Intent created:', paymentIntent.id);
|
|
618
|
+
console.log('Client Key:', paymentIntent.attributes.client_key);
|
|
619
|
+
console.log('Amount:', (paymentIntent.attributes.amount / 100).toFixed(2), paymentIntent.attributes.currency);
|
|
620
|
+
|
|
621
|
+
return {
|
|
622
|
+
id: paymentIntent.id,
|
|
623
|
+
clientKey: paymentIntent.attributes.client_key,
|
|
624
|
+
amount: paymentIntent.attributes.amount,
|
|
625
|
+
currency: paymentIntent.attributes.currency,
|
|
626
|
+
status: paymentIntent.attributes.status
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
} catch (error: any) {
|
|
630
|
+
console.error('Error creating payment intent:', error.response?.data || error.message);
|
|
631
|
+
throw error;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Example usage
|
|
636
|
+
async function example(): Promise<void> {
|
|
637
|
+
try {
|
|
638
|
+
const paymentIntent = await createPaymentIntent(
|
|
639
|
+
10000, // ₱100.00
|
|
640
|
+
'PHP',
|
|
641
|
+
'Sample payment'
|
|
642
|
+
);
|
|
643
|
+
|
|
644
|
+
console.log('Use this client key in your frontend:', paymentIntent.clientKey);
|
|
645
|
+
|
|
646
|
+
} catch (error) {
|
|
647
|
+
console.error('Failed to create payment intent');
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (require.main === module) {
|
|
652
|
+
example();
|
|
653
|
+
}`;
|
|
654
|
+
}
|
|
655
|
+
function generateHTMLCheckoutPage() {
|
|
656
|
+
return `<!DOCTYPE html>
|
|
657
|
+
<html lang="en">
|
|
658
|
+
<head>
|
|
659
|
+
<meta charset="UTF-8">
|
|
660
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
661
|
+
<title>PayMongo Checkout</title>
|
|
662
|
+
<script src="https://js.paymongo.com/v1/paymongo.js"></script>
|
|
663
|
+
<style>
|
|
664
|
+
body {
|
|
665
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
666
|
+
max-width: 400px;
|
|
667
|
+
margin: 50px auto;
|
|
668
|
+
padding: 20px;
|
|
669
|
+
}
|
|
670
|
+
.checkout-form {
|
|
671
|
+
background: #f9f9f9;
|
|
672
|
+
padding: 20px;
|
|
673
|
+
border-radius: 8px;
|
|
674
|
+
}
|
|
675
|
+
.form-group {
|
|
676
|
+
margin-bottom: 15px;
|
|
677
|
+
}
|
|
678
|
+
label {
|
|
679
|
+
display: block;
|
|
680
|
+
margin-bottom: 5px;
|
|
681
|
+
font-weight: 500;
|
|
682
|
+
}
|
|
683
|
+
input, select {
|
|
684
|
+
width: 100%;
|
|
685
|
+
padding: 10px;
|
|
686
|
+
border: 1px solid #ddd;
|
|
687
|
+
border-radius: 4px;
|
|
688
|
+
font-size: 16px;
|
|
689
|
+
}
|
|
690
|
+
button {
|
|
691
|
+
width: 100%;
|
|
692
|
+
padding: 12px;
|
|
693
|
+
background: #007bff;
|
|
694
|
+
color: white;
|
|
695
|
+
border: none;
|
|
696
|
+
border-radius: 4px;
|
|
697
|
+
font-size: 16px;
|
|
698
|
+
cursor: pointer;
|
|
699
|
+
}
|
|
700
|
+
button:hover {
|
|
701
|
+
background: #0056b3;
|
|
702
|
+
}
|
|
703
|
+
button:disabled {
|
|
704
|
+
background: #ccc;
|
|
705
|
+
cursor: not-allowed;
|
|
706
|
+
}
|
|
707
|
+
</style>
|
|
708
|
+
</head>
|
|
709
|
+
<body>
|
|
710
|
+
<div class="checkout-form">
|
|
711
|
+
<h2>Complete Your Payment</h2>
|
|
712
|
+
<form id="payment-form">
|
|
713
|
+
<div class="form-group">
|
|
714
|
+
<label for="email">Email</label>
|
|
715
|
+
<input type="email" id="email" required>
|
|
716
|
+
</div>
|
|
717
|
+
|
|
718
|
+
<div class="form-group">
|
|
719
|
+
<label for="card-number">Card Number</label>
|
|
720
|
+
<input type="text" id="card-number" placeholder="1234 5678 9012 3456" required>
|
|
721
|
+
</div>
|
|
722
|
+
|
|
723
|
+
<div class="form-group">
|
|
724
|
+
<label for="expiry">Expiry Date</label>
|
|
725
|
+
<input type="text" id="expiry" placeholder="MM/YY" required>
|
|
726
|
+
</div>
|
|
727
|
+
|
|
728
|
+
<div class="form-group">
|
|
729
|
+
<label for="cvc">CVC</label>
|
|
730
|
+
<input type="text" id="cvc" placeholder="123" required>
|
|
731
|
+
</div>
|
|
732
|
+
|
|
733
|
+
<button type="submit" id="pay-button">Pay ₱100.00</button>
|
|
734
|
+
</form>
|
|
735
|
+
</div>
|
|
736
|
+
|
|
737
|
+
<script>
|
|
738
|
+
// Replace with your actual client key from the payment intent
|
|
739
|
+
const clientKey = 'YOUR_CLIENT_KEY_HERE';
|
|
740
|
+
|
|
741
|
+
const paymongo = new Paymongo(clientKey);
|
|
742
|
+
|
|
743
|
+
document.getElementById('payment-form').addEventListener('submit', async (e) => {
|
|
744
|
+
e.preventDefault();
|
|
745
|
+
|
|
746
|
+
const payButton = document.getElementById('pay-button');
|
|
747
|
+
payButton.disabled = true;
|
|
748
|
+
payButton.textContent = 'Processing...';
|
|
749
|
+
|
|
750
|
+
try {
|
|
751
|
+
// Create payment method
|
|
752
|
+
const paymentMethod = await paymongo.createPaymentMethod({
|
|
753
|
+
type: 'card',
|
|
754
|
+
details: {
|
|
755
|
+
card_number: document.getElementById('card-number').value.replace(/\\s/g, ''),
|
|
756
|
+
exp_month: document.getElementById('expiry').value.split('/')[0],
|
|
757
|
+
exp_year: '20' + document.getElementById('expiry').value.split('/')[1],
|
|
758
|
+
cvc: document.getElementById('cvc').value,
|
|
759
|
+
},
|
|
760
|
+
billing: {
|
|
761
|
+
email: document.getElementById('email').value,
|
|
762
|
+
},
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
// Attach payment method to payment intent
|
|
766
|
+
const result = await paymongo.attachPaymentIntent('YOUR_PAYMENT_INTENT_ID', {
|
|
767
|
+
payment_method: paymentMethod.id,
|
|
768
|
+
return_url: window.location.origin + '/success',
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
if (result.next_action) {
|
|
772
|
+
// Handle 3D Secure or other next actions
|
|
773
|
+
window.location.href = result.next_action.redirect.url;
|
|
774
|
+
} else {
|
|
775
|
+
// Payment succeeded
|
|
776
|
+
window.location.href = '/success';
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
} catch (error) {
|
|
780
|
+
console.error('Payment failed:', error);
|
|
781
|
+
alert('Payment failed. Please try again.');
|
|
782
|
+
payButton.disabled = false;
|
|
783
|
+
payButton.textContent = 'Pay ₱100.00';
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
</script>
|
|
787
|
+
</body>
|
|
788
|
+
</html>`;
|
|
789
|
+
}
|
|
790
|
+
function generateReactCheckoutPage() {
|
|
791
|
+
return `import React, { useState } from 'react';
|
|
792
|
+
|
|
793
|
+
interface CheckoutFormProps {
|
|
794
|
+
clientKey: string;
|
|
795
|
+
paymentIntentId: string;
|
|
796
|
+
amount: number;
|
|
797
|
+
onSuccess: (result: any) => void;
|
|
798
|
+
onError: (error: any) => void;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const CheckoutForm: React.FC<CheckoutFormProps> = ({
|
|
802
|
+
clientKey,
|
|
803
|
+
paymentIntentId,
|
|
804
|
+
amount,
|
|
805
|
+
onSuccess,
|
|
806
|
+
onError
|
|
807
|
+
}) => {
|
|
808
|
+
const [loading, setLoading] = useState(false);
|
|
809
|
+
const [formData, setFormData] = useState({
|
|
810
|
+
email: '',
|
|
811
|
+
cardNumber: '',
|
|
812
|
+
expiry: '',
|
|
813
|
+
cvc: ''
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
817
|
+
const { name, value } = e.target;
|
|
818
|
+
setFormData(prev => ({
|
|
819
|
+
...prev,
|
|
820
|
+
[name]: value
|
|
821
|
+
}));
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
825
|
+
e.preventDefault();
|
|
826
|
+
setLoading(true);
|
|
827
|
+
|
|
828
|
+
try {
|
|
829
|
+
// Load PayMongo script dynamically if not already loaded
|
|
830
|
+
if (!window.Paymongo) {
|
|
831
|
+
await new Promise((resolve, reject) => {
|
|
832
|
+
const script = document.createElement('script');
|
|
833
|
+
script.src = 'https://js.paymongo.com/v1/paymongo.js';
|
|
834
|
+
script.onload = resolve;
|
|
835
|
+
script.onerror = reject;
|
|
836
|
+
document.head.appendChild(script);
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const paymongo = new (window as any).Paymongo(clientKey);
|
|
841
|
+
|
|
842
|
+
// Create payment method
|
|
843
|
+
const paymentMethod = await paymongo.createPaymentMethod({
|
|
844
|
+
type: 'card',
|
|
845
|
+
details: {
|
|
846
|
+
card_number: formData.cardNumber.replace(/\\s/g, ''),
|
|
847
|
+
exp_month: parseInt(formData.expiry.split('/')[0]),
|
|
848
|
+
exp_year: 2000 + parseInt(formData.expiry.split('/')[1]),
|
|
849
|
+
cvc: formData.cvc,
|
|
850
|
+
},
|
|
851
|
+
billing: {
|
|
852
|
+
email: formData.email,
|
|
853
|
+
},
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
// Attach payment method to payment intent
|
|
857
|
+
const result = await paymongo.attachPaymentIntent(paymentIntentId, {
|
|
858
|
+
payment_method: paymentMethod.id,
|
|
859
|
+
return_url: window.location.origin + '/success',
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
onSuccess(result);
|
|
863
|
+
|
|
864
|
+
} catch (error) {
|
|
865
|
+
onError(error);
|
|
866
|
+
} finally {
|
|
867
|
+
setLoading(false);
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
|
|
871
|
+
return (
|
|
872
|
+
<div style={{ maxWidth: '400px', margin: '50px auto', padding: '20px' }}>
|
|
873
|
+
<div style={{
|
|
874
|
+
background: '#f9f9f9',
|
|
875
|
+
padding: '20px',
|
|
876
|
+
borderRadius: '8px'
|
|
877
|
+
}}>
|
|
878
|
+
<h2>Complete Your Payment</h2>
|
|
879
|
+
<form onSubmit={handleSubmit}>
|
|
880
|
+
<div style={{ marginBottom: '15px' }}>
|
|
881
|
+
<label style={{ display: 'block', marginBottom: '5px', fontWeight: '500' }}>
|
|
882
|
+
Email
|
|
883
|
+
</label>
|
|
884
|
+
<input
|
|
885
|
+
type="email"
|
|
886
|
+
name="email"
|
|
887
|
+
value={formData.email}
|
|
888
|
+
onChange={handleInputChange}
|
|
889
|
+
required
|
|
890
|
+
style={{
|
|
891
|
+
width: '100%',
|
|
892
|
+
padding: '10px',
|
|
893
|
+
border: '1px solid #ddd',
|
|
894
|
+
borderRadius: '4px',
|
|
895
|
+
fontSize: '16px'
|
|
896
|
+
}}
|
|
897
|
+
/>
|
|
898
|
+
</div>
|
|
899
|
+
|
|
900
|
+
<div style={{ marginBottom: '15px' }}>
|
|
901
|
+
<label style={{ display: 'block', marginBottom: '5px', fontWeight: '500' }}>
|
|
902
|
+
Card Number
|
|
903
|
+
</label>
|
|
904
|
+
<input
|
|
905
|
+
type="text"
|
|
906
|
+
name="cardNumber"
|
|
907
|
+
value={formData.cardNumber}
|
|
908
|
+
onChange={handleInputChange}
|
|
909
|
+
placeholder="1234 5678 9012 3456"
|
|
910
|
+
required
|
|
911
|
+
style={{
|
|
912
|
+
width: '100%',
|
|
913
|
+
padding: '10px',
|
|
914
|
+
border: '1px solid #ddd',
|
|
915
|
+
borderRadius: '4px',
|
|
916
|
+
fontSize: '16px'
|
|
917
|
+
}}
|
|
918
|
+
/>
|
|
919
|
+
</div>
|
|
920
|
+
|
|
921
|
+
<div style={{ marginBottom: '15px' }}>
|
|
922
|
+
<label style={{ display: 'block', marginBottom: '5px', fontWeight: '500' }}>
|
|
923
|
+
Expiry Date
|
|
924
|
+
</label>
|
|
925
|
+
<input
|
|
926
|
+
type="text"
|
|
927
|
+
name="expiry"
|
|
928
|
+
value={formData.expiry}
|
|
929
|
+
onChange={handleInputChange}
|
|
930
|
+
placeholder="MM/YY"
|
|
931
|
+
required
|
|
932
|
+
style={{
|
|
933
|
+
width: '100%',
|
|
934
|
+
padding: '10px',
|
|
935
|
+
border: '1px solid #ddd',
|
|
936
|
+
borderRadius: '4px',
|
|
937
|
+
fontSize: '16px'
|
|
938
|
+
}}
|
|
939
|
+
/>
|
|
940
|
+
</div>
|
|
941
|
+
|
|
942
|
+
<div style={{ marginBottom: '15px' }}>
|
|
943
|
+
<label style={{ display: 'block', marginBottom: '5px', fontWeight: '500' }}>
|
|
944
|
+
CVC
|
|
945
|
+
</label>
|
|
946
|
+
<input
|
|
947
|
+
type="text"
|
|
948
|
+
name="cvc"
|
|
949
|
+
value={formData.cvc}
|
|
950
|
+
onChange={handleInputChange}
|
|
951
|
+
placeholder="123"
|
|
952
|
+
required
|
|
953
|
+
style={{
|
|
954
|
+
width: '100%',
|
|
955
|
+
padding: '10px',
|
|
956
|
+
border: '1px solid #ddd',
|
|
957
|
+
borderRadius: '4px',
|
|
958
|
+
fontSize: '16px'
|
|
959
|
+
}}
|
|
960
|
+
/>
|
|
961
|
+
</div>
|
|
962
|
+
|
|
963
|
+
<button
|
|
964
|
+
type="submit"
|
|
965
|
+
disabled={loading}
|
|
966
|
+
style={{
|
|
967
|
+
width: '100%',
|
|
968
|
+
padding: '12px',
|
|
969
|
+
background: loading ? '#ccc' : '#007bff',
|
|
970
|
+
color: 'white',
|
|
971
|
+
border: 'none',
|
|
972
|
+
borderRadius: '4px',
|
|
973
|
+
fontSize: '16px',
|
|
974
|
+
cursor: loading ? 'not-allowed' : 'pointer'
|
|
975
|
+
}}
|
|
976
|
+
>
|
|
977
|
+
{loading ? 'Processing...' : \`Pay ₱\${(amount / 100).toFixed(2)}\`}
|
|
978
|
+
</button>
|
|
979
|
+
</form>
|
|
980
|
+
</div>
|
|
981
|
+
</div>
|
|
982
|
+
);
|
|
983
|
+
};
|
|
984
|
+
|
|
985
|
+
export default CheckoutForm;`;
|
|
986
|
+
}
|
|
987
|
+
function generateVueCheckoutPage() {
|
|
988
|
+
return `<template>
|
|
989
|
+
<div class="checkout-container">
|
|
990
|
+
<div class="checkout-form">
|
|
991
|
+
<h2>Complete Your Payment</h2>
|
|
992
|
+
<form @submit.prevent="handleSubmit">
|
|
993
|
+
<div class="form-group">
|
|
994
|
+
<label for="email">Email</label>
|
|
995
|
+
<input
|
|
996
|
+
v-model="formData.email"
|
|
997
|
+
type="email"
|
|
998
|
+
id="email"
|
|
999
|
+
required
|
|
1000
|
+
>
|
|
1001
|
+
</div>
|
|
1002
|
+
|
|
1003
|
+
<div class="form-group">
|
|
1004
|
+
<label for="cardNumber">Card Number</label>
|
|
1005
|
+
<input
|
|
1006
|
+
v-model="formData.cardNumber"
|
|
1007
|
+
type="text"
|
|
1008
|
+
id="cardNumber"
|
|
1009
|
+
placeholder="1234 5678 9012 3456"
|
|
1010
|
+
required
|
|
1011
|
+
>
|
|
1012
|
+
</div>
|
|
1013
|
+
|
|
1014
|
+
<div class="form-group">
|
|
1015
|
+
<label for="expiry">Expiry Date</label>
|
|
1016
|
+
<input
|
|
1017
|
+
v-model="formData.expiry"
|
|
1018
|
+
type="text"
|
|
1019
|
+
id="expiry"
|
|
1020
|
+
placeholder="MM/YY"
|
|
1021
|
+
required
|
|
1022
|
+
>
|
|
1023
|
+
</div>
|
|
1024
|
+
|
|
1025
|
+
<div class="form-group">
|
|
1026
|
+
<label for="cvc">CVC</label>
|
|
1027
|
+
<input
|
|
1028
|
+
v-model="formData.cvc"
|
|
1029
|
+
type="text"
|
|
1030
|
+
id="cvc"
|
|
1031
|
+
placeholder="123"
|
|
1032
|
+
required
|
|
1033
|
+
>
|
|
1034
|
+
</div>
|
|
1035
|
+
|
|
1036
|
+
<button
|
|
1037
|
+
type="submit"
|
|
1038
|
+
:disabled="loading"
|
|
1039
|
+
class="pay-button"
|
|
1040
|
+
>
|
|
1041
|
+
{{ loading ? 'Processing...' : \`Pay ₱\${(amount / 100).toFixed(2)}\` }}
|
|
1042
|
+
</button>
|
|
1043
|
+
</form>
|
|
1044
|
+
</div>
|
|
1045
|
+
</div>
|
|
1046
|
+
</template>
|
|
1047
|
+
|
|
1048
|
+
<script>
|
|
1049
|
+
export default {
|
|
1050
|
+
name: 'CheckoutForm',
|
|
1051
|
+
props: {
|
|
1052
|
+
clientKey: {
|
|
1053
|
+
type: String,
|
|
1054
|
+
required: true
|
|
1055
|
+
},
|
|
1056
|
+
paymentIntentId: {
|
|
1057
|
+
type: String,
|
|
1058
|
+
required: true
|
|
1059
|
+
},
|
|
1060
|
+
amount: {
|
|
1061
|
+
type: Number,
|
|
1062
|
+
required: true
|
|
1063
|
+
}
|
|
1064
|
+
},
|
|
1065
|
+
data() {
|
|
1066
|
+
return {
|
|
1067
|
+
loading: false,
|
|
1068
|
+
formData: {
|
|
1069
|
+
email: '',
|
|
1070
|
+
cardNumber: '',
|
|
1071
|
+
expiry: '',
|
|
1072
|
+
cvc: ''
|
|
1073
|
+
}
|
|
1074
|
+
};
|
|
1075
|
+
},
|
|
1076
|
+
methods: {
|
|
1077
|
+
async handleSubmit() {
|
|
1078
|
+
this.loading = true;
|
|
1079
|
+
|
|
1080
|
+
try {
|
|
1081
|
+
// Load PayMongo script if not loaded
|
|
1082
|
+
if (!window.Paymongo) {
|
|
1083
|
+
await this.loadPayMongoScript();
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
const paymongo = new window.Paymongo(this.clientKey);
|
|
1087
|
+
|
|
1088
|
+
// Create payment method
|
|
1089
|
+
const paymentMethod = await paymongo.createPaymentMethod({
|
|
1090
|
+
type: 'card',
|
|
1091
|
+
details: {
|
|
1092
|
+
card_number: this.formData.cardNumber.replace(/\\s/g, ''),
|
|
1093
|
+
exp_month: parseInt(this.formData.expiry.split('/')[0]),
|
|
1094
|
+
exp_year: 2000 + parseInt(this.formData.expiry.split('/')[1]),
|
|
1095
|
+
cvc: this.formData.cvc,
|
|
1096
|
+
},
|
|
1097
|
+
billing: {
|
|
1098
|
+
email: this.formData.email,
|
|
1099
|
+
},
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
// Attach payment method to payment intent
|
|
1103
|
+
const result = await paymongo.attachPaymentIntent(this.paymentIntentId, {
|
|
1104
|
+
payment_method: paymentMethod.id,
|
|
1105
|
+
return_url: window.location.origin + '/success',
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
this.$emit('success', result);
|
|
1109
|
+
|
|
1110
|
+
} catch (error) {
|
|
1111
|
+
console.error('Payment failed:', error);
|
|
1112
|
+
this.$emit('error', error);
|
|
1113
|
+
} finally {
|
|
1114
|
+
this.loading = false;
|
|
1115
|
+
}
|
|
1116
|
+
},
|
|
1117
|
+
|
|
1118
|
+
loadPayMongoScript() {
|
|
1119
|
+
return new Promise((resolve, reject) => {
|
|
1120
|
+
const script = document.createElement('script');
|
|
1121
|
+
script.src = 'https://js.paymongo.com/v1/paymongo.js';
|
|
1122
|
+
script.onload = resolve;
|
|
1123
|
+
script.onerror = reject;
|
|
1124
|
+
document.head.appendChild(script);
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
};
|
|
1129
|
+
</script>
|
|
1130
|
+
|
|
1131
|
+
<style scoped>
|
|
1132
|
+
.checkout-container {
|
|
1133
|
+
max-width: 400px;
|
|
1134
|
+
margin: 50px auto;
|
|
1135
|
+
padding: 20px;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
.checkout-form {
|
|
1139
|
+
background: #f9f9f9;
|
|
1140
|
+
padding: 20px;
|
|
1141
|
+
border-radius: 8px;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
.form-group {
|
|
1145
|
+
margin-bottom: 15px;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
label {
|
|
1149
|
+
display: block;
|
|
1150
|
+
margin-bottom: 5px;
|
|
1151
|
+
font-weight: 500;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
input {
|
|
1155
|
+
width: 100%;
|
|
1156
|
+
padding: 10px;
|
|
1157
|
+
border: 1px solid #ddd;
|
|
1158
|
+
border-radius: 4px;
|
|
1159
|
+
font-size: 16px;
|
|
1160
|
+
box-sizing: border-box;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
.pay-button {
|
|
1164
|
+
width: 100%;
|
|
1165
|
+
padding: 12px;
|
|
1166
|
+
background: #007bff;
|
|
1167
|
+
color: white;
|
|
1168
|
+
border: none;
|
|
1169
|
+
border-radius: 4px;
|
|
1170
|
+
font-size: 16px;
|
|
1171
|
+
cursor: pointer;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
.pay-button:hover:not(:disabled) {
|
|
1175
|
+
background: #0056b3;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
.pay-button:disabled {
|
|
1179
|
+
background: #ccc;
|
|
1180
|
+
cursor: not-allowed;
|
|
1181
|
+
}
|
|
1182
|
+
</style>`;
|
|
1183
|
+
}
|
|
1184
|
+
export default command;
|