@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 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. Its works independently and without any percentage cuts by using your own API keys.
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' // optional
34
+ apiKey: 'xxx', // coinbase commerce API key
35
+ webhookSecret: 'xxx' // optional, for webhook verification
36
36
  },
37
-
38
- // events from the library
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
- returnUrl: 'https://custom.com/success',
70
- cancelUrl: 'https://custom.com/cancel',
71
- metadata: { userId: '123' }
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
- // manual verify. result.status === 'COMPLETED'. see paypal api.
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
- // requires you to setup a plan one time
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
- // save plan.planId
88
-
89
- // then you can create subscriptions based on that plan
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
- // or override manually
117
+ // step 2: create a subscription
97
118
  const sub = await payments.paypal.createSubscription({
98
119
  planId: 'P-xxxxx',
99
- returnUrl: 'https://custom.com/success',
100
- cancelUrl: 'https://custom.com/cancel'
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 sub.approvalUrl
104
- // also returns sub.subscriptionId
127
+ // redirect user to:
128
+ sub.approvalUrl
129
+ // also available: sub.subscriptionId
105
130
 
106
- // manually verify subscription
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
- redirectUrl: 'https://custom.com/success',
134
- cancelUrl: 'https://custom.com/cancel',
135
- metadata: { userId: '123' }
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: charge.hostedUrl
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
- // manually verify
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 creates verification routes and redirect pages for handling payment returns to make the entire payment process as simple and straight forward as possible.
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
- ### Auto-Generated Redirect Pages
202
+ ### Status Page
166
203
 
167
- The library automatically creates simple redirect pages at:
204
+ After a payment is completed, failed, or cancelled, the user is redirected to a built-in status page at:
168
205
 
169
- - `GET /payment-success` - shown after successful payment
170
- - `GET /payment-error` - shown when payment fails
171
- - `GET /payment-cancelled` - shown when user cancels payment
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&currency=EUR
208
+ ```
174
209
 
175
- You can customize these redirect URLs in the constructor with the `redirects` parameter.
210
+ The `status` query param controls what is shown:
176
211
 
177
- ### Usage Example
212
+ | status | description |
213
+ | ----------- | --------------------------------- |
214
+ | `success` | payment or subscription completed |
215
+ | `error` | payment failed |
216
+ | `cancelled` | user cancelled |
178
217
 
179
- ```javascript
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
- // redirect user to order.approvalUrl
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
- ### Custom Configuration
222
+ ------
223
+
224
+ ## Custom Configuration
192
225
 
193
- ```javascript
226
+ ```js
194
227
  const payments = new dSyncPay({
195
228
  app,
196
229
  domain: 'https://domain.com',
197
- basePath: '/api/pay', // default is '/payments'
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
- // redirect pages: /custom/success, /custom/error, etc.
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.emit('onSubscriptionCancelled', result);
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
- res.send('payment successful! thank you.');
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
- res.send('payment failed. please try again.');
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
- res.send('payment cancelled.');
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
- res.send('subscription activated!');
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
- res.send('subscription failed.');
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
- return res.redirect(this.redirects.success);
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
- return res.redirect(this.redirects.subscriptionSuccess);
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
- return res.redirect(this.redirects.success);
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.5",
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>