@paypercut/checkout-js 1.0.0

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.
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Supported locales for Paypercut Checkout
3
+ *
4
+ * @remarks
5
+ * Single source of truth for all supported locale codes.
6
+ *
7
+ * Supported languages:
8
+ * - Bulgarian: 'bg', 'bg-BG'
9
+ * - English: 'en', 'en-GB'
10
+ * - Greek: 'el', 'el-GR'
11
+ * - Romanian: 'ro', 'ro-RO'
12
+ * - Croatian: 'hr', 'hr-HR'
13
+ * - Polish: 'pl', 'pl-PL'
14
+ * - Czech: 'cs', 'cs-CZ'
15
+ * - Slovenian: 'sl', 'sl-SI'
16
+ * - Slovak: 'sk', 'sk-SK'
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * const checkout = PaypercutCheckout({
21
+ * locale: 'bg' // or 'bg-BG', 'en', 'en-GB', etc.
22
+ * });
23
+ * ```
24
+ */
25
+ declare const LOCALES: readonly ["bg", "bg-BG", "en", "en-GB", "el", "el-GR", "ro", "ro-RO", "hr", "hr-HR", "pl", "pl-PL", "cs", "cs-CZ", "sl", "sl-SI", "sk", "sk-SK"];
26
+ /**
27
+ * Locale type - union of all supported locale codes plus 'auto'
28
+ * @example
29
+ * ```typescript
30
+ * const locale: Locale = 'bg';
31
+ * const autoLocale: Locale = 'auto';
32
+ * ```
33
+ */
34
+ type Locale = typeof LOCALES[number] | 'auto';
35
+
36
+ /**
37
+ * UI mode for checkout
38
+ */
39
+ declare enum UIMode {
40
+ /** Custom UI mode - merchant provides their own submit button */
41
+ CUSTOM = "custom",
42
+ /** Embedded mode - checkout embedded in merchant page */
43
+ EMBEDDED = "embedded",
44
+ /** Hosted mode - full-page checkout experience */
45
+ HOSTED = "hosted"
46
+ }
47
+ /**
48
+ * Payment method types
49
+ */
50
+ declare enum PaymentMethod {
51
+ /** Card payment (credit/debit) */
52
+ CARD = "card"
53
+ }
54
+ /**
55
+ * Wallet options for digital wallets
56
+ */
57
+ declare enum WalletOption {
58
+ /** Apple Pay */
59
+ APPLE_PAY = "apple_pay",
60
+ /** Google Pay */
61
+ GOOGLE_PAY = "google_pay"
62
+ }
63
+ /**
64
+ * Configuration options for PaypercutCheckout
65
+ */
66
+ interface PaypercutCheckoutOptions {
67
+ /** Checkout session identifier (e.g., 'CHK_12345') */
68
+ id: string;
69
+ /** CSS selector or HTMLElement where iframe mounts (required) */
70
+ containerId: string | HTMLElement;
71
+ /**
72
+ * Optional: Custom hosted checkout URL (only available in internal builds)
73
+ * Production builds will ignore this option for security
74
+ */
75
+ hostedCheckoutUrl?: string;
76
+ /**
77
+ * Optional: Locale for checkout UI
78
+ * @default 'en'
79
+ */
80
+ locale?: Locale | string;
81
+ /**
82
+ * Optional: UI mode for checkout
83
+ */
84
+ ui_mode?: UIMode | `${UIMode}`;
85
+ /**
86
+ * Optional: Payment methods to enable
87
+ * @default ['card']
88
+ */
89
+ payment_methods?: (PaymentMethod | `${PaymentMethod}`)[];
90
+ /**
91
+ * Optional: Digital wallet options
92
+ * Can include both or just one
93
+ */
94
+ wallet_options?: (WalletOption | `${WalletOption}`)[];
95
+ /**
96
+ * Optional: Show only the payment form without header/footer
97
+ * @default false
98
+ */
99
+ form_only?: boolean;
100
+ }
101
+ /**
102
+ * Event names that can be emitted by the checkout
103
+ */
104
+ type EventName = 'loaded' | 'success' | 'error';
105
+ /**
106
+ * Checkout instance interface
107
+ */
108
+ interface CheckoutInstance {
109
+ /** Mount and render the iframe into the container */
110
+ render(): void;
111
+ /** Submit payment - sends message to hosted checkout to confirm payment */
112
+ submit(): void;
113
+ /** Destroy instance and cleanup all listeners */
114
+ destroy(): void;
115
+ /** Subscribe to events. Returns unsubscribe function */
116
+ on(event: EventName, handler: (...args: any[]) => void): () => void;
117
+ /** Subscribe to event that auto-unsubscribes after first emission */
118
+ once(event: EventName, handler: (...args: any[]) => void): () => void;
119
+ /** Unsubscribe from events */
120
+ off(event: EventName, handler: (...args: any[]) => void): void;
121
+ /** Check if checkout is currently mounted */
122
+ isMounted(): boolean;
123
+ }
124
+ /**
125
+ * PaypercutCheckout static interface (callable and constructable)
126
+ */
127
+ interface PaypercutCheckoutStatic {
128
+ /** Callable factory function */
129
+ (options: PaypercutCheckoutOptions): CheckoutInstance;
130
+ /** Constructor support */
131
+ new (options: PaypercutCheckoutOptions): CheckoutInstance;
132
+ /** SDK version */
133
+ version: string;
134
+ }
135
+
136
+ /**
137
+ * Factory function that works both as callable and constructable.
138
+ * Always returns a new CheckoutImpl instance - no prototype manipulation needed.
139
+ */
140
+ declare const PaypercutCheckout: PaypercutCheckoutStatic;
141
+
142
+ export { LOCALES, PaymentMethod, PaypercutCheckout, UIMode, WalletOption, PaypercutCheckout as default };
143
+ export type { CheckoutInstance, EventName, Locale, PaypercutCheckoutOptions, PaypercutCheckoutStatic };
package/dist/index.mjs ADDED
@@ -0,0 +1,509 @@
1
+ /**
2
+ * Simple event emitter for handling checkout events
3
+ * Supports subscribing, unsubscribing, and emitting events
4
+ */
5
+ class Emitter {
6
+ constructor() {
7
+ this.handlers = new Map();
8
+ }
9
+ /**
10
+ * Subscribe to an event
11
+ * @param event - Event name to listen to
12
+ * @param handler - Callback function to execute when event is emitted
13
+ * @returns Unsubscribe function
14
+ */
15
+ on(event, handler) {
16
+ if (!this.handlers.has(event)) {
17
+ this.handlers.set(event, new Set());
18
+ }
19
+ this.handlers.get(event).add(handler);
20
+ // Return unsubscribe function
21
+ return () => this.off(event, handler);
22
+ }
23
+ /**
24
+ * Subscribe to an event that auto-unsubscribes after first emission
25
+ *
26
+ * Common use case: waiting for 'loaded' event or handling first 'success'.
27
+ * Without this helper, developers would need to manually unsubscribe inside
28
+ * their handler, which is error-prone and leads to memory leaks if forgotten.
29
+ *
30
+ * @param event - Event name to listen to
31
+ * @param handler - Callback function to execute when event is emitted
32
+ * @returns Unsubscribe function
33
+ */
34
+ once(event, handler) {
35
+ const wrappedHandler = (...args) => {
36
+ handler(...args);
37
+ this.off(event, wrappedHandler);
38
+ };
39
+ return this.on(event, wrappedHandler);
40
+ }
41
+ /**
42
+ * Unsubscribe from an event
43
+ * @param event - Event name to stop listening to
44
+ * @param handler - Callback function to remove
45
+ */
46
+ off(event, handler) {
47
+ this.handlers.get(event)?.delete(handler);
48
+ }
49
+ /**
50
+ * Emit an event with optional arguments
51
+ * @param event - Event name to emit
52
+ * @param args - Arguments to pass to event handlers
53
+ */
54
+ emit(event, ...args) {
55
+ this.handlers.get(event)?.forEach(h => {
56
+ try {
57
+ h(...args);
58
+ }
59
+ catch (err) {
60
+ console.error(`[Emitter] Error in ${event} handler:`, err);
61
+ }
62
+ });
63
+ }
64
+ /**
65
+ * Clear all event handlers
66
+ */
67
+ clear() {
68
+ this.handlers.clear();
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Build-time configuration
74
+ *
75
+ * This file is processed at build time with different values for:
76
+ * - Production build (for merchants)
77
+ * - Internal build (for team development)
78
+ */
79
+ /**
80
+ * Production configuration (for merchants)
81
+ */
82
+ /**
83
+ * Internal configuration (for team development)
84
+ */
85
+ const internalConfig = {
86
+ version: "1.0.0",
87
+ defaultCheckoutOrigin: 'https://buy.paypercut.io',
88
+ allowedOrigins: [
89
+ 'https://buy.paypercut.io',
90
+ 'http://localhost:3000',
91
+ 'http://localhost:3001',
92
+ 'http://127.0.0.1:3000',
93
+ 'http://127.0.0.1:3001'
94
+ ],
95
+ allowOriginOverride: true // ← Team CAN override for testing
96
+ };
97
+ /**
98
+ * Active configuration (selected at build time)
99
+ */
100
+ const config = internalConfig
101
+ ;
102
+ /**
103
+ * Helper to check if origin is allowed
104
+ */
105
+ function isOriginAllowed(origin) {
106
+ return config.allowedOrigins.includes(origin);
107
+ }
108
+ /**
109
+ * Get the checkout origin (with optional override for internal builds)
110
+ */
111
+ function getCheckoutOrigin(override) {
112
+ // Only allow override in internal builds
113
+ if (override && config.allowOriginOverride) {
114
+ return override;
115
+ }
116
+ return config.defaultCheckoutOrigin;
117
+ }
118
+
119
+ /**
120
+ * Supported locales for Paypercut Checkout
121
+ *
122
+ * @remarks
123
+ * Single source of truth for all supported locale codes.
124
+ *
125
+ * Supported languages:
126
+ * - Bulgarian: 'bg', 'bg-BG'
127
+ * - English: 'en', 'en-GB'
128
+ * - Greek: 'el', 'el-GR'
129
+ * - Romanian: 'ro', 'ro-RO'
130
+ * - Croatian: 'hr', 'hr-HR'
131
+ * - Polish: 'pl', 'pl-PL'
132
+ * - Czech: 'cs', 'cs-CZ'
133
+ * - Slovenian: 'sl', 'sl-SI'
134
+ * - Slovak: 'sk', 'sk-SK'
135
+ *
136
+ * @example
137
+ * ```typescript
138
+ * const checkout = PaypercutCheckout({
139
+ * locale: 'bg' // or 'bg-BG', 'en', 'en-GB', etc.
140
+ * });
141
+ * ```
142
+ */
143
+ const LOCALES = [
144
+ 'bg', 'bg-BG',
145
+ 'en', 'en-GB',
146
+ 'el', 'el-GR',
147
+ 'ro', 'ro-RO',
148
+ 'hr', 'hr-HR',
149
+ 'pl', 'pl-PL',
150
+ 'cs', 'cs-CZ',
151
+ 'sl', 'sl-SI',
152
+ 'sk', 'sk-SK',
153
+ ];
154
+ /**
155
+ * Fast runtime check using Set for O(1) lookup
156
+ * @internal
157
+ */
158
+ const LOCALE_SET = new Set(LOCALES);
159
+ /**
160
+ * Normalize and validate locale
161
+ *
162
+ * @param locale - Locale code to normalize
163
+ * @returns Normalized locale code or 'en' as fallback
164
+ *
165
+ * @remarks
166
+ * - 'auto' or empty → 'en'
167
+ * - Unsupported locale → 'en' with console warning
168
+ * - Supported locale → original value
169
+ *
170
+ * @example
171
+ * ```typescript
172
+ * normalizeLocale('auto') // → 'en'
173
+ * normalizeLocale('bg') // → 'bg'
174
+ * normalizeLocale('de') // → 'en' (with warning)
175
+ * ```
176
+ */
177
+ function normalizeLocale(locale) {
178
+ if (!locale || locale === 'auto')
179
+ return 'en';
180
+ if (LOCALE_SET.has(locale))
181
+ return locale;
182
+ console.warn(`[PaypercutCheckout] Locale "${locale}" is not supported. Falling back to "en".`);
183
+ return 'en';
184
+ }
185
+
186
+ /**
187
+ * UI mode for checkout
188
+ */
189
+ var UIMode;
190
+ (function (UIMode) {
191
+ /** Custom UI mode - merchant provides their own submit button */
192
+ UIMode["CUSTOM"] = "custom";
193
+ /** Embedded mode - checkout embedded in merchant page */
194
+ UIMode["EMBEDDED"] = "embedded";
195
+ /** Hosted mode - full-page checkout experience */
196
+ UIMode["HOSTED"] = "hosted";
197
+ })(UIMode || (UIMode = {}));
198
+ /**
199
+ * Type guard for UIMode
200
+ */
201
+ function isValidUIMode(value) {
202
+ return Object.values(UIMode).includes(value);
203
+ }
204
+ /**
205
+ * Payment method types
206
+ */
207
+ var PaymentMethod;
208
+ (function (PaymentMethod) {
209
+ /** Card payment (credit/debit) */
210
+ PaymentMethod["CARD"] = "card";
211
+ })(PaymentMethod || (PaymentMethod = {}));
212
+ /**
213
+ * Type guard for PaymentMethod
214
+ */
215
+ function isValidPaymentMethod(value) {
216
+ return Object.values(PaymentMethod).includes(value);
217
+ }
218
+ /**
219
+ * Wallet options for digital wallets
220
+ */
221
+ var WalletOption;
222
+ (function (WalletOption) {
223
+ /** Apple Pay */
224
+ WalletOption["APPLE_PAY"] = "apple_pay";
225
+ /** Google Pay */
226
+ WalletOption["GOOGLE_PAY"] = "google_pay";
227
+ })(WalletOption || (WalletOption = {}));
228
+ /**
229
+ * Type guard for WalletOption
230
+ */
231
+ function isValidWalletOption(value) {
232
+ return Object.values(WalletOption).includes(value);
233
+ }
234
+ /**
235
+ * Validate payment methods array
236
+ */
237
+ function validatePaymentMethods(methods) {
238
+ const validated = [];
239
+ for (const method of methods) {
240
+ if (isValidPaymentMethod(method)) {
241
+ validated.push(method);
242
+ }
243
+ else {
244
+ console.warn(`[PaypercutCheckout] Invalid payment method: "${method}". Skipping.`);
245
+ }
246
+ }
247
+ // Default to card if no valid methods
248
+ if (validated.length === 0) {
249
+ console.warn('[PaypercutCheckout] No valid payment methods provided. Defaulting to "card".');
250
+ validated.push(PaymentMethod.CARD);
251
+ }
252
+ return validated;
253
+ }
254
+ /**
255
+ * Validate wallet options array
256
+ */
257
+ function validateWalletOptions(options) {
258
+ const validated = [];
259
+ for (const option of options) {
260
+ if (isValidWalletOption(option)) {
261
+ validated.push(option);
262
+ }
263
+ else {
264
+ console.warn(`[PaypercutCheckout] Invalid wallet option: "${option}". Skipping.`);
265
+ }
266
+ }
267
+ return validated;
268
+ }
269
+ /**
270
+ * Validate UI mode
271
+ */
272
+ function validateUIMode(mode) {
273
+ if (!mode) {
274
+ return undefined;
275
+ }
276
+ if (isValidUIMode(mode)) {
277
+ return mode;
278
+ }
279
+ console.warn(`[PaypercutCheckout] Invalid ui_mode: "${mode}". Valid options: ${Object.values(UIMode).join(', ')}`);
280
+ return undefined;
281
+ }
282
+
283
+ /**
284
+ * Internal implementation of CheckoutInstance
285
+ */
286
+ class CheckoutImpl {
287
+ constructor(options) {
288
+ this.options = options;
289
+ this.emitter = new Emitter();
290
+ this.mounted = false;
291
+ this.destroyed = false;
292
+ this.iframe = null;
293
+ // Bind message handler
294
+ this.messageHandler = this.onMessage.bind(this);
295
+ window.addEventListener('message', this.messageHandler);
296
+ }
297
+ /**
298
+ * Build the iframe source URL with query parameters
299
+ */
300
+ buildSrc() {
301
+ const baseUrl = getCheckoutOrigin(this.options.hostedCheckoutUrl);
302
+ const url = new URL(`/c/${this.options.id}`, baseUrl);
303
+ // Add locale parameter
304
+ if (this.options.locale) {
305
+ const normalizedLocale = normalizeLocale(this.options.locale);
306
+ url.searchParams.set('locale', normalizedLocale);
307
+ }
308
+ // Add ui_mode parameter
309
+ if (this.options.ui_mode) {
310
+ const validatedUIMode = validateUIMode(this.options.ui_mode);
311
+ if (validatedUIMode) {
312
+ url.searchParams.set('ui_mode', validatedUIMode);
313
+ }
314
+ }
315
+ // Add payment_methods parameters (repeated for each method)
316
+ if (this.options.payment_methods && this.options.payment_methods.length > 0) {
317
+ const validatedMethods = validatePaymentMethods(this.options.payment_methods);
318
+ validatedMethods.forEach(method => {
319
+ url.searchParams.append('payment_methods', method);
320
+ });
321
+ }
322
+ // Add wallet_options parameters (repeated for each wallet)
323
+ if (this.options.wallet_options && this.options.wallet_options.length > 0) {
324
+ const validatedWallets = validateWalletOptions(this.options.wallet_options);
325
+ validatedWallets.forEach(wallet => {
326
+ url.searchParams.append('wallet_options', wallet);
327
+ });
328
+ }
329
+ console.log('this.options', this.options);
330
+ if (this.options.form_only !== undefined) {
331
+ url.searchParams.set('form_only', String(this.options.form_only));
332
+ }
333
+ return url.toString();
334
+ }
335
+ /**
336
+ * Get the container element from selector or HTMLElement
337
+ */
338
+ getContainer() {
339
+ const container = typeof this.options.containerId === 'string'
340
+ ? document.querySelector(this.options.containerId)
341
+ : this.options.containerId;
342
+ if (!container) {
343
+ throw new Error(`Container not found: ${this.options.containerId}`);
344
+ }
345
+ return container;
346
+ }
347
+ /**
348
+ * Handle incoming postMessage events from iframe
349
+ */
350
+ onMessage(evt) {
351
+ // Validate origin against allowed origins (build-time whitelist)
352
+ if (!isOriginAllowed(evt.origin)) {
353
+ console.warn('[PaypercutCheckout] Rejected message from unauthorized origin:', evt.origin, 'Allowed:', config.allowedOrigins);
354
+ return;
355
+ }
356
+ // Validate structure
357
+ const data = evt.data;
358
+ if (!data || typeof data !== 'object' || !('type' in data)) {
359
+ return;
360
+ }
361
+ // Filter messages by checkout session ID to prevent cross-instance message handling
362
+ // This ensures each checkout instance only processes its own messages
363
+ if (data.checkoutId && data.checkoutId !== this.options.id) {
364
+ return; // Message is for a different checkout instance
365
+ }
366
+ // Handle specific events
367
+ switch (data.type) {
368
+ case 'CHECKOUT_LOADED':
369
+ this.emitter.emit('loaded');
370
+ break;
371
+ case 'CHECKOUT_SUCCESS':
372
+ this.emitter.emit('success');
373
+ break;
374
+ case 'CHECKOUT_ERROR':
375
+ this.emitter.emit('error');
376
+ break;
377
+ }
378
+ }
379
+ /**
380
+ * Render the checkout iframe into the container
381
+ */
382
+ render() {
383
+ if (this.mounted) {
384
+ console.warn('[PaypercutCheckout] Already mounted');
385
+ return;
386
+ }
387
+ try {
388
+ const container = this.getContainer();
389
+ // Create iframe
390
+ this.iframe = document.createElement('iframe');
391
+ this.iframe.id = 'paypercut-checkout-iframe';
392
+ this.iframe.src = this.buildSrc();
393
+ this.iframe.allow = 'payment *; clipboard-write';
394
+ this.iframe.setAttribute('frameborder', '0');
395
+ // Apply default styles - just construct URL and assign to iframe
396
+ Object.assign(this.iframe.style, {
397
+ width: '100%',
398
+ height: '100%',
399
+ border: 'none',
400
+ display: 'block'
401
+ });
402
+ // Listen for iframe load event (fallback)
403
+ // This ensures 'loaded' event is always emitted even if hosted checkout
404
+ // doesn't send CHECKOUT_LOADED message
405
+ /*this.iframe.addEventListener('load', () => {
406
+ this.emitter.emit('loaded');
407
+ });*/
408
+ container.appendChild(this.iframe);
409
+ this.mounted = true;
410
+ }
411
+ catch (err) {
412
+ console.error('[PaypercutCheckout] Failed to render:', err);
413
+ throw err;
414
+ }
415
+ }
416
+ /**
417
+ * Submit payment - sends message to hosted checkout to confirm payment
418
+ */
419
+ submit() {
420
+ if (!this.mounted) {
421
+ console.warn('[PaypercutCheckout] Cannot submit: checkout not mounted');
422
+ return;
423
+ }
424
+ try {
425
+ this.postToIframe({
426
+ type: 'START_PROCESSING',
427
+ checkoutId: this.options.id,
428
+ });
429
+ console.log('[PaypercutCheckout] Submit payment message sent');
430
+ }
431
+ catch (err) {
432
+ console.error('[PaypercutCheckout] Failed to submit payment:', err);
433
+ throw err;
434
+ }
435
+ }
436
+ /**
437
+ * Send message to iframe - used for submit and other commands
438
+ */
439
+ postToIframe(message) {
440
+ if (!this.iframe?.contentWindow) {
441
+ console.error('[PaypercutCheckout] Cannot post message: iframe not mounted');
442
+ return;
443
+ }
444
+ const checkoutOrigin = getCheckoutOrigin(this.options.hostedCheckoutUrl);
445
+ this.iframe.contentWindow.postMessage(message, checkoutOrigin);
446
+ }
447
+ /**
448
+ * Destroy the checkout instance and cleanup (idempotent)
449
+ */
450
+ destroy() {
451
+ // Early return if already destroyed
452
+ if (this.destroyed) {
453
+ return;
454
+ }
455
+ // Remove iframe from DOM
456
+ if (this.iframe) {
457
+ try {
458
+ this.iframe.remove();
459
+ this.iframe = null;
460
+ }
461
+ catch (err) {
462
+ console.error('[PaypercutCheckout] Error removing iframe:', err);
463
+ }
464
+ }
465
+ // Only set mounted = false after successful DOM removal
466
+ this.mounted = false;
467
+ this.destroyed = true;
468
+ // Cleanup event listeners
469
+ window.removeEventListener('message', this.messageHandler);
470
+ this.emitter.clear();
471
+ }
472
+ /**
473
+ * Subscribe to an event
474
+ */
475
+ on(event, handler) {
476
+ return this.emitter.on(event, handler);
477
+ }
478
+ /**
479
+ * Subscribe to event that auto-unsubscribes after first emission
480
+ */
481
+ once(event, handler) {
482
+ return this.emitter.once(event, handler);
483
+ }
484
+ /**
485
+ * Unsubscribe from an event
486
+ */
487
+ off(event, handler) {
488
+ this.emitter.off(event, handler);
489
+ }
490
+ /**
491
+ * Check if checkout is currently mounted
492
+ */
493
+ isMounted() {
494
+ return this.mounted;
495
+ }
496
+ }
497
+ /**
498
+ * Factory function that works both as callable and constructable.
499
+ * Always returns a new CheckoutImpl instance - no prototype manipulation needed.
500
+ */
501
+ const PaypercutCheckout = function (options) {
502
+ // Always return a new instance, regardless of how it's called
503
+ return new CheckoutImpl(options);
504
+ };
505
+ // Add static version property
506
+ PaypercutCheckout.version = config.version;
507
+
508
+ export { LOCALES, PaymentMethod, PaypercutCheckout, UIMode, WalletOption, PaypercutCheckout as default };
509
+ //# sourceMappingURL=index.mjs.map