@hackthedev/dsync-pay 1.0.6 → 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({
@@ -43,6 +49,9 @@ export default class dSyncPay {
43
49
  onError
44
50
  };
45
51
 
52
+ this.webPath = path.join(__dirname, "web");
53
+ if(!fs.existsSync(this.webPath)) throw new Error("missing web path");
54
+
46
55
  if (paypal) {
47
56
  if (!paypal.clientId) throw new Error("missing paypal.clientId");
48
57
  if (!paypal.clientSecret) throw new Error("missing paypal.clientSecret");
@@ -54,6 +63,11 @@ export default class dSyncPay {
54
63
  this.coinbase = new this.Coinbase(this, coinbase);
55
64
  }
56
65
 
66
+ // serve static files from web/ folder
67
+ this.app.get(`${this.basePath}/payment-status.html`, (req, res) => {
68
+ res.sendFile(path.join(__dirname, "web", "payment-status.html"));
69
+ });
70
+
57
71
  this.registerRoutes(basePath);
58
72
  this.registerRedirectRoutes();
59
73
  }
@@ -102,7 +116,7 @@ export default class dSyncPay {
102
116
 
103
117
  const fetchOptions = {
104
118
  method,
105
- headers: {...headers}
119
+ headers: { ...headers }
106
120
  };
107
121
 
108
122
  if (auth) {
@@ -252,10 +266,8 @@ export default class dSyncPay {
252
266
  rawResponse: response
253
267
  };
254
268
 
255
- // save metadata in cache
256
269
  this.parent.metadataCache.set(response.id, metadata);
257
270
 
258
- // auto cleanup after 1 hour if never verified
259
271
  setTimeout(() => {
260
272
  this.parent.metadataCache.delete(response.id);
261
273
  }, 60 * 60 * 1000);
@@ -315,7 +327,6 @@ export default class dSyncPay {
315
327
  amount = parseFloat(purchaseUnit.amount.value);
316
328
  }
317
329
 
318
- // get metadata from cache
319
330
  const metadata = this.parent.metadataCache.get(orderId) || {};
320
331
 
321
332
  const result = {
@@ -338,7 +349,6 @@ export default class dSyncPay {
338
349
  await this.parent.emit('onPaymentFailed', result);
339
350
  }
340
351
 
341
- // cleanup cache after verify
342
352
  this.parent.metadataCache.delete(orderId);
343
353
 
344
354
  return result;
@@ -350,7 +360,6 @@ export default class dSyncPay {
350
360
  error: error.response || error.message
351
361
  });
352
362
 
353
- // cleanup cache after verify
354
363
  this.parent.metadataCache.delete(orderId);
355
364
 
356
365
  throw error;
@@ -501,10 +510,8 @@ export default class dSyncPay {
501
510
  rawResponse: response
502
511
  };
503
512
 
504
- // save metadata in cache
505
513
  this.parent.metadataCache.set(response.id, metadata);
506
514
 
507
- // auto cleanup after 1 hour if never verified
508
515
  setTimeout(() => {
509
516
  this.parent.metadataCache.delete(response.id);
510
517
  }, 60 * 60 * 1000);
@@ -534,7 +541,6 @@ export default class dSyncPay {
534
541
  }
535
542
  );
536
543
 
537
- // get metadata from cache
538
544
  const metadata = this.parent.metadataCache.get(subscriptionId) || {};
539
545
 
540
546
  const result = {
@@ -554,7 +560,6 @@ export default class dSyncPay {
554
560
  await this.parent.emit('onSubscriptionCancelled', result);
555
561
  }
556
562
 
557
- // cleanup cache after verify
558
563
  this.parent.metadataCache.delete(subscriptionId);
559
564
 
560
565
  return result;
@@ -566,7 +571,6 @@ export default class dSyncPay {
566
571
  error: error.response || error.message
567
572
  });
568
573
 
569
- // cleanup cache after verify
570
574
  this.parent.metadataCache.delete(subscriptionId);
571
575
 
572
576
  throw error;
@@ -585,11 +589,10 @@ export default class dSyncPay {
585
589
  "Content-Type": "application/json",
586
590
  "Authorization": `Bearer ${accessToken}`
587
591
  },
588
- body: {reason}
592
+ body: { reason }
589
593
  }
590
594
  );
591
595
 
592
- // get metadata from cache
593
596
  const metadata = this.parent.metadataCache.get(subscriptionId) || {};
594
597
 
595
598
  const result = {
@@ -601,7 +604,6 @@ export default class dSyncPay {
601
604
  metadata
602
605
  };
603
606
 
604
- // cleanup cache
605
607
  this.parent.metadataCache.delete(subscriptionId);
606
608
 
607
609
  await this.parent.emit('onSubscriptionCancelled', result);
@@ -614,7 +616,6 @@ export default class dSyncPay {
614
616
  error: error.response || error.message
615
617
  });
616
618
 
617
- // cleanup cache on error too
618
619
  this.parent.metadataCache.delete(subscriptionId);
619
620
 
620
621
  throw error;
@@ -755,23 +756,28 @@ export default class dSyncPay {
755
756
 
756
757
  registerRedirectRoutes() {
757
758
  this.app.get(this.redirects.success, (req, res) => {
758
- 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}`);
759
761
  });
760
762
 
761
763
  this.app.get(this.redirects.error, (req, res) => {
762
- 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}`);
763
766
  });
764
767
 
765
768
  this.app.get(this.redirects.cancelled, (req, res) => {
766
- res.send('payment cancelled.');
769
+ const query = new URLSearchParams({ ...req.query, status: 'cancelled' }).toString();
770
+ return res.redirect(`${this.basePath}/payment-status.html?${query}`);
767
771
  });
768
772
 
769
773
  this.app.get(this.redirects.subscriptionSuccess, (req, res) => {
770
- 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}`);
771
776
  });
772
777
 
773
778
  this.app.get(this.redirects.subscriptionError, (req, res) => {
774
- 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}`);
775
781
  });
776
782
  }
777
783
 
@@ -780,12 +786,18 @@ export default class dSyncPay {
780
786
  this.app.get(`${basePath}/paypal/verify`, async (req, res) => {
781
787
  try {
782
788
  const orderId = req.query.token;
783
- if (!orderId) return res.status(400).json({ok: false, error: 'missing_token'});
789
+ if (!orderId) return res.status(400).json({ ok: false, error: 'missing_token' });
784
790
 
785
791
  const result = await this.paypal.verifyOrder(orderId);
786
792
 
787
793
  if (result.status === 'COMPLETED') {
788
- 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}`);
789
801
  } else {
790
802
  return res.redirect(this.redirects.error);
791
803
  }
@@ -797,12 +809,16 @@ export default class dSyncPay {
797
809
  this.app.get(`${basePath}/paypal/subscription/verify`, async (req, res) => {
798
810
  try {
799
811
  const subscriptionId = req.query.subscription_id;
800
- if (!subscriptionId) return res.status(400).json({ok: false, error: 'missing_subscription_id'});
812
+ if (!subscriptionId) return res.status(400).json({ ok: false, error: 'missing_subscription_id' });
801
813
 
802
814
  const result = await this.paypal.verifySubscription(subscriptionId);
803
815
 
804
816
  if (result.status === 'ACTIVE') {
805
- 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}`);
806
822
  } else {
807
823
  return res.redirect(this.redirects.subscriptionError);
808
824
  }
@@ -826,12 +842,18 @@ export default class dSyncPay {
826
842
  this.app.get(`${basePath}/coinbase/verify`, async (req, res) => {
827
843
  try {
828
844
  const chargeCode = req.query.code;
829
- if (!chargeCode) return res.status(400).json({ok: false, error: 'missing_code'});
845
+ if (!chargeCode) return res.status(400).json({ ok: false, error: 'missing_code' });
830
846
 
831
847
  const result = await this.coinbase.verifyCharge(chargeCode);
832
848
 
833
849
  if (result.status === 'COMPLETED') {
834
- 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}`);
835
857
  } else {
836
858
  return res.redirect(this.redirects.error);
837
859
  }
@@ -850,7 +872,7 @@ export default class dSyncPay {
850
872
  this.coinbase.config.webhookSecret
851
873
  );
852
874
 
853
- if (!isValid) return res.status(401).json({ok: false, error: 'invalid_signature'});
875
+ if (!isValid) return res.status(401).json({ ok: false, error: 'invalid_signature' });
854
876
 
855
877
  const event = req.body;
856
878
 
@@ -859,9 +881,9 @@ export default class dSyncPay {
859
881
  await this.coinbase.verifyCharge(chargeId);
860
882
  }
861
883
 
862
- res.status(200).json({ok: true});
884
+ res.status(200).json({ ok: true });
863
885
  } catch (error) {
864
- res.status(500).json({ok: false, error: 'webhook_error'});
886
+ res.status(500).json({ ok: false, error: 'webhook_error' });
865
887
  }
866
888
  });
867
889
  }
@@ -869,4 +891,4 @@ export default class dSyncPay {
869
891
 
870
892
  return this;
871
893
  }
872
- }
894
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hackthedev/dsync-pay",
3
- "version": "1.0.6",
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>