@hackthedev/dsync-pay 1.0.0 → 1.0.6

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.
Files changed (3) hide show
  1. package/README.md +44 -11
  2. package/index.mjs +145 -28
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -4,11 +4,12 @@ As another part of the dSync library family this library is responsible for paym
4
4
 
5
5
  > [!NOTE]
6
6
  >
7
- > Payment Providers may take a cut from your money or have other fees that are outside of this library's control.
7
+ > Payment Providers may take a cut from your money or have other fees that are outside of this library's control.
8
8
 
9
9
  ------
10
10
 
11
11
  ## Setup
12
+
12
13
  ```js
13
14
  import dSyncPay from '@hackthedev/dsync-pay';
14
15
 
@@ -17,6 +18,13 @@ const payments = new dSyncPay({
17
18
  app,
18
19
  domain: 'https://domain.com',
19
20
  basePath: '/payments', // optional, default is '/payments'
21
+ redirects: { // optional, customize redirect pages
22
+ success: '/payment-success',
23
+ error: '/payment-error',
24
+ cancelled: '/payment-cancelled',
25
+ subscriptionSuccess: '/subscription-success',
26
+ subscriptionError: '/subscription-error'
27
+ },
20
28
  paypal: {
21
29
  clientId: 'xxx',
22
30
  clientSecret: 'xxx',
@@ -138,17 +146,33 @@ const result = await payments.coinbase.verifyCharge(chargeCode);
138
146
 
139
147
  ## Routes
140
148
 
141
- dSyncPay automatically creates verification routes for handling payment returns as well to make the entire payment process as simple and straight forward as possible.
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.
150
+
151
+ ### Verification Routes
152
+
153
+ #### PayPal
154
+
155
+ - `GET /payments/paypal/verify?token=xxx`
156
+ - `GET /payments/paypal/subscription/verify?subscription_id=xxx`
157
+ - `GET /payments/cancel`
142
158
 
143
- ### PayPal
144
- * `GET /payments/paypal/verify?token=xxx`
145
- * `GET /payments/paypal/subscription/verify?subscription_id=xxx`
146
- * `GET /payments/cancel`
159
+ #### Coinbase
147
160
 
148
- ### Coinbase
149
- * `GET /payments/coinbase/verify?code=xxx`
150
- * `POST /payments/webhook/coinbase` (if webhookSecret set)
151
- * `GET /payments/cancel`
161
+ - `GET /payments/coinbase/verify?code=xxx`
162
+ - `POST /payments/webhook/coinbase` (if webhookSecret set)
163
+ - `GET /payments/cancel`
164
+
165
+ ### Auto-Generated Redirect Pages
166
+
167
+ The library automatically creates simple redirect pages at:
168
+
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
174
+
175
+ You can customize these redirect URLs in the constructor with the `redirects` parameter.
152
176
 
153
177
  ### Usage Example
154
178
 
@@ -161,18 +185,27 @@ const order = await payments.paypal.createOrder({
161
185
  // redirect user to order.approvalUrl
162
186
  // paypal redirects back to /payments/paypal/verify?token=XXX
163
187
  // route automatically verifies and triggers onPaymentCompleted
188
+ // then redirects to /payment-success
164
189
  ```
165
190
 
166
- ### Custom Base Path
191
+ ### Custom Configuration
167
192
 
168
193
  ```javascript
169
194
  const payments = new dSyncPay({
170
195
  app,
171
196
  domain: 'https://domain.com',
172
197
  basePath: '/api/pay', // default is '/payments'
198
+ redirects: {
199
+ success: '/custom/success',
200
+ error: '/custom/error',
201
+ cancelled: '/custom/cancelled',
202
+ subscriptionSuccess: '/custom/sub-success',
203
+ subscriptionError: '/custom/sub-error'
204
+ },
173
205
  paypal: { ... }
174
206
  });
175
207
 
176
208
  // routes: /api/pay/paypal/verify, /api/pay/coinbase/verify, etc.
177
209
  // auto urls: https://domain.com/api/pay/paypal/verify
210
+ // redirect pages: /custom/success, /custom/error, etc.
178
211
  ```
package/index.mjs CHANGED
@@ -5,6 +5,13 @@ export default class dSyncPay {
5
5
  app = null,
6
6
  domain = null,
7
7
  basePath = '/payments',
8
+ redirects = {
9
+ success: '/payment-success',
10
+ error: '/payment-error',
11
+ cancelled: '/payment-cancelled',
12
+ subscriptionSuccess: '/subscription-success',
13
+ subscriptionError: '/subscription-error'
14
+ },
8
15
  paypal = null,
9
16
  coinbase = null,
10
17
  onPaymentCreated = null,
@@ -22,6 +29,8 @@ export default class dSyncPay {
22
29
  this.app = app;
23
30
  this.domain = domain.endsWith('/') ? domain.slice(0, -1) : domain;
24
31
  this.basePath = basePath;
32
+ this.redirects = redirects;
33
+ this.metadataCache = new Map();
25
34
 
26
35
  this.callbacks = {
27
36
  onPaymentCreated,
@@ -46,17 +55,18 @@ export default class dSyncPay {
46
55
  }
47
56
 
48
57
  this.registerRoutes(basePath);
58
+ this.registerRedirectRoutes();
49
59
  }
50
60
 
51
61
  getUrl(path) {
52
62
  return `${this.domain}${this.basePath}${path}`;
53
63
  }
54
64
 
55
- emit(event, data) {
65
+ async emit(event, data) {
56
66
  const callback = this.callbacks[event];
57
67
  if (callback) {
58
68
  try {
59
- callback(data);
69
+ await callback(data);
60
70
  } catch (err) {
61
71
  console.error("callback error:", err);
62
72
  }
@@ -242,6 +252,14 @@ export default class dSyncPay {
242
252
  rawResponse: response
243
253
  };
244
254
 
255
+ // save metadata in cache
256
+ this.parent.metadataCache.set(response.id, metadata);
257
+
258
+ // auto cleanup after 1 hour if never verified
259
+ setTimeout(() => {
260
+ this.parent.metadataCache.delete(response.id);
261
+ }, 60 * 60 * 1000);
262
+
245
263
  this.parent.emit('onPaymentCreated', result);
246
264
  return result;
247
265
  } catch (error) {
@@ -258,7 +276,7 @@ export default class dSyncPay {
258
276
  const accessToken = await this.getAccessToken();
259
277
 
260
278
  try {
261
- const orderResponse = await this.parent.request(
279
+ let orderResponse = await this.parent.request(
262
280
  `${this.baseUrl}/v2/checkout/orders/${orderId}`,
263
281
  {
264
282
  headers: {
@@ -267,10 +285,10 @@ export default class dSyncPay {
267
285
  }
268
286
  );
269
287
 
270
- const orderStatus = orderResponse.status;
288
+ let orderStatus = orderResponse.status;
271
289
 
272
290
  if (orderStatus === "APPROVED") {
273
- await this.parent.request(
291
+ const captureResponse = await this.parent.request(
274
292
  `${this.baseUrl}/v2/checkout/orders/${orderId}/capture`,
275
293
  {
276
294
  method: 'POST',
@@ -280,27 +298,49 @@ export default class dSyncPay {
280
298
  body: {}
281
299
  }
282
300
  );
301
+
302
+ orderStatus = captureResponse.status;
303
+ orderResponse = captureResponse;
304
+ }
305
+
306
+ const purchaseUnit = orderResponse.purchase_units?.[0] || {};
307
+
308
+ let amount = 0;
309
+ let customId = purchaseUnit.custom_id;
310
+
311
+ if (purchaseUnit.payments?.captures?.[0]) {
312
+ amount = parseFloat(purchaseUnit.payments.captures[0].amount.value);
313
+ customId = purchaseUnit.payments.captures[0].custom_id || purchaseUnit.custom_id;
314
+ } else if (purchaseUnit.amount?.value) {
315
+ amount = parseFloat(purchaseUnit.amount.value);
283
316
  }
284
317
 
285
- const purchaseUnit = orderResponse.purchase_units[0];
318
+ // get metadata from cache
319
+ const metadata = this.parent.metadataCache.get(orderId) || {};
286
320
 
287
321
  const result = {
288
322
  provider: 'paypal',
289
323
  type: 'order',
290
324
  status: orderStatus,
291
- transactionId: purchaseUnit.custom_id,
325
+ transactionId: customId,
292
326
  orderId: orderResponse.id,
293
- amount: parseFloat(purchaseUnit.amount.value),
294
- currency: purchaseUnit.amount.currency_code,
327
+ amount: amount,
328
+ currency: purchaseUnit.payments?.captures?.[0]?.amount?.currency_code || purchaseUnit.amount?.currency_code || 'EUR',
329
+ metadata,
295
330
  rawResponse: orderResponse
296
331
  };
297
332
 
298
333
  if (orderStatus === 'COMPLETED') {
299
- this.parent.emit('onPaymentCompleted', result);
300
- } else if (orderStatus === 'VOIDED') {
301
- this.parent.emit('onPaymentCancelled', result);
334
+ await this.parent.emit('onPaymentCompleted', result);
335
+ } else if (orderStatus === 'VOIDED' || orderStatus === 'CANCELLED') {
336
+ await this.parent.emit('onPaymentCancelled', result);
337
+ } else {
338
+ await this.parent.emit('onPaymentFailed', result);
302
339
  }
303
340
 
341
+ // cleanup cache after verify
342
+ this.parent.metadataCache.delete(orderId);
343
+
304
344
  return result;
305
345
  } catch (error) {
306
346
  this.parent.emit('onError', {
@@ -309,6 +349,10 @@ export default class dSyncPay {
309
349
  orderId,
310
350
  error: error.response || error.message
311
351
  });
352
+
353
+ // cleanup cache after verify
354
+ this.parent.metadataCache.delete(orderId);
355
+
312
356
  throw error;
313
357
  }
314
358
  }
@@ -457,6 +501,14 @@ export default class dSyncPay {
457
501
  rawResponse: response
458
502
  };
459
503
 
504
+ // save metadata in cache
505
+ this.parent.metadataCache.set(response.id, metadata);
506
+
507
+ // auto cleanup after 1 hour if never verified
508
+ setTimeout(() => {
509
+ this.parent.metadataCache.delete(response.id);
510
+ }, 60 * 60 * 1000);
511
+
460
512
  this.parent.emit('onSubscriptionCreated', result);
461
513
  return result;
462
514
  } catch (error) {
@@ -482,6 +534,9 @@ export default class dSyncPay {
482
534
  }
483
535
  );
484
536
 
537
+ // get metadata from cache
538
+ const metadata = this.parent.metadataCache.get(subscriptionId) || {};
539
+
485
540
  const result = {
486
541
  provider: 'paypal',
487
542
  type: 'subscription',
@@ -489,15 +544,19 @@ export default class dSyncPay {
489
544
  subscriptionId: response.id,
490
545
  planId: response.plan_id,
491
546
  customId: response.custom_id,
547
+ metadata,
492
548
  rawResponse: response
493
549
  };
494
550
 
495
551
  if (response.status === 'ACTIVE') {
496
- this.parent.emit('onSubscriptionActivated', result);
552
+ await this.parent.emit('onSubscriptionActivated', result);
497
553
  } else if (response.status === 'CANCELLED') {
498
- this.parent.emit('onSubscriptionCancelled', result);
554
+ await this.parent.emit('onSubscriptionCancelled', result);
499
555
  }
500
556
 
557
+ // cleanup cache after verify
558
+ this.parent.metadataCache.delete(subscriptionId);
559
+
501
560
  return result;
502
561
  } catch (error) {
503
562
  this.parent.emit('onError', {
@@ -506,6 +565,10 @@ export default class dSyncPay {
506
565
  subscriptionId,
507
566
  error: error.response || error.message
508
567
  });
568
+
569
+ // cleanup cache after verify
570
+ this.parent.metadataCache.delete(subscriptionId);
571
+
509
572
  throw error;
510
573
  }
511
574
  }
@@ -526,15 +589,22 @@ export default class dSyncPay {
526
589
  }
527
590
  );
528
591
 
592
+ // get metadata from cache
593
+ const metadata = this.parent.metadataCache.get(subscriptionId) || {};
594
+
529
595
  const result = {
530
596
  provider: 'paypal',
531
597
  type: 'subscription',
532
598
  subscriptionId,
533
599
  status: 'CANCELLED',
534
- reason
600
+ reason,
601
+ metadata
535
602
  };
536
603
 
537
- this.parent.emit('onSubscriptionCancelled', result);
604
+ // cleanup cache
605
+ this.parent.metadataCache.delete(subscriptionId);
606
+
607
+ await this.parent.emit('onSubscriptionCancelled', result);
538
608
  return result;
539
609
  } catch (error) {
540
610
  this.parent.emit('onError', {
@@ -543,6 +613,10 @@ export default class dSyncPay {
543
613
  subscriptionId,
544
614
  error: error.response || error.message
545
615
  });
616
+
617
+ // cleanup cache on error too
618
+ this.parent.metadataCache.delete(subscriptionId);
619
+
546
620
  throw error;
547
621
  }
548
622
  }
@@ -652,11 +726,11 @@ export default class dSyncPay {
652
726
  };
653
727
 
654
728
  if (latestStatus === 'COMPLETED') {
655
- this.parent.emit('onPaymentCompleted', result);
729
+ await this.parent.emit('onPaymentCompleted', result);
656
730
  } else if (latestStatus === 'CANCELED') {
657
- this.parent.emit('onPaymentCancelled', result);
731
+ await this.parent.emit('onPaymentCancelled', result);
658
732
  } else if (latestStatus === 'EXPIRED' || latestStatus === 'UNRESOLVED') {
659
- this.parent.emit('onPaymentFailed', result);
733
+ await this.parent.emit('onPaymentFailed', result);
660
734
  }
661
735
 
662
736
  return result;
@@ -679,6 +753,28 @@ export default class dSyncPay {
679
753
  }
680
754
  }
681
755
 
756
+ registerRedirectRoutes() {
757
+ this.app.get(this.redirects.success, (req, res) => {
758
+ res.send('payment successful! thank you.');
759
+ });
760
+
761
+ this.app.get(this.redirects.error, (req, res) => {
762
+ res.send('payment failed. please try again.');
763
+ });
764
+
765
+ this.app.get(this.redirects.cancelled, (req, res) => {
766
+ res.send('payment cancelled.');
767
+ });
768
+
769
+ this.app.get(this.redirects.subscriptionSuccess, (req, res) => {
770
+ res.send('subscription activated!');
771
+ });
772
+
773
+ this.app.get(this.redirects.subscriptionError, (req, res) => {
774
+ res.send('subscription failed.');
775
+ });
776
+ }
777
+
682
778
  registerRoutes(basePath = '/payments') {
683
779
  if (this.paypal) {
684
780
  this.app.get(`${basePath}/paypal/verify`, async (req, res) => {
@@ -687,9 +783,14 @@ export default class dSyncPay {
687
783
  if (!orderId) return res.status(400).json({ok: false, error: 'missing_token'});
688
784
 
689
785
  const result = await this.paypal.verifyOrder(orderId);
690
- res.json({ok: true, ...result});
786
+
787
+ if (result.status === 'COMPLETED') {
788
+ return res.redirect(this.redirects.success);
789
+ } else {
790
+ return res.redirect(this.redirects.error);
791
+ }
691
792
  } catch (error) {
692
- res.status(500).json({ok: false, error: error.message});
793
+ return res.redirect(this.redirects.error);
693
794
  }
694
795
  });
695
796
 
@@ -699,14 +800,25 @@ export default class dSyncPay {
699
800
  if (!subscriptionId) return res.status(400).json({ok: false, error: 'missing_subscription_id'});
700
801
 
701
802
  const result = await this.paypal.verifySubscription(subscriptionId);
702
- res.json({ok: true, ...result});
803
+
804
+ if (result.status === 'ACTIVE') {
805
+ return res.redirect(this.redirects.subscriptionSuccess);
806
+ } else {
807
+ return res.redirect(this.redirects.subscriptionError);
808
+ }
703
809
  } catch (error) {
704
- res.status(500).json({ok: false, error: error.message});
810
+ return res.redirect(this.redirects.subscriptionError);
705
811
  }
706
812
  });
707
813
 
708
814
  this.app.get(`${basePath}/cancel`, async (req, res) => {
709
- res.send('payment cancelled');
815
+ const orderId = req.query.token;
816
+ if (orderId) {
817
+ try {
818
+ await this.paypal.verifyOrder(orderId);
819
+ } catch (e) {}
820
+ }
821
+ return res.redirect(this.redirects.cancelled);
710
822
  });
711
823
  }
712
824
 
@@ -717,9 +829,14 @@ export default class dSyncPay {
717
829
  if (!chargeCode) return res.status(400).json({ok: false, error: 'missing_code'});
718
830
 
719
831
  const result = await this.coinbase.verifyCharge(chargeCode);
720
- res.json({ok: true, ...result});
832
+
833
+ if (result.status === 'COMPLETED') {
834
+ return res.redirect(this.redirects.success);
835
+ } else {
836
+ return res.redirect(this.redirects.error);
837
+ }
721
838
  } catch (error) {
722
- res.status(500).json({ok: false, error: error.message});
839
+ return res.redirect(this.redirects.error);
723
840
  }
724
841
  });
725
842
 
@@ -739,7 +856,7 @@ export default class dSyncPay {
739
856
 
740
857
  if (event.event.type === 'charge:confirmed') {
741
858
  const chargeId = event.event.data.id;
742
- const result = await this.coinbase.verifyCharge(chargeId);
859
+ await this.coinbase.verifyCharge(chargeId);
743
860
  }
744
861
 
745
862
  res.status(200).json({ok: true});
@@ -752,4 +869,4 @@ export default class dSyncPay {
752
869
 
753
870
  return this;
754
871
  }
755
- }
872
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hackthedev/dsync-pay",
3
- "version": "1.0.0",
3
+ "version": "1.0.6",
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": {