@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.
- package/README.md +44 -11
- package/index.mjs +145 -28
- 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
|
|
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
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
325
|
+
transactionId: customId,
|
|
292
326
|
orderId: orderResponse.id,
|
|
293
|
-
amount:
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
810
|
+
return res.redirect(this.redirects.subscriptionError);
|
|
705
811
|
}
|
|
706
812
|
});
|
|
707
813
|
|
|
708
814
|
this.app.get(`${basePath}/cancel`, async (req, res) => {
|
|
709
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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": {
|