@paypercut/checkout-js 1.0.10 → 1.0.12

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
@@ -54,12 +54,12 @@ pnpm add @paypercut/checkout-js
54
54
 
55
55
  ```html
56
56
  <!-- jsDelivr (recommended with SRI) -->
57
- <script src="https://cdn.jsdelivr.net/npm/@paypercut/checkout-js@1.0.10/dist/paypercut-checkout.iife.min.js"
57
+ <script src="https://cdn.jsdelivr.net/npm/@paypercut/checkout-js@1.0.12/dist/paypercut-checkout.iife.min.js"
58
58
  integrity="sha384-..."
59
59
  crossorigin="anonymous"></script>
60
60
 
61
61
  <!-- or UNPKG -->
62
- <script src="https://unpkg.com/@paypercut/checkout-js@1.0.10/dist/paypercut-checkout.iife.min.js"></script>
62
+ <script src="https://unpkg.com/@paypercut/checkout-js@1.0.12/dist/paypercut-checkout.iife.min.js"></script>
63
63
  ```
64
64
 
65
65
  ---
@@ -118,12 +118,18 @@ Creates a new checkout instance.
118
118
 
119
119
  #### Options
120
120
 
121
- | Option | Type | Required | Description |
122
- |--------|------|----------|-------------|
123
- | `id` | `string` | Yes | Checkout session identifier |
124
- | `containerId` | `string \| HTMLElement` | Yes | CSS selector or element where iframe mounts. |
121
+ | Option | Type | Required | Default | Description |
122
+ |--------|------|----------|---------|-------------|
123
+ | `id` | `string` | Yes | — | Checkout session identifier (e.g., `CHK_12345`) |
124
+ | `containerId` | `string \| HTMLElement` | Yes | — | CSS selector or element where iframe mounts |
125
+ | `locale` | `string` | No | `'auto'` | Locale for checkout UI. Options: `'auto'`, `'en'`, `'en-GB'`, `'bg'`, `'bg-BG'` |
126
+ | `ui_mode` | `'hosted' \| 'embedded'` | No | `'embedded'` | UI mode for checkout display |
127
+ | `wallet_options` | `string[]` | No | `['apple_pay', 'google_pay']` | Digital wallet options. Pass `[]` to disable all wallets |
128
+ | `form_only` | `boolean` | No | `false` | Show only payment form (no Pay Now button - use external button with `submit()`) |
125
129
 
126
- #### Example
130
+ #### Examples
131
+
132
+ **Basic initialization:**
127
133
 
128
134
  ```typescript
129
135
  const checkout = PaypercutCheckout({
@@ -132,6 +138,54 @@ const checkout = PaypercutCheckout({
132
138
  });
133
139
  ```
134
140
 
141
+ **With all options:**
142
+
143
+ ```typescript
144
+ const checkout = PaypercutCheckout({
145
+ id: 'CHK_12345',
146
+ containerId: '#checkout-container',
147
+ locale: 'en', // 'auto' | 'en' | 'en-GB' | 'bg' | 'bg-BG'
148
+ ui_mode: 'embedded', // 'hosted' | 'embedded'
149
+ wallet_options: ['apple_pay', 'google_pay'], // Can be empty array [] or contain one/both options
150
+ form_only: false // Set true to hide Pay Now button (use external button)
151
+ });
152
+ ```
153
+
154
+ **Disable wallet payments:**
155
+
156
+ ```typescript
157
+ const checkout = PaypercutCheckout({
158
+ id: 'CHK_12345',
159
+ containerId: '#checkout-container',
160
+ wallet_options: [] // No Apple Pay or Google Pay buttons
161
+ });
162
+ ```
163
+
164
+ **Only Apple Pay:**
165
+
166
+ ```typescript
167
+ const checkout = PaypercutCheckout({
168
+ id: 'CHK_12345',
169
+ containerId: '#checkout-container',
170
+ wallet_options: ['apple_pay'] // Only Apple Pay, no Google Pay
171
+ });
172
+ ```
173
+
174
+ **Form-only mode (external submit button):**
175
+
176
+ ```typescript
177
+ const checkout = PaypercutCheckout({
178
+ id: 'CHK_12345',
179
+ containerId: '#checkout-container',
180
+ form_only: true // No Pay Now button inside checkout
181
+ });
182
+
183
+ // Use your own button to trigger payment
184
+ document.getElementById('my-pay-button').addEventListener('click', () => {
185
+ checkout.submit();
186
+ });
187
+ ```
188
+
135
189
  ### Instance Methods
136
190
 
137
191
  #### `render()`
@@ -197,97 +251,50 @@ if (checkout.isMounted()) {
197
251
 
198
252
  ## Events
199
253
 
200
- Subscribe to events using the `on()` method:
201
-
202
- | Event | Description | Payload |
203
- |-------|-------------|---------|
204
- | `loaded` | Checkout iframe has finished loading | `void` |
205
- | `success` | Payment completed successfully | `PaymentSuccessPayload` |
206
- | `error` | Terminal failure (tokenize, confirm, or 3DS) | `ApiErrorPayload` (see details below) |
207
- | `expired` | Checkout session expired | `void` |
208
- | `threeds_started` | 3DS challenge flow started; SDK modal opened | `object` (flow context) |
209
- | `threeds_complete` | 3DS challenge completed | `object` (auth result context) |
210
- | `threeds_canceled` | 3DS challenge canceled by user | `object` (flow context) |
211
- | `threeds_error` | 3DS challenge error | `ApiErrorPayload` (if provided) or `object` |
212
-
213
-
214
- #### Success payload example
215
-
216
- ```ts
217
- checkout.on('success', (payload) => {
218
- // payload: PaymentSuccessPayload
219
- // Example:
220
- // {
221
- // payment_method: {
222
- // brand: 'visa',
223
- // last4: '4242',
224
- // exp_month: 12,
225
- // exp_year: 2030,
226
- // }
227
- // }
228
- console.log('PM last4', payload.payment_method.last4);
229
- });
230
- ```
254
+ Subscribe to events using the `on()` method. You can use string event names or the `SdkEvent` enum.
231
255
 
232
- #### Error payloads overview
256
+ ### Event Reference
233
257
 
234
- ```ts
235
- checkout.on('error', (err) => {
236
- switch (err.code) {
237
- case 'card_validation_error':
238
- // err.errors[] has field-level issues. Messages are localized.
239
- break;
240
- case 'card_declined':
241
- // Optional err.decline_code and user-friendly err.message
242
- break;
243
- case 'threeds_error':
244
- case 'threeds_authentication_failed':
245
- case 'threeds_canceled':
246
- // 3DS issue. Some servers use status_code 424 with a detailed message.
247
- break;
248
- default:
249
- // Other terminal errors (e.g., authentication_failed, session_expired)
250
- break;
251
- }
252
- });
253
- ```
258
+ | Event | Enum | Description | Payload |
259
+ |-------|------|-------------|---------|
260
+ | `loaded` | `SdkEvent.Loaded` | Checkout iframe has finished loading | `void` |
261
+ | `success` | `SdkEvent.Success` | Payment completed successfully | `PaymentSuccessPayload` |
262
+ | `error` | `SdkEvent.Error` | Terminal failure (tokenize, confirm, or 3DS) | `ApiErrorPayload` |
263
+ | `expired` | `SdkEvent.Expired` | Checkout session expired | `void` |
264
+ | `threeds_started` | `SdkEvent.ThreeDSStarted` | 3DS challenge flow started | `object` |
265
+ | `threeds_complete` | `SdkEvent.ThreeDSComplete` | 3DS challenge completed | `object` |
266
+ | `threeds_canceled` | `SdkEvent.ThreeDSCanceled` | 3DS challenge canceled by user | `object` |
267
+ | `threeds_error` | `SdkEvent.ThreeDSError` | 3DS challenge error | `ApiErrorPayload` or `object` |
254
268
 
255
- ### 3DS Events (enum usage recommended)
269
+ ### Usage Examples
256
270
 
257
- You can subscribe using string event names or the SdkEvent enum:
271
+ **Using string event names:**
258
272
 
259
273
  ```ts
260
- import { PaypercutCheckout, SdkEvent } from '@paypercut/checkout-js';
261
-
262
274
  const checkout = PaypercutCheckout({ id: 'CHK_12345', containerId: '#checkout' });
263
275
 
264
- checkout.on(SdkEvent.ThreeDSStarted, (ctx) => {
265
- // e.g., show your own UI indicator or log ctx
276
+ checkout.on('loaded', () => {
277
+ console.log('Checkout loaded');
266
278
  });
267
279
 
268
- checkout.on(SdkEvent.ThreeDSComplete, (payload) => {
269
- // 3DS completed successfully; Hosted Checkout will continue the flow
280
+ checkout.on('success', (payload) => {
281
+ // PaymentSuccessPayload
282
+ console.log('Payment successful');
283
+ console.log('Card brand:', payload.payment_method.brand);
284
+ console.log('Last 4:', payload.payment_method.last4);
285
+ console.log('Expiry:', payload.payment_method.exp_month + '/' + payload.payment_method.exp_year);
270
286
  });
271
287
 
272
- checkout.on(SdkEvent.ThreeDSCanceled, (payload) => {
273
- // User canceled the 3DS challenge
288
+ checkout.on('error', (err) => {
289
+ console.error('Payment error:', err.code, err.message);
274
290
  });
275
291
 
276
- checkout.on(SdkEvent.ThreeDSError, (err) => {
277
- // Normalized ApiErrorPayload when available; fallback may be a raw object
278
- console.error('3DS error:', err);
292
+ checkout.on('expired', () => {
293
+ console.warn('Checkout session expired');
279
294
  });
280
295
  ```
281
296
 
282
- Payload notes:
283
- - `error`: the SDK forwards a normalized `ApiErrorPayload` when provided by Hosted Checkout.
284
- - `threeds_error`: forwards `payload.error` when available; otherwise the raw message data.
285
- - `threeds_*` non-error events: payload is forwarded as-is from Hosted Checkout (shape may evolve).
286
-
287
-
288
- ### Core events (loaded, success, error, expired)
289
-
290
- Using enums:
297
+ **Using SdkEvent enum (recommended for TypeScript):**
291
298
 
292
299
  ```ts
293
300
  import { PaypercutCheckout, SdkEvent } from '@paypercut/checkout-js';
@@ -303,48 +310,98 @@ checkout.on(SdkEvent.Success, (payload) => {
303
310
  });
304
311
 
305
312
  checkout.on(SdkEvent.Error, (err) => {
306
- // err: ApiErrorPayload
307
313
  console.error('Payment error:', err.code, err.message);
308
314
  });
309
315
 
310
316
  checkout.on(SdkEvent.Expired, () => {
311
317
  console.warn('Checkout session expired');
312
318
  });
313
- ```
314
-
315
- Or with string event names:
316
319
 
317
- ```ts
318
- checkout.on('loaded', () => {});
319
- checkout.on('success', (payload) => {
320
- console.log('Payment successful', payload.payment_method);
320
+ // 3DS events
321
+ checkout.on(SdkEvent.ThreeDSStarted, (ctx) => {
322
+ console.log('3DS challenge started');
321
323
  });
322
- checkout.on('error', (err) => {
323
- console.error('Payment error', err);
324
+
325
+ checkout.on(SdkEvent.ThreeDSComplete, (payload) => {
326
+ console.log('3DS completed');
324
327
  });
325
- checkout.on('expired', () => {});
326
- ```
327
328
 
329
+ checkout.on(SdkEvent.ThreeDSCanceled, (payload) => {
330
+ console.log('3DS canceled by user');
331
+ });
328
332
 
329
- ## Types
333
+ checkout.on(SdkEvent.ThreeDSError, (err) => {
334
+ console.error('3DS error:', err);
335
+ });
336
+ ```
330
337
 
331
- ### Payload types
338
+ ### Success Payload
332
339
 
333
- #### PaymentSuccessPayload
340
+ The `success` event returns a `PaymentSuccessPayload` with the payment method details:
334
341
 
335
342
  ```ts
336
- // Payload for `checkout.on('success', (payload))`
337
343
  type PaymentSuccessPayload = {
338
344
  payment_method: {
339
- brand: string; // e.g., 'visa', 'mastercard'
340
- last4: string;
341
- exp_month: number;
342
- exp_year: number;
345
+ brand: string; // e.g., 'visa', 'mastercard', 'amex'
346
+ last4: string; // Last 4 digits of card number
347
+ exp_month: number; // Expiration month (1-12)
348
+ exp_year: number; // Expiration year (e.g., 2030)
343
349
  };
344
350
  };
345
351
  ```
346
352
 
347
- #### ApiErrorPayload (common shapes)
353
+ **Example:**
354
+
355
+ ```ts
356
+ checkout.on('success', (payload) => {
357
+ // payload:
358
+ // {
359
+ // payment_method: {
360
+ // brand: 'visa',
361
+ // last4: '4242',
362
+ // exp_month: 12,
363
+ // exp_year: 2030
364
+ // }
365
+ // }
366
+
367
+ console.log(`Paid with ${payload.payment_method.brand} ending in ${payload.payment_method.last4}`);
368
+ // Output: "Paid with visa ending in 4242"
369
+ });
370
+ ```
371
+
372
+ ### Error Handling
373
+
374
+ ```ts
375
+ checkout.on('error', (err) => {
376
+ switch (err.code) {
377
+ case 'card_validation_error':
378
+ // err.errors[] has field-level issues. Messages are localized.
379
+ break;
380
+ case 'card_declined':
381
+ // Optional err.decline_code and user-friendly err.message
382
+ break;
383
+ case 'threeds_error':
384
+ case 'threeds_authentication_failed':
385
+ case 'threeds_canceled':
386
+ // 3DS issue. Some servers use status_code 424 with a detailed message.
387
+ break;
388
+ default:
389
+ // Other terminal errors (e.g., authentication_failed, session_expired)
390
+ break;
391
+ }
392
+ });
393
+ ```
394
+
395
+ ### Notes
396
+
397
+ - `error`: the SDK forwards a normalized `ApiErrorPayload` when provided by Hosted Checkout.
398
+ - `threeds_error`: forwards `payload.error` when available; otherwise the raw message data.
399
+ - `threeds_*` non-error events: payload is forwarded as-is from Hosted Checkout (shape may evolve).
400
+
401
+
402
+ ## Types
403
+
404
+ ### ApiErrorPayload
348
405
 
349
406
  The SDK forwards the error payload provided by Hosted Checkout. Common shapes include:
350
407
 
@@ -505,15 +562,19 @@ Tip: Prefer subscribing with the SdkEvent enum for stronger typing.
505
562
  <body>
506
563
  <div id="checkout"></div>
507
564
 
508
- <script src="https://cdn.jsdelivr.net/npm/@paypercut/checkout-js@1.0.10/dist/paypercut-checkout.iife.min.js"></script>
565
+ <script src="https://cdn.jsdelivr.net/npm/@paypercut/checkout-js@1.0.12/dist/paypercut-checkout.iife.min.js"></script>
509
566
  <script>
510
567
  const checkout = PaypercutCheckout({
511
568
  id: 'CHK_12345',
512
- containerId: '#checkout'
569
+ containerId: '#checkout',
570
+ locale: 'en',
571
+ ui_mode: 'embedded',
572
+ wallet_options: ['apple_pay', 'google_pay']
513
573
  });
514
574
 
515
- checkout.on('success', function() {
516
- alert('Payment successful!');
575
+ checkout.on('success', function(payload) {
576
+ // payload.payment_method: { brand, last4, exp_month, exp_year }
577
+ alert('Payment successful with ' + payload.payment_method.brand + ' ending in ' + payload.payment_method.last4);
517
578
  });
518
579
 
519
580
  checkout.on('error', function(error) {
@@ -533,11 +594,17 @@ import { PaypercutCheckout } from '@paypercut/checkout-js';
533
594
 
534
595
  const checkout = PaypercutCheckout({
535
596
  id: 'CHK_12345',
536
- containerId: '#checkout'
597
+ containerId: '#checkout',
598
+ locale: 'auto',
599
+ ui_mode: 'embedded',
600
+ wallet_options: ['apple_pay', 'google_pay'],
601
+ form_only: false
537
602
  });
538
603
 
539
- checkout.on('success', () => {
604
+ checkout.on('success', (payload) => {
605
+ // payload.payment_method: { brand, last4, exp_month, exp_year }
540
606
  console.log('Payment successful');
607
+ console.log(`Paid with ${payload.payment_method.brand} **** ${payload.payment_method.last4}`);
541
608
  // Redirect to success page
542
609
  window.location.href = '/success';
543
610
  });
@@ -557,17 +624,29 @@ checkout.render();
557
624
  import { useEffect, useRef, useState } from 'react';
558
625
  import { PaypercutCheckout, CheckoutInstance } from '@paypercut/checkout-js';
559
626
 
627
+ interface PaymentMethod {
628
+ brand: string;
629
+ last4: string;
630
+ exp_month: number;
631
+ exp_year: number;
632
+ }
633
+
560
634
  export function CheckoutComponent({ checkoutId }: { checkoutId: string }) {
561
635
  const containerRef = useRef<HTMLDivElement>(null);
562
636
  const checkoutRef = useRef<CheckoutInstance | null>(null);
563
637
  const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
638
+ const [paymentMethod, setPaymentMethod] = useState<PaymentMethod | null>(null);
564
639
 
565
640
  useEffect(() => {
566
641
  if (!containerRef.current) return;
567
642
 
568
643
  const checkout = PaypercutCheckout({
569
644
  id: checkoutId,
570
- containerId: containerRef.current
645
+ containerId: containerRef.current,
646
+ locale: 'auto',
647
+ ui_mode: 'embedded',
648
+ wallet_options: ['apple_pay', 'google_pay'],
649
+ form_only: false
571
650
  });
572
651
 
573
652
  checkout.on('loaded', () => {
@@ -575,9 +654,10 @@ export function CheckoutComponent({ checkoutId }: { checkoutId: string }) {
575
654
  console.log('Checkout loaded');
576
655
  });
577
656
 
578
- checkout.on('success', () => {
657
+ checkout.on('success', (payload) => {
579
658
  setStatus('success');
580
- console.log('Payment successful');
659
+ setPaymentMethod(payload.payment_method);
660
+ console.log('Payment successful:', payload.payment_method);
581
661
  });
582
662
 
583
663
  checkout.on('error', (error) => {
@@ -597,7 +677,9 @@ export function CheckoutComponent({ checkoutId }: { checkoutId: string }) {
597
677
  <div>
598
678
  <div ref={containerRef} style={{ width: '100%', height: '600px' }} />
599
679
  {status === 'loading' && <p>Processing payment...</p>}
600
- {status === 'success' && <p>✅ Payment successful!</p>}
680
+ {status === 'success' && paymentMethod && (
681
+ <p>✅ Payment successful with {paymentMethod.brand} ending in {paymentMethod.last4}!</p>
682
+ )}
601
683
  {status === 'error' && <p>❌ Payment failed. Please try again.</p>}
602
684
  </div>
603
685
  );
@@ -701,6 +783,56 @@ export function ModalCheckout({ checkoutId, onClose }: { checkoutId: string; onC
701
783
  }
702
784
  ```
703
785
 
786
+ ## Container Styling
787
+
788
+ The SDK automatically resizes the iframe to match the checkout content height. To ensure proper display, style your container with `max-width` and `max-height` constraints rather than fixed dimensions.
789
+
790
+ ### Recommended Container Styles
791
+
792
+ ```css
793
+ .checkout-container {
794
+ /* Use max-width/max-height to constrain the container */
795
+ max-width: 450px;
796
+ max-height: 600px;
797
+ overflow: hidden;
798
+ overflow-y: auto;
799
+ scroll-behavior: smooth;
800
+ }
801
+
802
+ /* The mount point should be full width with no fixed height */
803
+ #checkout-mount {
804
+ width: 100%;
805
+ /* Height is set dynamically by SDK via resize messages */
806
+ overflow: hidden;
807
+ position: relative;
808
+ }
809
+ ```
810
+
811
+ ### Key Points
812
+
813
+ 1. **Avoid fixed `height` or `min-height`** on the container - the SDK will resize the iframe to fit the checkout content
814
+ 2. **Use `max-height`** if you want to limit the container size and enable scrolling for longer content
815
+ 3. **Use `max-width`** to constrain the container width (recommended: 375px–450px for optimal checkout display)
816
+ 4. **Set `overflow-y: auto`** on the container if using `max-height` to enable scrolling
817
+
818
+ ### Example HTML
819
+
820
+ ```html
821
+ <div class="checkout-container">
822
+ <div id="checkout-mount"></div>
823
+ </div>
824
+ ```
825
+
826
+ ```javascript
827
+ const checkout = PaypercutCheckout({
828
+ id: 'CHK_12345',
829
+ containerId: '#checkout-mount'
830
+ });
831
+ checkout.render();
832
+ ```
833
+
834
+ ---
835
+
704
836
  ## Security
705
837
 
706
838
  ### Origin Validation
package/dist/index.cjs CHANGED
@@ -84,7 +84,7 @@ class Emitter {
84
84
  * Production configuration (for merchants)
85
85
  */
86
86
  const productionConfig = {
87
- version: "1.0.10",
87
+ version: "1.0.12",
88
88
  defaultCheckoutOrigin: 'https://buy.paypercut.io',
89
89
  allowedOrigins: [
90
90
  'https://buy.paypercut.io',
@@ -274,6 +274,7 @@ exports.SdkEvent = void 0;
274
274
  SdkEvent["Success"] = "success";
275
275
  SdkEvent["Error"] = "error";
276
276
  SdkEvent["Expired"] = "expired";
277
+ SdkEvent["Resize"] = "resize";
277
278
  SdkEvent["ThreeDSComplete"] = "threeds_complete";
278
279
  SdkEvent["ThreeDSError"] = "threeds_error";
279
280
  SdkEvent["ThreeDSCanceled"] = "threeds_canceled";
@@ -323,11 +324,18 @@ class CheckoutImpl {
323
324
  });
324
325
  }
325
326
  // Add wallet_options parameters (repeated for each wallet)
326
- if (this.options.wallet_options && this.options.wallet_options.length > 0) {
327
- const validatedWallets = validateWalletOptions(this.options.wallet_options);
328
- validatedWallets.forEach((wallet) => {
329
- url.searchParams.append('wallet_options', wallet);
330
- });
327
+ // If wallet_options is explicitly set (even as empty array), we need to pass it
328
+ if (this.options.wallet_options !== undefined) {
329
+ if (this.options.wallet_options.length === 0) {
330
+ // Explicit empty array means "no wallets" - pass null (coerced to "null" string)
331
+ url.searchParams.set('wallet_options', null);
332
+ }
333
+ else {
334
+ const validatedWallets = validateWalletOptions(this.options.wallet_options);
335
+ validatedWallets.forEach((wallet) => {
336
+ url.searchParams.append('wallet_options', wallet);
337
+ });
338
+ }
331
339
  }
332
340
  if (this.options.appearance?.preset) {
333
341
  url.searchParams.set('preset', this.options.appearance.preset);
@@ -383,6 +391,10 @@ class CheckoutImpl {
383
391
  case 'CHECKOUT_EXPIRED':
384
392
  this.emitter.emit(exports.SdkEvent.Expired);
385
393
  break;
394
+ case 'CHECKOUT_RESIZE':
395
+ this.emitter.emit(exports.SdkEvent.Resize, data.height);
396
+ this.handleResize(data.height);
397
+ break;
386
398
  case 'THREEDS_START_FLOW':
387
399
  this.show3DSModal(data);
388
400
  this.emitter.emit(exports.SdkEvent.ThreeDSStarted);
@@ -475,6 +487,16 @@ class CheckoutImpl {
475
487
  const checkoutOrigin = getCheckoutOrigin(this.options.hostedCheckoutUrl);
476
488
  this.iframe.contentWindow.postMessage(message, checkoutOrigin);
477
489
  }
490
+ /**
491
+ * Handle CHECKOUT_RESIZE message - resize iframe to match content height
492
+ */
493
+ handleResize(height) {
494
+ if (!this.iframe || typeof height !== 'number' || height <= 0) {
495
+ return;
496
+ }
497
+ // Set iframe height to match content
498
+ this.iframe.style.height = `${height}px`;
499
+ }
478
500
  /**
479
501
  * Show 3DS modal with challenge/decoupled flow
480
502
  */