@hackthedev/dsync-pay 1.0.5 → 1.0.7
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 +97 -24
- 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({
|
|
@@ -30,6 +36,7 @@ export default class dSyncPay {
|
|
|
30
36
|
this.domain = domain.endsWith('/') ? domain.slice(0, -1) : domain;
|
|
31
37
|
this.basePath = basePath;
|
|
32
38
|
this.redirects = redirects;
|
|
39
|
+
this.metadataCache = new Map();
|
|
33
40
|
|
|
34
41
|
this.callbacks = {
|
|
35
42
|
onPaymentCreated,
|
|
@@ -42,6 +49,9 @@ export default class dSyncPay {
|
|
|
42
49
|
onError
|
|
43
50
|
};
|
|
44
51
|
|
|
52
|
+
this.webPath = path.join(__dirname, "web");
|
|
53
|
+
if(!fs.existsSync(this.webPath)) throw new Error("missing web path");
|
|
54
|
+
|
|
45
55
|
if (paypal) {
|
|
46
56
|
if (!paypal.clientId) throw new Error("missing paypal.clientId");
|
|
47
57
|
if (!paypal.clientSecret) throw new Error("missing paypal.clientSecret");
|
|
@@ -53,6 +63,11 @@ export default class dSyncPay {
|
|
|
53
63
|
this.coinbase = new this.Coinbase(this, coinbase);
|
|
54
64
|
}
|
|
55
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
|
+
|
|
56
71
|
this.registerRoutes(basePath);
|
|
57
72
|
this.registerRedirectRoutes();
|
|
58
73
|
}
|
|
@@ -101,7 +116,7 @@ export default class dSyncPay {
|
|
|
101
116
|
|
|
102
117
|
const fetchOptions = {
|
|
103
118
|
method,
|
|
104
|
-
headers: {...headers}
|
|
119
|
+
headers: { ...headers }
|
|
105
120
|
};
|
|
106
121
|
|
|
107
122
|
if (auth) {
|
|
@@ -251,6 +266,12 @@ export default class dSyncPay {
|
|
|
251
266
|
rawResponse: response
|
|
252
267
|
};
|
|
253
268
|
|
|
269
|
+
this.parent.metadataCache.set(response.id, metadata);
|
|
270
|
+
|
|
271
|
+
setTimeout(() => {
|
|
272
|
+
this.parent.metadataCache.delete(response.id);
|
|
273
|
+
}, 60 * 60 * 1000);
|
|
274
|
+
|
|
254
275
|
this.parent.emit('onPaymentCreated', result);
|
|
255
276
|
return result;
|
|
256
277
|
} catch (error) {
|
|
@@ -306,6 +327,8 @@ export default class dSyncPay {
|
|
|
306
327
|
amount = parseFloat(purchaseUnit.amount.value);
|
|
307
328
|
}
|
|
308
329
|
|
|
330
|
+
const metadata = this.parent.metadataCache.get(orderId) || {};
|
|
331
|
+
|
|
309
332
|
const result = {
|
|
310
333
|
provider: 'paypal',
|
|
311
334
|
type: 'order',
|
|
@@ -314,6 +337,7 @@ export default class dSyncPay {
|
|
|
314
337
|
orderId: orderResponse.id,
|
|
315
338
|
amount: amount,
|
|
316
339
|
currency: purchaseUnit.payments?.captures?.[0]?.amount?.currency_code || purchaseUnit.amount?.currency_code || 'EUR',
|
|
340
|
+
metadata,
|
|
317
341
|
rawResponse: orderResponse
|
|
318
342
|
};
|
|
319
343
|
|
|
@@ -324,6 +348,9 @@ export default class dSyncPay {
|
|
|
324
348
|
} else {
|
|
325
349
|
await this.parent.emit('onPaymentFailed', result);
|
|
326
350
|
}
|
|
351
|
+
|
|
352
|
+
this.parent.metadataCache.delete(orderId);
|
|
353
|
+
|
|
327
354
|
return result;
|
|
328
355
|
} catch (error) {
|
|
329
356
|
this.parent.emit('onError', {
|
|
@@ -332,6 +359,9 @@ export default class dSyncPay {
|
|
|
332
359
|
orderId,
|
|
333
360
|
error: error.response || error.message
|
|
334
361
|
});
|
|
362
|
+
|
|
363
|
+
this.parent.metadataCache.delete(orderId);
|
|
364
|
+
|
|
335
365
|
throw error;
|
|
336
366
|
}
|
|
337
367
|
}
|
|
@@ -480,6 +510,12 @@ export default class dSyncPay {
|
|
|
480
510
|
rawResponse: response
|
|
481
511
|
};
|
|
482
512
|
|
|
513
|
+
this.parent.metadataCache.set(response.id, metadata);
|
|
514
|
+
|
|
515
|
+
setTimeout(() => {
|
|
516
|
+
this.parent.metadataCache.delete(response.id);
|
|
517
|
+
}, 60 * 60 * 1000);
|
|
518
|
+
|
|
483
519
|
this.parent.emit('onSubscriptionCreated', result);
|
|
484
520
|
return result;
|
|
485
521
|
} catch (error) {
|
|
@@ -505,6 +541,8 @@ export default class dSyncPay {
|
|
|
505
541
|
}
|
|
506
542
|
);
|
|
507
543
|
|
|
544
|
+
const metadata = this.parent.metadataCache.get(subscriptionId) || {};
|
|
545
|
+
|
|
508
546
|
const result = {
|
|
509
547
|
provider: 'paypal',
|
|
510
548
|
type: 'subscription',
|
|
@@ -512,15 +550,18 @@ export default class dSyncPay {
|
|
|
512
550
|
subscriptionId: response.id,
|
|
513
551
|
planId: response.plan_id,
|
|
514
552
|
customId: response.custom_id,
|
|
553
|
+
metadata,
|
|
515
554
|
rawResponse: response
|
|
516
555
|
};
|
|
517
556
|
|
|
518
557
|
if (response.status === 'ACTIVE') {
|
|
519
|
-
this.parent.emit('onSubscriptionActivated', result);
|
|
558
|
+
await this.parent.emit('onSubscriptionActivated', result);
|
|
520
559
|
} else if (response.status === 'CANCELLED') {
|
|
521
|
-
this.parent.emit('onSubscriptionCancelled', result);
|
|
560
|
+
await this.parent.emit('onSubscriptionCancelled', result);
|
|
522
561
|
}
|
|
523
562
|
|
|
563
|
+
this.parent.metadataCache.delete(subscriptionId);
|
|
564
|
+
|
|
524
565
|
return result;
|
|
525
566
|
} catch (error) {
|
|
526
567
|
this.parent.emit('onError', {
|
|
@@ -529,6 +570,9 @@ export default class dSyncPay {
|
|
|
529
570
|
subscriptionId,
|
|
530
571
|
error: error.response || error.message
|
|
531
572
|
});
|
|
573
|
+
|
|
574
|
+
this.parent.metadataCache.delete(subscriptionId);
|
|
575
|
+
|
|
532
576
|
throw error;
|
|
533
577
|
}
|
|
534
578
|
}
|
|
@@ -545,19 +589,24 @@ export default class dSyncPay {
|
|
|
545
589
|
"Content-Type": "application/json",
|
|
546
590
|
"Authorization": `Bearer ${accessToken}`
|
|
547
591
|
},
|
|
548
|
-
body: {reason}
|
|
592
|
+
body: { reason }
|
|
549
593
|
}
|
|
550
594
|
);
|
|
551
595
|
|
|
596
|
+
const metadata = this.parent.metadataCache.get(subscriptionId) || {};
|
|
597
|
+
|
|
552
598
|
const result = {
|
|
553
599
|
provider: 'paypal',
|
|
554
600
|
type: 'subscription',
|
|
555
601
|
subscriptionId,
|
|
556
602
|
status: 'CANCELLED',
|
|
557
|
-
reason
|
|
603
|
+
reason,
|
|
604
|
+
metadata
|
|
558
605
|
};
|
|
559
606
|
|
|
560
|
-
this.parent.
|
|
607
|
+
this.parent.metadataCache.delete(subscriptionId);
|
|
608
|
+
|
|
609
|
+
await this.parent.emit('onSubscriptionCancelled', result);
|
|
561
610
|
return result;
|
|
562
611
|
} catch (error) {
|
|
563
612
|
this.parent.emit('onError', {
|
|
@@ -566,6 +615,9 @@ export default class dSyncPay {
|
|
|
566
615
|
subscriptionId,
|
|
567
616
|
error: error.response || error.message
|
|
568
617
|
});
|
|
618
|
+
|
|
619
|
+
this.parent.metadataCache.delete(subscriptionId);
|
|
620
|
+
|
|
569
621
|
throw error;
|
|
570
622
|
}
|
|
571
623
|
}
|
|
@@ -675,11 +727,11 @@ export default class dSyncPay {
|
|
|
675
727
|
};
|
|
676
728
|
|
|
677
729
|
if (latestStatus === 'COMPLETED') {
|
|
678
|
-
this.parent.emit('onPaymentCompleted', result);
|
|
730
|
+
await this.parent.emit('onPaymentCompleted', result);
|
|
679
731
|
} else if (latestStatus === 'CANCELED') {
|
|
680
|
-
this.parent.emit('onPaymentCancelled', result);
|
|
732
|
+
await this.parent.emit('onPaymentCancelled', result);
|
|
681
733
|
} else if (latestStatus === 'EXPIRED' || latestStatus === 'UNRESOLVED') {
|
|
682
|
-
this.parent.emit('onPaymentFailed', result);
|
|
734
|
+
await this.parent.emit('onPaymentFailed', result);
|
|
683
735
|
}
|
|
684
736
|
|
|
685
737
|
return result;
|
|
@@ -704,37 +756,48 @@ export default class dSyncPay {
|
|
|
704
756
|
|
|
705
757
|
registerRedirectRoutes() {
|
|
706
758
|
this.app.get(this.redirects.success, (req, res) => {
|
|
707
|
-
|
|
759
|
+
const query = new URLSearchParams({ ...req.query, status: 'success' }).toString();
|
|
760
|
+
return res.redirect(`${this.basePath}/payment-status.html?${query}`);
|
|
708
761
|
});
|
|
709
762
|
|
|
710
763
|
this.app.get(this.redirects.error, (req, res) => {
|
|
711
|
-
|
|
764
|
+
const query = new URLSearchParams({ ...req.query, status: 'error' }).toString();
|
|
765
|
+
return res.redirect(`${this.basePath}/payment-status.html?${query}`);
|
|
712
766
|
});
|
|
713
767
|
|
|
714
768
|
this.app.get(this.redirects.cancelled, (req, res) => {
|
|
715
|
-
|
|
769
|
+
const query = new URLSearchParams({ ...req.query, status: 'cancelled' }).toString();
|
|
770
|
+
return res.redirect(`${this.basePath}/payment-status.html?${query}`);
|
|
716
771
|
});
|
|
717
772
|
|
|
718
773
|
this.app.get(this.redirects.subscriptionSuccess, (req, res) => {
|
|
719
|
-
|
|
774
|
+
const query = new URLSearchParams({ ...req.query, status: 'success', type: 'subscription' }).toString();
|
|
775
|
+
return res.redirect(`${this.basePath}/payment-status.html?${query}`);
|
|
720
776
|
});
|
|
721
777
|
|
|
722
778
|
this.app.get(this.redirects.subscriptionError, (req, res) => {
|
|
723
|
-
|
|
779
|
+
const query = new URLSearchParams({ ...req.query, status: 'error', type: 'subscription' }).toString();
|
|
780
|
+
return res.redirect(`${this.basePath}/payment-status.html?${query}`);
|
|
724
781
|
});
|
|
725
782
|
}
|
|
726
783
|
|
|
727
|
-
registerRoutes(basePath = '/payments') {
|
|
784
|
+
registerRoutes(basePath = '/payments') {
|
|
728
785
|
if (this.paypal) {
|
|
729
786
|
this.app.get(`${basePath}/paypal/verify`, async (req, res) => {
|
|
730
787
|
try {
|
|
731
788
|
const orderId = req.query.token;
|
|
732
|
-
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' });
|
|
733
790
|
|
|
734
791
|
const result = await this.paypal.verifyOrder(orderId);
|
|
735
792
|
|
|
736
793
|
if (result.status === 'COMPLETED') {
|
|
737
|
-
|
|
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}`);
|
|
738
801
|
} else {
|
|
739
802
|
return res.redirect(this.redirects.error);
|
|
740
803
|
}
|
|
@@ -746,12 +809,16 @@ export default class dSyncPay {
|
|
|
746
809
|
this.app.get(`${basePath}/paypal/subscription/verify`, async (req, res) => {
|
|
747
810
|
try {
|
|
748
811
|
const subscriptionId = req.query.subscription_id;
|
|
749
|
-
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' });
|
|
750
813
|
|
|
751
814
|
const result = await this.paypal.verifySubscription(subscriptionId);
|
|
752
815
|
|
|
753
816
|
if (result.status === 'ACTIVE') {
|
|
754
|
-
|
|
817
|
+
const query = new URLSearchParams({
|
|
818
|
+
payment_id: subscriptionId,
|
|
819
|
+
provider: 'paypal'
|
|
820
|
+
}).toString();
|
|
821
|
+
return res.redirect(`${this.redirects.subscriptionSuccess}?${query}`);
|
|
755
822
|
} else {
|
|
756
823
|
return res.redirect(this.redirects.subscriptionError);
|
|
757
824
|
}
|
|
@@ -775,12 +842,18 @@ export default class dSyncPay {
|
|
|
775
842
|
this.app.get(`${basePath}/coinbase/verify`, async (req, res) => {
|
|
776
843
|
try {
|
|
777
844
|
const chargeCode = req.query.code;
|
|
778
|
-
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' });
|
|
779
846
|
|
|
780
847
|
const result = await this.coinbase.verifyCharge(chargeCode);
|
|
781
848
|
|
|
782
849
|
if (result.status === 'COMPLETED') {
|
|
783
|
-
|
|
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}`);
|
|
784
857
|
} else {
|
|
785
858
|
return res.redirect(this.redirects.error);
|
|
786
859
|
}
|
|
@@ -799,7 +872,7 @@ export default class dSyncPay {
|
|
|
799
872
|
this.coinbase.config.webhookSecret
|
|
800
873
|
);
|
|
801
874
|
|
|
802
|
-
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' });
|
|
803
876
|
|
|
804
877
|
const event = req.body;
|
|
805
878
|
|
|
@@ -808,9 +881,9 @@ export default class dSyncPay {
|
|
|
808
881
|
await this.coinbase.verifyCharge(chargeId);
|
|
809
882
|
}
|
|
810
883
|
|
|
811
|
-
res.status(200).json({ok: true});
|
|
884
|
+
res.status(200).json({ ok: true });
|
|
812
885
|
} catch (error) {
|
|
813
|
-
res.status(500).json({ok: false, error: 'webhook_error'});
|
|
886
|
+
res.status(500).json({ ok: false, error: 'webhook_error' });
|
|
814
887
|
}
|
|
815
888
|
});
|
|
816
889
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hackthedev/dsync-pay",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
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>
|