@hackthedev/dsync-pay 1.0.6 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +127 -77
- package/index.mjs +52 -30
- package/package.json +1 -1
- package/web/payment-status.html +434 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# dSyncPay
|
|
2
2
|
|
|
3
|
-
As another part of the dSync library family this library is responsible for payment handling currently supporting PayPal and Coinbase Crypto payments.
|
|
3
|
+
As another part of the dSync library family this library is responsible for payment handling currently supporting PayPal and Coinbase Crypto payments. It works independently and without any percentage cuts by using your own API keys.
|
|
4
4
|
|
|
5
5
|
> [!NOTE]
|
|
6
6
|
>
|
|
@@ -31,11 +31,11 @@ const payments = new dSyncPay({
|
|
|
31
31
|
sandbox: true // or false for production
|
|
32
32
|
},
|
|
33
33
|
coinbase: {
|
|
34
|
-
apiKey: 'xxx',
|
|
35
|
-
webhookSecret: 'xxx'
|
|
34
|
+
apiKey: 'xxx', // coinbase commerce API key
|
|
35
|
+
webhookSecret: 'xxx' // optional, for webhook verification
|
|
36
36
|
},
|
|
37
|
-
|
|
38
|
-
// events
|
|
37
|
+
|
|
38
|
+
// events
|
|
39
39
|
onPaymentCreated: (data) => {},
|
|
40
40
|
onPaymentCompleted: (data) => {},
|
|
41
41
|
onPaymentFailed: (data) => {},
|
|
@@ -47,6 +47,10 @@ const payments = new dSyncPay({
|
|
|
47
47
|
});
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
+
### Coinbase API Key
|
|
51
|
+
|
|
52
|
+
dSyncPay uses **Coinbase Commerce** for crypto payments - not the Coinbase exchange or developer platform. Get your API key at `https://commerce.coinbase.com/settings/security`.
|
|
53
|
+
|
|
50
54
|
------
|
|
51
55
|
|
|
52
56
|
## PayPal Usage
|
|
@@ -54,60 +58,81 @@ const payments = new dSyncPay({
|
|
|
54
58
|
### Create an order
|
|
55
59
|
|
|
56
60
|
```js
|
|
57
|
-
// returnUrl and cancelUrl are auto-generated based on domain + basePath
|
|
58
|
-
const payment = await payments.paypal.createOrder({
|
|
59
|
-
title: 'product name',
|
|
60
|
-
price: 19.99
|
|
61
|
-
// returnUrl automatically becomes https://domain.com/payments/paypal/verify
|
|
62
|
-
// cancelUrl will become https://domain.com/payments/cancel
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
// or override manually
|
|
66
61
|
const payment = await payments.paypal.createOrder({
|
|
67
62
|
title: 'product name',
|
|
68
63
|
price: 19.99,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
64
|
+
// optional params:
|
|
65
|
+
description: 'product description', // default: 'no description'
|
|
66
|
+
quantity: 1, // default: 1
|
|
67
|
+
currency: 'EUR', // default: 'EUR'
|
|
68
|
+
customId: 'your-custom-id', // default: auto-generated 17-digit id
|
|
69
|
+
metadata: { userId: '123' }, // passed through to onPaymentCompleted
|
|
70
|
+
returnUrl: 'https://custom.com/ok', // default: https://domain.com/payments/paypal/verify
|
|
71
|
+
cancelUrl: 'https://custom.com/no' // default: https://domain.com/payments/cancel
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
-
//
|
|
74
|
+
// redirect user to:
|
|
75
|
+
payment.approvalUrl
|
|
76
|
+
|
|
77
|
+
// result object:
|
|
78
|
+
{
|
|
79
|
+
provider: 'paypal',
|
|
80
|
+
type: 'order',
|
|
81
|
+
approvalUrl: '...',
|
|
82
|
+
transactionId: 'customId',
|
|
83
|
+
orderId: '...',
|
|
84
|
+
amount: 19.99,
|
|
85
|
+
currency: 'EUR',
|
|
86
|
+
metadata: {},
|
|
87
|
+
rawResponse: {}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
> [!NOTE]
|
|
92
|
+
>
|
|
93
|
+
> metadata is cached in memory for 1 hour and passed through to the payment callbacks automatically.
|
|
94
|
+
|
|
95
|
+
### Verify an order manually
|
|
96
|
+
|
|
97
|
+
```js
|
|
75
98
|
const result = await payments.paypal.verifyOrder(orderId);
|
|
99
|
+
// result.status === 'COMPLETED'
|
|
76
100
|
```
|
|
77
101
|
|
|
78
102
|
### Managing subscriptions
|
|
79
103
|
|
|
80
104
|
```js
|
|
81
|
-
//
|
|
105
|
+
// step 1: create a plan (one time setup)
|
|
82
106
|
const plan = await payments.paypal.createPlan({
|
|
83
107
|
name: 'monthly premium',
|
|
84
108
|
price: 9.99,
|
|
85
|
-
interval: 'MONTH'
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
//
|
|
90
|
-
const sub = await payments.paypal.createSubscription({
|
|
91
|
-
planId: 'P-xxxxx'
|
|
92
|
-
// returnUrl becomes https://domain.com/payments/paypal/subscription/verify
|
|
93
|
-
// cancelUrl becomes https://domain.com/payments/cancel
|
|
109
|
+
interval: 'MONTH', // MONTH, YEAR, WEEK, DAY
|
|
110
|
+
// optional:
|
|
111
|
+
description: '...',
|
|
112
|
+
currency: 'EUR', // default: 'EUR'
|
|
113
|
+
frequency: 1 // default: 1
|
|
94
114
|
});
|
|
115
|
+
// save plan.planId for later use
|
|
95
116
|
|
|
96
|
-
//
|
|
117
|
+
// step 2: create a subscription
|
|
97
118
|
const sub = await payments.paypal.createSubscription({
|
|
98
119
|
planId: 'P-xxxxx',
|
|
99
|
-
|
|
100
|
-
|
|
120
|
+
// optional:
|
|
121
|
+
customId: 'your-custom-id',
|
|
122
|
+
metadata: { userId: '123' },
|
|
123
|
+
returnUrl: 'https://custom.com/success', // default: https://domain.com/payments/paypal/subscription/verify
|
|
124
|
+
cancelUrl: 'https://custom.com/cancel' // default: https://domain.com/payments/cancel
|
|
101
125
|
});
|
|
102
126
|
|
|
103
|
-
// redirect to
|
|
104
|
-
|
|
127
|
+
// redirect user to:
|
|
128
|
+
sub.approvalUrl
|
|
129
|
+
// also available: sub.subscriptionId
|
|
105
130
|
|
|
106
|
-
//
|
|
131
|
+
// step 3: verify subscription manually
|
|
107
132
|
const result = await payments.paypal.verifySubscription(subscriptionId);
|
|
108
133
|
// result.status === 'ACTIVE'
|
|
109
134
|
|
|
110
|
-
// cancel subscription
|
|
135
|
+
// cancel a subscription
|
|
111
136
|
await payments.paypal.cancelSubscription(subscriptionId, 'reason');
|
|
112
137
|
```
|
|
113
138
|
|
|
@@ -118,26 +143,38 @@ await payments.paypal.cancelSubscription(subscriptionId, 'reason');
|
|
|
118
143
|
### Creating a charge
|
|
119
144
|
|
|
120
145
|
```js
|
|
121
|
-
// redirectUrl and cancelUrl are auto-generated
|
|
122
|
-
const charge = await payments.coinbase.createCharge({
|
|
123
|
-
title: 'product name',
|
|
124
|
-
price: 19.99
|
|
125
|
-
// redirectUrl becomes https://domain.com/payments/coinbase/verify
|
|
126
|
-
// cancelUrl becomes https://domain.com/payments/cancel
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
// or override manually
|
|
130
146
|
const charge = await payments.coinbase.createCharge({
|
|
131
147
|
title: 'product name',
|
|
132
148
|
price: 19.99,
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
149
|
+
// optional:
|
|
150
|
+
description: 'product description', // default: 'no description'
|
|
151
|
+
quantity: 1, // default: 1
|
|
152
|
+
currency: 'EUR', // default: 'EUR'
|
|
153
|
+
metadata: { userId: '123' },
|
|
154
|
+
redirectUrl: 'https://custom.com/ok', // default: https://domain.com/payments/coinbase/verify
|
|
155
|
+
cancelUrl: 'https://custom.com/no' // default: https://domain.com/payments/cancel
|
|
136
156
|
});
|
|
137
157
|
|
|
138
|
-
// redirect to:
|
|
158
|
+
// redirect user to:
|
|
159
|
+
charge.hostedUrl
|
|
160
|
+
|
|
161
|
+
// result object:
|
|
162
|
+
{
|
|
163
|
+
provider: 'coinbase',
|
|
164
|
+
type: 'charge',
|
|
165
|
+
hostedUrl: '...',
|
|
166
|
+
chargeId: '...',
|
|
167
|
+
chargeCode: '...',
|
|
168
|
+
amount: 19.99,
|
|
169
|
+
currency: 'EUR',
|
|
170
|
+
metadata: {},
|
|
171
|
+
rawResponse: {}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
139
174
|
|
|
140
|
-
|
|
175
|
+
### Verify a charge manually
|
|
176
|
+
|
|
177
|
+
```js
|
|
141
178
|
const result = await payments.coinbase.verifyCharge(chargeCode);
|
|
142
179
|
// result.status === 'COMPLETED'
|
|
143
180
|
```
|
|
@@ -146,7 +183,7 @@ const result = await payments.coinbase.verifyCharge(chargeCode);
|
|
|
146
183
|
|
|
147
184
|
## Routes
|
|
148
185
|
|
|
149
|
-
dSyncPay automatically
|
|
186
|
+
dSyncPay automatically registers verification routes that handle payment returns from PayPal and Coinbase.
|
|
150
187
|
|
|
151
188
|
### Verification Routes
|
|
152
189
|
|
|
@@ -159,42 +196,38 @@ dSyncPay automatically creates verification routes and redirect pages for handli
|
|
|
159
196
|
#### Coinbase
|
|
160
197
|
|
|
161
198
|
- `GET /payments/coinbase/verify?code=xxx`
|
|
162
|
-
- `POST /payments/webhook/coinbase` (if webhookSecret set)
|
|
199
|
+
- `POST /payments/webhook/coinbase` (only registered if `webhookSecret` is set)
|
|
163
200
|
- `GET /payments/cancel`
|
|
164
201
|
|
|
165
|
-
###
|
|
202
|
+
### Status Page
|
|
166
203
|
|
|
167
|
-
|
|
204
|
+
After a payment is completed, failed, or cancelled, the user is redirected to a built-in status page at:
|
|
168
205
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
- `GET /subscription-success` - shown after successful subscription
|
|
173
|
-
- `GET /subscription-error` - shown when subscription fails
|
|
206
|
+
```
|
|
207
|
+
GET /payments/payment-status.html?status=success&provider=paypal&amount=19.99¤cy=EUR
|
|
208
|
+
```
|
|
174
209
|
|
|
175
|
-
|
|
210
|
+
The `status` query param controls what is shown:
|
|
176
211
|
|
|
177
|
-
|
|
212
|
+
| status | description |
|
|
213
|
+
| ----------- | --------------------------------- |
|
|
214
|
+
| `success` | payment or subscription completed |
|
|
215
|
+
| `error` | payment failed |
|
|
216
|
+
| `cancelled` | user cancelled |
|
|
178
217
|
|
|
179
|
-
|
|
180
|
-
const order = await payments.paypal.createOrder({
|
|
181
|
-
title: 'premium plan',
|
|
182
|
-
price: 19.99
|
|
183
|
-
});
|
|
218
|
+
Additional query params (`payment_id`, `provider`, `amount`, `currency`, `type`) are passed through automatically and displayed on the page.
|
|
184
219
|
|
|
185
|
-
|
|
186
|
-
// paypal redirects back to /payments/paypal/verify?token=XXX
|
|
187
|
-
// route automatically verifies and triggers onPaymentCompleted
|
|
188
|
-
// then redirects to /payment-success
|
|
189
|
-
```
|
|
220
|
+
You can customize where users land before the status page using the `redirects` option in the constructor. These are intermediate routes that pass through query params and redirect to the status page.
|
|
190
221
|
|
|
191
|
-
|
|
222
|
+
------
|
|
223
|
+
|
|
224
|
+
## Custom Configuration
|
|
192
225
|
|
|
193
|
-
```
|
|
226
|
+
```js
|
|
194
227
|
const payments = new dSyncPay({
|
|
195
228
|
app,
|
|
196
229
|
domain: 'https://domain.com',
|
|
197
|
-
basePath: '/api/pay',
|
|
230
|
+
basePath: '/api/pay', // default: '/payments'
|
|
198
231
|
redirects: {
|
|
199
232
|
success: '/custom/success',
|
|
200
233
|
error: '/custom/error',
|
|
@@ -205,7 +238,24 @@ const payments = new dSyncPay({
|
|
|
205
238
|
paypal: { ... }
|
|
206
239
|
});
|
|
207
240
|
|
|
208
|
-
// routes: /api/pay/paypal/verify, /api/pay/coinbase/verify, etc.
|
|
209
|
-
// auto urls: https://domain.com/api/pay/paypal/verify
|
|
210
|
-
//
|
|
211
|
-
```
|
|
241
|
+
// verification routes: /api/pay/paypal/verify, /api/pay/coinbase/verify, etc.
|
|
242
|
+
// auto-generated return urls: https://domain.com/api/pay/paypal/verify
|
|
243
|
+
// status page: /api/pay/payment-status.html
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
------
|
|
247
|
+
|
|
248
|
+
## Events
|
|
249
|
+
|
|
250
|
+
All callbacks receive a data object with at minimum `provider`, `type`, `status`, `metadata`, and `rawResponse`.
|
|
251
|
+
|
|
252
|
+
| event | trigger |
|
|
253
|
+
| ------------------------- | ------------------------------------- |
|
|
254
|
+
| `onPaymentCreated` | order or charge was created |
|
|
255
|
+
| `onPaymentCompleted` | payment verified as completed |
|
|
256
|
+
| `onPaymentFailed` | payment failed or expired |
|
|
257
|
+
| `onPaymentCancelled` | user cancelled payment |
|
|
258
|
+
| `onSubscriptionCreated` | subscription was created |
|
|
259
|
+
| `onSubscriptionActivated` | subscription verified as active |
|
|
260
|
+
| `onSubscriptionCancelled` | subscription was cancelled |
|
|
261
|
+
| `onError` | internal error (auth, api call, etc.) |
|
package/index.mjs
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import crypto from "crypto";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
2
8
|
|
|
3
9
|
export default class dSyncPay {
|
|
4
10
|
constructor({
|
|
@@ -43,6 +49,9 @@ export default class dSyncPay {
|
|
|
43
49
|
onError
|
|
44
50
|
};
|
|
45
51
|
|
|
52
|
+
this.webPath = path.join(__dirname, "web");
|
|
53
|
+
if(!fs.existsSync(this.webPath)) throw new Error("missing web path");
|
|
54
|
+
|
|
46
55
|
if (paypal) {
|
|
47
56
|
if (!paypal.clientId) throw new Error("missing paypal.clientId");
|
|
48
57
|
if (!paypal.clientSecret) throw new Error("missing paypal.clientSecret");
|
|
@@ -54,6 +63,11 @@ export default class dSyncPay {
|
|
|
54
63
|
this.coinbase = new this.Coinbase(this, coinbase);
|
|
55
64
|
}
|
|
56
65
|
|
|
66
|
+
// serve static files from web/ folder
|
|
67
|
+
this.app.get(`${this.basePath}/payment-status.html`, (req, res) => {
|
|
68
|
+
res.sendFile(path.join(__dirname, "web", "payment-status.html"));
|
|
69
|
+
});
|
|
70
|
+
|
|
57
71
|
this.registerRoutes(basePath);
|
|
58
72
|
this.registerRedirectRoutes();
|
|
59
73
|
}
|
|
@@ -102,7 +116,7 @@ export default class dSyncPay {
|
|
|
102
116
|
|
|
103
117
|
const fetchOptions = {
|
|
104
118
|
method,
|
|
105
|
-
headers: {...headers}
|
|
119
|
+
headers: { ...headers }
|
|
106
120
|
};
|
|
107
121
|
|
|
108
122
|
if (auth) {
|
|
@@ -252,10 +266,8 @@ export default class dSyncPay {
|
|
|
252
266
|
rawResponse: response
|
|
253
267
|
};
|
|
254
268
|
|
|
255
|
-
// save metadata in cache
|
|
256
269
|
this.parent.metadataCache.set(response.id, metadata);
|
|
257
270
|
|
|
258
|
-
// auto cleanup after 1 hour if never verified
|
|
259
271
|
setTimeout(() => {
|
|
260
272
|
this.parent.metadataCache.delete(response.id);
|
|
261
273
|
}, 60 * 60 * 1000);
|
|
@@ -315,7 +327,6 @@ export default class dSyncPay {
|
|
|
315
327
|
amount = parseFloat(purchaseUnit.amount.value);
|
|
316
328
|
}
|
|
317
329
|
|
|
318
|
-
// get metadata from cache
|
|
319
330
|
const metadata = this.parent.metadataCache.get(orderId) || {};
|
|
320
331
|
|
|
321
332
|
const result = {
|
|
@@ -338,7 +349,6 @@ export default class dSyncPay {
|
|
|
338
349
|
await this.parent.emit('onPaymentFailed', result);
|
|
339
350
|
}
|
|
340
351
|
|
|
341
|
-
// cleanup cache after verify
|
|
342
352
|
this.parent.metadataCache.delete(orderId);
|
|
343
353
|
|
|
344
354
|
return result;
|
|
@@ -350,7 +360,6 @@ export default class dSyncPay {
|
|
|
350
360
|
error: error.response || error.message
|
|
351
361
|
});
|
|
352
362
|
|
|
353
|
-
// cleanup cache after verify
|
|
354
363
|
this.parent.metadataCache.delete(orderId);
|
|
355
364
|
|
|
356
365
|
throw error;
|
|
@@ -501,10 +510,8 @@ export default class dSyncPay {
|
|
|
501
510
|
rawResponse: response
|
|
502
511
|
};
|
|
503
512
|
|
|
504
|
-
// save metadata in cache
|
|
505
513
|
this.parent.metadataCache.set(response.id, metadata);
|
|
506
514
|
|
|
507
|
-
// auto cleanup after 1 hour if never verified
|
|
508
515
|
setTimeout(() => {
|
|
509
516
|
this.parent.metadataCache.delete(response.id);
|
|
510
517
|
}, 60 * 60 * 1000);
|
|
@@ -534,7 +541,6 @@ export default class dSyncPay {
|
|
|
534
541
|
}
|
|
535
542
|
);
|
|
536
543
|
|
|
537
|
-
// get metadata from cache
|
|
538
544
|
const metadata = this.parent.metadataCache.get(subscriptionId) || {};
|
|
539
545
|
|
|
540
546
|
const result = {
|
|
@@ -554,7 +560,6 @@ export default class dSyncPay {
|
|
|
554
560
|
await this.parent.emit('onSubscriptionCancelled', result);
|
|
555
561
|
}
|
|
556
562
|
|
|
557
|
-
// cleanup cache after verify
|
|
558
563
|
this.parent.metadataCache.delete(subscriptionId);
|
|
559
564
|
|
|
560
565
|
return result;
|
|
@@ -566,7 +571,6 @@ export default class dSyncPay {
|
|
|
566
571
|
error: error.response || error.message
|
|
567
572
|
});
|
|
568
573
|
|
|
569
|
-
// cleanup cache after verify
|
|
570
574
|
this.parent.metadataCache.delete(subscriptionId);
|
|
571
575
|
|
|
572
576
|
throw error;
|
|
@@ -585,11 +589,10 @@ export default class dSyncPay {
|
|
|
585
589
|
"Content-Type": "application/json",
|
|
586
590
|
"Authorization": `Bearer ${accessToken}`
|
|
587
591
|
},
|
|
588
|
-
body: {reason}
|
|
592
|
+
body: { reason }
|
|
589
593
|
}
|
|
590
594
|
);
|
|
591
595
|
|
|
592
|
-
// get metadata from cache
|
|
593
596
|
const metadata = this.parent.metadataCache.get(subscriptionId) || {};
|
|
594
597
|
|
|
595
598
|
const result = {
|
|
@@ -601,7 +604,6 @@ export default class dSyncPay {
|
|
|
601
604
|
metadata
|
|
602
605
|
};
|
|
603
606
|
|
|
604
|
-
// cleanup cache
|
|
605
607
|
this.parent.metadataCache.delete(subscriptionId);
|
|
606
608
|
|
|
607
609
|
await this.parent.emit('onSubscriptionCancelled', result);
|
|
@@ -614,7 +616,6 @@ export default class dSyncPay {
|
|
|
614
616
|
error: error.response || error.message
|
|
615
617
|
});
|
|
616
618
|
|
|
617
|
-
// cleanup cache on error too
|
|
618
619
|
this.parent.metadataCache.delete(subscriptionId);
|
|
619
620
|
|
|
620
621
|
throw error;
|
|
@@ -755,23 +756,28 @@ export default class dSyncPay {
|
|
|
755
756
|
|
|
756
757
|
registerRedirectRoutes() {
|
|
757
758
|
this.app.get(this.redirects.success, (req, res) => {
|
|
758
|
-
|
|
759
|
+
const query = new URLSearchParams({ ...req.query, status: 'success' }).toString();
|
|
760
|
+
return res.redirect(`${this.basePath}/payment-status.html?${query}`);
|
|
759
761
|
});
|
|
760
762
|
|
|
761
763
|
this.app.get(this.redirects.error, (req, res) => {
|
|
762
|
-
|
|
764
|
+
const query = new URLSearchParams({ ...req.query, status: 'error' }).toString();
|
|
765
|
+
return res.redirect(`${this.basePath}/payment-status.html?${query}`);
|
|
763
766
|
});
|
|
764
767
|
|
|
765
768
|
this.app.get(this.redirects.cancelled, (req, res) => {
|
|
766
|
-
|
|
769
|
+
const query = new URLSearchParams({ ...req.query, status: 'cancelled' }).toString();
|
|
770
|
+
return res.redirect(`${this.basePath}/payment-status.html?${query}`);
|
|
767
771
|
});
|
|
768
772
|
|
|
769
773
|
this.app.get(this.redirects.subscriptionSuccess, (req, res) => {
|
|
770
|
-
|
|
774
|
+
const query = new URLSearchParams({ ...req.query, status: 'success', type: 'subscription' }).toString();
|
|
775
|
+
return res.redirect(`${this.basePath}/payment-status.html?${query}`);
|
|
771
776
|
});
|
|
772
777
|
|
|
773
778
|
this.app.get(this.redirects.subscriptionError, (req, res) => {
|
|
774
|
-
|
|
779
|
+
const query = new URLSearchParams({ ...req.query, status: 'error', type: 'subscription' }).toString();
|
|
780
|
+
return res.redirect(`${this.basePath}/payment-status.html?${query}`);
|
|
775
781
|
});
|
|
776
782
|
}
|
|
777
783
|
|
|
@@ -780,12 +786,18 @@ export default class dSyncPay {
|
|
|
780
786
|
this.app.get(`${basePath}/paypal/verify`, async (req, res) => {
|
|
781
787
|
try {
|
|
782
788
|
const orderId = req.query.token;
|
|
783
|
-
if (!orderId) return res.status(400).json({ok: false, error: 'missing_token'});
|
|
789
|
+
if (!orderId) return res.status(400).json({ ok: false, error: 'missing_token' });
|
|
784
790
|
|
|
785
791
|
const result = await this.paypal.verifyOrder(orderId);
|
|
786
792
|
|
|
787
793
|
if (result.status === 'COMPLETED') {
|
|
788
|
-
|
|
794
|
+
const query = new URLSearchParams({
|
|
795
|
+
payment_id: orderId,
|
|
796
|
+
provider: 'paypal',
|
|
797
|
+
amount: result.amount,
|
|
798
|
+
currency: result.currency
|
|
799
|
+
}).toString();
|
|
800
|
+
return res.redirect(`${this.redirects.success}?${query}`);
|
|
789
801
|
} else {
|
|
790
802
|
return res.redirect(this.redirects.error);
|
|
791
803
|
}
|
|
@@ -797,12 +809,16 @@ export default class dSyncPay {
|
|
|
797
809
|
this.app.get(`${basePath}/paypal/subscription/verify`, async (req, res) => {
|
|
798
810
|
try {
|
|
799
811
|
const subscriptionId = req.query.subscription_id;
|
|
800
|
-
if (!subscriptionId) return res.status(400).json({ok: false, error: 'missing_subscription_id'});
|
|
812
|
+
if (!subscriptionId) return res.status(400).json({ ok: false, error: 'missing_subscription_id' });
|
|
801
813
|
|
|
802
814
|
const result = await this.paypal.verifySubscription(subscriptionId);
|
|
803
815
|
|
|
804
816
|
if (result.status === 'ACTIVE') {
|
|
805
|
-
|
|
817
|
+
const query = new URLSearchParams({
|
|
818
|
+
payment_id: subscriptionId,
|
|
819
|
+
provider: 'paypal'
|
|
820
|
+
}).toString();
|
|
821
|
+
return res.redirect(`${this.redirects.subscriptionSuccess}?${query}`);
|
|
806
822
|
} else {
|
|
807
823
|
return res.redirect(this.redirects.subscriptionError);
|
|
808
824
|
}
|
|
@@ -826,12 +842,18 @@ export default class dSyncPay {
|
|
|
826
842
|
this.app.get(`${basePath}/coinbase/verify`, async (req, res) => {
|
|
827
843
|
try {
|
|
828
844
|
const chargeCode = req.query.code;
|
|
829
|
-
if (!chargeCode) return res.status(400).json({ok: false, error: 'missing_code'});
|
|
845
|
+
if (!chargeCode) return res.status(400).json({ ok: false, error: 'missing_code' });
|
|
830
846
|
|
|
831
847
|
const result = await this.coinbase.verifyCharge(chargeCode);
|
|
832
848
|
|
|
833
849
|
if (result.status === 'COMPLETED') {
|
|
834
|
-
|
|
850
|
+
const query = new URLSearchParams({
|
|
851
|
+
payment_id: result.chargeId,
|
|
852
|
+
provider: 'coinbase',
|
|
853
|
+
amount: result.amount,
|
|
854
|
+
currency: result.currency
|
|
855
|
+
}).toString();
|
|
856
|
+
return res.redirect(`${this.redirects.success}?${query}`);
|
|
835
857
|
} else {
|
|
836
858
|
return res.redirect(this.redirects.error);
|
|
837
859
|
}
|
|
@@ -850,7 +872,7 @@ export default class dSyncPay {
|
|
|
850
872
|
this.coinbase.config.webhookSecret
|
|
851
873
|
);
|
|
852
874
|
|
|
853
|
-
if (!isValid) return res.status(401).json({ok: false, error: 'invalid_signature'});
|
|
875
|
+
if (!isValid) return res.status(401).json({ ok: false, error: 'invalid_signature' });
|
|
854
876
|
|
|
855
877
|
const event = req.body;
|
|
856
878
|
|
|
@@ -859,9 +881,9 @@ export default class dSyncPay {
|
|
|
859
881
|
await this.coinbase.verifyCharge(chargeId);
|
|
860
882
|
}
|
|
861
883
|
|
|
862
|
-
res.status(200).json({ok: true});
|
|
884
|
+
res.status(200).json({ ok: true });
|
|
863
885
|
} catch (error) {
|
|
864
|
-
res.status(500).json({ok: false, error: 'webhook_error'});
|
|
886
|
+
res.status(500).json({ ok: false, error: 'webhook_error' });
|
|
865
887
|
}
|
|
866
888
|
});
|
|
867
889
|
}
|
|
@@ -869,4 +891,4 @@ export default class dSyncPay {
|
|
|
869
891
|
|
|
870
892
|
return this;
|
|
871
893
|
}
|
|
872
|
-
}
|
|
894
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hackthedev/dsync-pay",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "As another part of the dSync library family this library is responsible for payment handling currently supporting PayPal and Coinbase Crypto payments. Its works independently and without any percentage cuts by using your own API keys.",
|
|
5
5
|
"homepage": "https://github.com/NETWORK-Z-Dev/dSyncPay#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Payment Status</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500&family=IBM+Plex+Sans:wght@300;400;500&display=swap" rel="stylesheet">
|
|
10
|
+
<style>
|
|
11
|
+
*, *::before, *::after {
|
|
12
|
+
box-sizing: border-box;
|
|
13
|
+
margin: 0;
|
|
14
|
+
padding: 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
:root {
|
|
18
|
+
--bg: #0a0a0a;
|
|
19
|
+
--surface: #101010;
|
|
20
|
+
--border: #1c1c1c;
|
|
21
|
+
--border-bright: #252525;
|
|
22
|
+
--text: #d8d8d8;
|
|
23
|
+
--text-dim: #555;
|
|
24
|
+
--text-dimmer: #2e2e2e;
|
|
25
|
+
--accent: #00e676;
|
|
26
|
+
--accent-glow: rgba(0, 230, 118, 0.12);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
html, body { height: 100%; }
|
|
30
|
+
|
|
31
|
+
body {
|
|
32
|
+
background: var(--bg);
|
|
33
|
+
color: var(--text);
|
|
34
|
+
font-family: 'IBM Plex Sans', sans-serif;
|
|
35
|
+
display: flex;
|
|
36
|
+
flex-direction: column;
|
|
37
|
+
align-items: center;
|
|
38
|
+
justify-content: center;
|
|
39
|
+
min-height: 100vh;
|
|
40
|
+
overflow: hidden;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
body::before {
|
|
44
|
+
content: '';
|
|
45
|
+
position: fixed;
|
|
46
|
+
inset: 0;
|
|
47
|
+
background-image:
|
|
48
|
+
linear-gradient(var(--border) 1px, transparent 1px),
|
|
49
|
+
linear-gradient(90deg, var(--border) 1px, transparent 1px);
|
|
50
|
+
background-size: 40px 40px;
|
|
51
|
+
opacity: 0.45;
|
|
52
|
+
pointer-events: none;
|
|
53
|
+
z-index: 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
body::after {
|
|
57
|
+
content: '';
|
|
58
|
+
position: fixed;
|
|
59
|
+
top: 50%;
|
|
60
|
+
left: 50%;
|
|
61
|
+
transform: translate(-50%, -50%);
|
|
62
|
+
width: 700px;
|
|
63
|
+
height: 700px;
|
|
64
|
+
background: radial-gradient(circle, var(--accent-glow) 0%, transparent 65%);
|
|
65
|
+
pointer-events: none;
|
|
66
|
+
z-index: 0;
|
|
67
|
+
animation: glow-pulse 5s ease-in-out infinite;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@keyframes glow-pulse {
|
|
71
|
+
0%, 100% { opacity: 0.6; transform: translate(-50%, -50%) scale(1); }
|
|
72
|
+
50% { opacity: 1; transform: translate(-50%, -50%) scale(1.08); }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* error/cancelled glow override */
|
|
76
|
+
body.status-error::after {
|
|
77
|
+
background: radial-gradient(circle, rgba(255, 82, 82, 0.1) 0%, transparent 65%);
|
|
78
|
+
}
|
|
79
|
+
body.status-cancelled::after {
|
|
80
|
+
background: radial-gradient(circle, rgba(255, 193, 7, 0.08) 0%, transparent 65%);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.container {
|
|
84
|
+
position: relative;
|
|
85
|
+
z-index: 1;
|
|
86
|
+
width: 100%;
|
|
87
|
+
max-width: 460px;
|
|
88
|
+
padding: 0 24px;
|
|
89
|
+
animation: fade-up 0.5s cubic-bezier(0.16, 1, 0.3, 1) both;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@keyframes fade-up {
|
|
93
|
+
from { opacity: 0; transform: translateY(20px); }
|
|
94
|
+
to { opacity: 1; transform: translateY(0); }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.card {
|
|
98
|
+
background: var(--surface);
|
|
99
|
+
border: 1px solid var(--border-bright);
|
|
100
|
+
border-radius: 12px;
|
|
101
|
+
padding: 36px;
|
|
102
|
+
position: relative;
|
|
103
|
+
overflow: hidden;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.card::before {
|
|
107
|
+
content: '';
|
|
108
|
+
position: absolute;
|
|
109
|
+
top: 0;
|
|
110
|
+
left: 0;
|
|
111
|
+
right: 0;
|
|
112
|
+
height: 1px;
|
|
113
|
+
background: linear-gradient(90deg, transparent, var(--accent), transparent);
|
|
114
|
+
opacity: 0.5;
|
|
115
|
+
transition: background 0.3s;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.card.status-error::before {
|
|
119
|
+
background: linear-gradient(90deg, transparent, #ff5252, transparent);
|
|
120
|
+
}
|
|
121
|
+
.card.status-cancelled::before {
|
|
122
|
+
background: linear-gradient(90deg, transparent, #ffc107, transparent);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.header {
|
|
126
|
+
display: flex;
|
|
127
|
+
align-items: center;
|
|
128
|
+
gap: 12px;
|
|
129
|
+
margin-bottom: 8px;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.icon-svg {
|
|
133
|
+
flex-shrink: 0;
|
|
134
|
+
width: 22px;
|
|
135
|
+
height: 22px;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* success */
|
|
139
|
+
.check-circle {
|
|
140
|
+
fill: none;
|
|
141
|
+
stroke: var(--accent);
|
|
142
|
+
stroke-width: 1.5;
|
|
143
|
+
stroke-dasharray: 69;
|
|
144
|
+
stroke-dashoffset: 69;
|
|
145
|
+
animation: draw-circle 0.45s ease 0.1s forwards;
|
|
146
|
+
}
|
|
147
|
+
.check-tick {
|
|
148
|
+
fill: none;
|
|
149
|
+
stroke: var(--accent);
|
|
150
|
+
stroke-width: 1.8;
|
|
151
|
+
stroke-linecap: round;
|
|
152
|
+
stroke-linejoin: round;
|
|
153
|
+
stroke-dasharray: 16;
|
|
154
|
+
stroke-dashoffset: 16;
|
|
155
|
+
animation: draw-tick 0.3s ease 0.45s forwards;
|
|
156
|
+
}
|
|
157
|
+
@keyframes draw-circle { to { stroke-dashoffset: 0; } }
|
|
158
|
+
@keyframes draw-tick { to { stroke-dashoffset: 0; } }
|
|
159
|
+
|
|
160
|
+
/* error */
|
|
161
|
+
.error-circle {
|
|
162
|
+
fill: none;
|
|
163
|
+
stroke: #ff5252;
|
|
164
|
+
stroke-width: 1.5;
|
|
165
|
+
stroke-dasharray: 69;
|
|
166
|
+
stroke-dashoffset: 69;
|
|
167
|
+
animation: draw-circle 0.45s ease 0.1s forwards;
|
|
168
|
+
}
|
|
169
|
+
.error-x {
|
|
170
|
+
stroke: #ff5252;
|
|
171
|
+
stroke-width: 1.8;
|
|
172
|
+
stroke-linecap: round;
|
|
173
|
+
stroke-dasharray: 20;
|
|
174
|
+
stroke-dashoffset: 20;
|
|
175
|
+
animation: draw-tick 0.3s ease 0.45s forwards;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/* cancelled */
|
|
179
|
+
.cancelled-circle {
|
|
180
|
+
fill: none;
|
|
181
|
+
stroke: #ffc107;
|
|
182
|
+
stroke-width: 1.5;
|
|
183
|
+
stroke-dasharray: 69;
|
|
184
|
+
stroke-dashoffset: 69;
|
|
185
|
+
animation: draw-circle 0.45s ease 0.1s forwards;
|
|
186
|
+
}
|
|
187
|
+
.cancelled-line {
|
|
188
|
+
stroke: #ffc107;
|
|
189
|
+
stroke-width: 1.8;
|
|
190
|
+
stroke-linecap: round;
|
|
191
|
+
stroke-dasharray: 12;
|
|
192
|
+
stroke-dashoffset: 12;
|
|
193
|
+
animation: draw-tick 0.3s ease 0.45s forwards;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
h1 {
|
|
197
|
+
font-size: 20px;
|
|
198
|
+
font-weight: 500;
|
|
199
|
+
letter-spacing: -0.2px;
|
|
200
|
+
color: var(--text);
|
|
201
|
+
line-height: 1;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.subtitle {
|
|
205
|
+
font-size: 13px;
|
|
206
|
+
color: var(--text-dim);
|
|
207
|
+
font-weight: 300;
|
|
208
|
+
margin-bottom: 28px;
|
|
209
|
+
margin-top: 6px;
|
|
210
|
+
padding-left: 34px;
|
|
211
|
+
line-height: 1.5;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.details {
|
|
215
|
+
display: flex;
|
|
216
|
+
flex-direction: column;
|
|
217
|
+
gap: 1px;
|
|
218
|
+
background: var(--border);
|
|
219
|
+
border-radius: 8px;
|
|
220
|
+
overflow: hidden;
|
|
221
|
+
animation: fade-up 0.4s ease 0.25s both;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.detail-row {
|
|
225
|
+
display: flex;
|
|
226
|
+
justify-content: space-between;
|
|
227
|
+
align-items: center;
|
|
228
|
+
padding: 11px 14px;
|
|
229
|
+
background: var(--bg);
|
|
230
|
+
gap: 16px;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.detail-label {
|
|
234
|
+
font-family: 'IBM Plex Mono', monospace;
|
|
235
|
+
font-size: 11px;
|
|
236
|
+
color: var(--text-dimmer);
|
|
237
|
+
text-transform: uppercase;
|
|
238
|
+
letter-spacing: 0.07em;
|
|
239
|
+
white-space: nowrap;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.detail-value {
|
|
243
|
+
font-family: 'IBM Plex Mono', monospace;
|
|
244
|
+
font-size: 12px;
|
|
245
|
+
color: var(--text-dim);
|
|
246
|
+
text-align: right;
|
|
247
|
+
word-break: break-all;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.detail-value.accent { color: var(--accent); }
|
|
251
|
+
.detail-value.accent-error { color: #ff5252; }
|
|
252
|
+
.detail-value.accent-warn { color: #ffc107; }
|
|
253
|
+
|
|
254
|
+
.status-wrap {
|
|
255
|
+
display: flex;
|
|
256
|
+
align-items: center;
|
|
257
|
+
gap: 7px;
|
|
258
|
+
font-family: 'IBM Plex Mono', monospace;
|
|
259
|
+
font-size: 12px;
|
|
260
|
+
color: var(--text-dim);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.status-dot {
|
|
264
|
+
width: 5px;
|
|
265
|
+
height: 5px;
|
|
266
|
+
border-radius: 50%;
|
|
267
|
+
background: var(--accent);
|
|
268
|
+
flex-shrink: 0;
|
|
269
|
+
animation: dot-blink 2.5s ease-in-out infinite;
|
|
270
|
+
}
|
|
271
|
+
.status-dot.error { background: #ff5252; animation: none; }
|
|
272
|
+
.status-dot.cancelled { background: #ffc107; animation: none; }
|
|
273
|
+
|
|
274
|
+
@keyframes dot-blink {
|
|
275
|
+
0%, 100% { opacity: 1; }
|
|
276
|
+
50% { opacity: 0.25; }
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.hidden { display: none; }
|
|
280
|
+
</style>
|
|
281
|
+
</head>
|
|
282
|
+
<body>
|
|
283
|
+
<div class="container">
|
|
284
|
+
<div class="card" id="card">
|
|
285
|
+
|
|
286
|
+
<div class="header">
|
|
287
|
+
<!-- success -->
|
|
288
|
+
<svg id="icon-success" class="icon-svg" viewBox="0 0 22 22">
|
|
289
|
+
<circle class="check-circle" cx="11" cy="11" r="10"/>
|
|
290
|
+
<polyline class="check-tick" points="6.5,11 9.5,14 15.5,8"/>
|
|
291
|
+
</svg>
|
|
292
|
+
<!-- error -->
|
|
293
|
+
<svg id="icon-error" class="icon-svg hidden" viewBox="0 0 22 22">
|
|
294
|
+
<circle class="error-circle" cx="11" cy="11" r="10"/>
|
|
295
|
+
<line class="error-x" x1="7.5" y1="7.5" x2="14.5" y2="14.5"/>
|
|
296
|
+
<line class="error-x" x1="14.5" y1="7.5" x2="7.5" y2="14.5"/>
|
|
297
|
+
</svg>
|
|
298
|
+
<!-- cancelled -->
|
|
299
|
+
<svg id="icon-cancelled" class="icon-svg hidden" viewBox="0 0 22 22">
|
|
300
|
+
<circle class="cancelled-circle" cx="11" cy="11" r="10"/>
|
|
301
|
+
<line class="cancelled-line" x1="7" y1="11" x2="15" y2="11"/>
|
|
302
|
+
</svg>
|
|
303
|
+
<h1 id="title">Payment Successful</h1>
|
|
304
|
+
</div>
|
|
305
|
+
|
|
306
|
+
<p class="subtitle" id="subtitle">Your order has been confirmed.</p>
|
|
307
|
+
|
|
308
|
+
<div class="details">
|
|
309
|
+
<div class="detail-row" id="row-payment-id">
|
|
310
|
+
<span class="detail-label">Payment ID</span>
|
|
311
|
+
<span class="detail-value accent" id="val-payment-id">—</span>
|
|
312
|
+
</div>
|
|
313
|
+
<div class="detail-row" id="row-charge-code">
|
|
314
|
+
<span class="detail-label">Charge Code</span>
|
|
315
|
+
<span class="detail-value accent" id="val-charge-code">—</span>
|
|
316
|
+
</div>
|
|
317
|
+
<div class="detail-row" id="row-provider">
|
|
318
|
+
<span class="detail-label">Provider</span>
|
|
319
|
+
<span class="detail-value" id="val-provider">—</span>
|
|
320
|
+
</div>
|
|
321
|
+
<div class="detail-row" id="row-order-id">
|
|
322
|
+
<span class="detail-label">Order ID</span>
|
|
323
|
+
<span class="detail-value" id="val-order-id">—</span>
|
|
324
|
+
</div>
|
|
325
|
+
<div class="detail-row" id="row-amount">
|
|
326
|
+
<span class="detail-label">Amount</span>
|
|
327
|
+
<span class="detail-value accent" id="val-amount">—</span>
|
|
328
|
+
</div>
|
|
329
|
+
<div class="detail-row" id="row-type">
|
|
330
|
+
<span class="detail-label">Type</span>
|
|
331
|
+
<span class="detail-value" id="val-type">—</span>
|
|
332
|
+
</div>
|
|
333
|
+
<div class="detail-row">
|
|
334
|
+
<span class="detail-label">Status</span>
|
|
335
|
+
<span class="status-wrap">
|
|
336
|
+
<span class="status-dot" id="status-dot"></span>
|
|
337
|
+
<span id="status-text">Confirmed</span>
|
|
338
|
+
</span>
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
|
|
345
|
+
<script>
|
|
346
|
+
const params = new URLSearchParams(window.location.search);
|
|
347
|
+
const status = params.get('status') || 'success';
|
|
348
|
+
const provider = params.get('provider') || '';
|
|
349
|
+
const type = params.get('type');
|
|
350
|
+
|
|
351
|
+
// paypal params
|
|
352
|
+
const paymentId = params.get('payment_id') || params.get('paymentId') || params.get('token');
|
|
353
|
+
const orderId = params.get('order_id') || params.get('orderId');
|
|
354
|
+
const amount = params.get('amount');
|
|
355
|
+
const currency = params.get('currency');
|
|
356
|
+
|
|
357
|
+
// coinbase params
|
|
358
|
+
const chargeCode = params.get('charge_code') || params.get('chargeCode');
|
|
359
|
+
const chargeId = params.get('charge_id') || params.get('chargeId');
|
|
360
|
+
|
|
361
|
+
const card = document.getElementById('card');
|
|
362
|
+
const title = document.getElementById('title');
|
|
363
|
+
const subtitle = document.getElementById('subtitle');
|
|
364
|
+
const statusDot = document.getElementById('status-dot');
|
|
365
|
+
const statusText = document.getElementById('status-text');
|
|
366
|
+
|
|
367
|
+
const configs = {
|
|
368
|
+
success: {
|
|
369
|
+
icon: 'success',
|
|
370
|
+
title: type === 'subscription' ? 'Subscription Activated' : 'Payment Successful',
|
|
371
|
+
subtitle: type === 'subscription' ? 'Your subscription is now active.' : 'Your order has been confirmed.',
|
|
372
|
+
statusLabel: type === 'subscription' ? 'Active' : 'Confirmed',
|
|
373
|
+
dotClass: '',
|
|
374
|
+
accentClass: 'accent',
|
|
375
|
+
cardClass: ''
|
|
376
|
+
},
|
|
377
|
+
error: {
|
|
378
|
+
icon: 'error',
|
|
379
|
+
title: type === 'subscription' ? 'Subscription Failed' : 'Payment Failed',
|
|
380
|
+
subtitle: 'Something went wrong. Please try again.',
|
|
381
|
+
statusLabel: 'Failed',
|
|
382
|
+
dotClass: 'error',
|
|
383
|
+
accentClass: 'accent-error',
|
|
384
|
+
cardClass: 'status-error'
|
|
385
|
+
},
|
|
386
|
+
cancelled: {
|
|
387
|
+
icon: 'cancelled',
|
|
388
|
+
title: 'Payment Cancelled',
|
|
389
|
+
subtitle: 'The payment was cancelled.',
|
|
390
|
+
statusLabel: 'Cancelled',
|
|
391
|
+
dotClass: 'cancelled',
|
|
392
|
+
accentClass: 'accent-warn',
|
|
393
|
+
cardClass: 'status-cancelled'
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
const cfg = configs[status] || configs.success;
|
|
398
|
+
|
|
399
|
+
// apply status styles
|
|
400
|
+
document.body.classList.add(`status-${status}`);
|
|
401
|
+
if (cfg.cardClass) card.classList.add(cfg.cardClass);
|
|
402
|
+
|
|
403
|
+
document.getElementById('icon-success').classList.toggle('hidden', cfg.icon !== 'success');
|
|
404
|
+
document.getElementById('icon-error').classList.toggle('hidden', cfg.icon !== 'error');
|
|
405
|
+
document.getElementById('icon-cancelled').classList.toggle('hidden', cfg.icon !== 'cancelled');
|
|
406
|
+
|
|
407
|
+
title.textContent = cfg.title;
|
|
408
|
+
subtitle.textContent = cfg.subtitle;
|
|
409
|
+
statusDot.className = 'status-dot ' + cfg.dotClass;
|
|
410
|
+
statusText.textContent = cfg.statusLabel;
|
|
411
|
+
|
|
412
|
+
// update accent color on amount + payment id
|
|
413
|
+
document.getElementById('val-payment-id').className = 'detail-value ' + cfg.accentClass;
|
|
414
|
+
document.getElementById('val-amount').className = 'detail-value ' + cfg.accentClass;
|
|
415
|
+
document.getElementById('val-charge-code').className = 'detail-value ' + cfg.accentClass;
|
|
416
|
+
|
|
417
|
+
function setRow(rowId, valId, value) {
|
|
418
|
+
if (value) {
|
|
419
|
+
document.getElementById(valId).textContent = value;
|
|
420
|
+
} else {
|
|
421
|
+
const row = document.getElementById(rowId);
|
|
422
|
+
if (row) row.classList.add('hidden');
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
setRow('row-payment-id', 'val-payment-id', paymentId || chargeId);
|
|
427
|
+
setRow('row-charge-code', 'val-charge-code', chargeCode);
|
|
428
|
+
setRow('row-provider', 'val-provider', provider ? provider.toUpperCase() : null);
|
|
429
|
+
setRow('row-order-id', 'val-order-id', orderId);
|
|
430
|
+
setRow('row-amount', 'val-amount', amount ? `${amount} ${currency || ''}`.trim() : null);
|
|
431
|
+
setRow('row-type', 'val-type', type ? type.charAt(0).toUpperCase() + type.slice(1) : null);
|
|
432
|
+
</script>
|
|
433
|
+
</body>
|
|
434
|
+
</html>
|