elpaypg 0.0.0-stage → 2.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.
Files changed (90) hide show
  1. package/.env.example +8 -0
  2. package/README.md +176 -2
  3. package/package.json +21 -4
  4. package/public/elpay-checkout.js +769 -0
  5. package/public/elpay-sms-gateway.apk +0 -0
  6. package/public/elpay-sms-gateway.apk.sha256 +1 -0
  7. package/public/elpay.js +785 -0
  8. package/public/fonts/dm-sans.woff2 +0 -0
  9. package/public/fonts/sora.woff2 +0 -0
  10. package/public/views/checkout.html +2839 -0
  11. package/public/views/partials/auth.html +110 -0
  12. package/public/views/partials/footer.html +54 -0
  13. package/public/views/partials/header.html +101 -0
  14. package/public/views/partials/modals.html +179 -0
  15. package/public/views/partials/payment-methods.html +526 -0
  16. package/public/views/partials/sidebar.html +75 -0
  17. package/public/views/partials/splash.html +35 -0
  18. package/src/__tests__/smoke.test.js +8 -0
  19. package/src/auth-guard.js +95 -0
  20. package/src/index.js +137 -0
  21. package/src/router.js +46 -0
  22. package/src/routes/admin.routes.js +2421 -0
  23. package/src/routes/checkout.routes.js +2000 -0
  24. package/src/routes/customer.routes.js +650 -0
  25. package/src/routes/device.routes.js +303 -0
  26. package/src/routes/payments.routes.js +446 -0
  27. package/src/routes/pollers.routes.js +653 -0
  28. package/src/services/observer-poller.service.js +369 -0
  29. package/src/services/tunnel-manager.js +130 -0
  30. package/src/services/whatsapp-berry.service.js +743 -0
  31. package/src/vendor/common/__tests__/common.test.js +297 -0
  32. package/src/vendor/common/__tests__/common.test.ts +160 -0
  33. package/src/vendor/common/contracts.js +64 -0
  34. package/src/vendor/common/contracts.ts +188 -0
  35. package/src/vendor/common/crypto.js +132 -0
  36. package/src/vendor/common/crypto.ts +112 -0
  37. package/src/vendor/common/enums.js +91 -0
  38. package/src/vendor/common/enums.ts +91 -0
  39. package/src/vendor/common/index.js +12 -0
  40. package/src/vendor/common/index.ts +8 -0
  41. package/src/vendor/common/money.js +62 -0
  42. package/src/vendor/common/money.ts +56 -0
  43. package/src/vendor/common/observation.js +27 -0
  44. package/src/vendor/common/observation.ts +52 -0
  45. package/src/vendor/common/runtime-config.js +246 -0
  46. package/src/vendor/common/secrets.js +374 -0
  47. package/src/vendor/common/state-machine.d.ts +1 -0
  48. package/src/vendor/common/state-machine.js +1 -0
  49. package/src/vendor/common/state-transitions.js +85 -0
  50. package/src/vendor/common/tunnel-command.js +59 -0
  51. package/src/vendor/common/upi-providers.js +389 -0
  52. package/src/vendor/common/upi-providers.ts +400 -0
  53. package/src/vendor/common/upi.js +69 -0
  54. package/src/vendor/common/upi.ts +61 -0
  55. package/src/vendor/core/__tests__/core.test.js +683 -0
  56. package/src/vendor/core/__tests__/queue-timing.test.js +237 -0
  57. package/src/vendor/core/__tests__/webhook-engine.test.js +243 -0
  58. package/src/vendor/core/attribution-engine.js +855 -0
  59. package/src/vendor/core/bank-email-parser.js +105 -0
  60. package/src/vendor/core/bank-sms-parser.js +183 -0
  61. package/src/vendor/core/device-security.js +506 -0
  62. package/src/vendor/core/index.js +13 -0
  63. package/src/vendor/core/normalizer.js +165 -0
  64. package/src/vendor/core/qr-lib.js +8 -0
  65. package/src/vendor/core/qr.js +62 -0
  66. package/src/vendor/core/queue-engine.js +153 -0
  67. package/src/vendor/core/recovery-engine.js +159 -0
  68. package/src/vendor/core/state-machine.js +42 -0
  69. package/src/vendor/core/timing-engine.js +272 -0
  70. package/src/vendor/core/verification-pipeline.js +1164 -0
  71. package/src/vendor/core/webhook-engine.js +280 -0
  72. package/src/vendor/db/__tests__/auth-matrix.test.js +564 -0
  73. package/src/vendor/db/__tests__/customer-reset.test.js +95 -0
  74. package/src/vendor/db/__tests__/db.test.js +265 -0
  75. package/src/vendor/db/__tests__/schema-parity.test.js +137 -0
  76. package/src/vendor/db/__tests__/sqlite_verify.test.js +15 -0
  77. package/src/vendor/db/database.js +181 -0
  78. package/src/vendor/db/index.js +12 -0
  79. package/src/vendor/db/migrate.js +210 -0
  80. package/src/vendor/db/repositories/audit-log.repository.js +68 -0
  81. package/src/vendor/db/repositories/customer-session.repository.js +181 -0
  82. package/src/vendor/db/repositories/customer.repository.js +636 -0
  83. package/src/vendor/db/repositories/device.repository.js +285 -0
  84. package/src/vendor/db/repositories/merchant.repository.js +158 -0
  85. package/src/vendor/db/repositories/observation.repository.js +124 -0
  86. package/src/vendor/db/repositories/payment-session.repository.js +618 -0
  87. package/src/vendor/db/repositories/receiving-account.repository.js +244 -0
  88. package/src/vendor/db/repositories/support-ticket.repository.js +140 -0
  89. package/src/vendor/db/schema.sql +266 -0
  90. package/src/vendor/db/sqlite-schema.sql +270 -0
@@ -0,0 +1,769 @@
1
+ /**
2
+ * ELPAY PG V2 — Universal One-Script Integration SDK
3
+ *
4
+ * Official Distributable Browser Script (elpay.js / elpay-checkout.js)
5
+ * Provides zero-JavaScript button configuration [data-elpay-pay] and programmatic Elpay.pay({...})
6
+ * Launches the responsive iframe checkout modal with background blur.
7
+ * Emits normalized browser payment events and manages checkout modal lifecycle.
8
+ *
9
+ * IMPORTANT SECURITY NOTE:
10
+ * Browser events and client-side callbacks are strictly for UI and user experience.
11
+ * Authoritative proof of payment MUST ALWAYS come from server-to-server HMAC-SHA256 signed webhooks.
12
+ */
13
+
14
+ (function (globalScope) {
15
+ 'use strict';
16
+
17
+ // Prevent multiple initializations
18
+ if (globalScope.__ELPAY_SDK_INITIALIZED__) {
19
+ return;
20
+ }
21
+ globalScope.__ELPAY_SDK_INITIALIZED__ = true;
22
+
23
+ const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined';
24
+
25
+ /**
26
+ * Helper: extract configuration from the <script> tag loading this file.
27
+ */
28
+ function getScriptConfig() {
29
+ if (!isBrowser) return {};
30
+
31
+ const script = document.currentScript ||
32
+ document.querySelector('script[src*="elpay.js"], script[src*="elpay-checkout.js"]');
33
+
34
+ if (!script) return {};
35
+
36
+ let scriptUrl = '';
37
+ try {
38
+ scriptUrl = new URL(script.src, window.location.href);
39
+ } catch (e) { }
40
+
41
+ const origin = scriptUrl ? scriptUrl.origin : (window.location.protocol + '//' + window.location.hostname + ':7000');
42
+
43
+ return {
44
+ key: script.getAttribute('data-elpay-key') || script.getAttribute('data-key') || script.getAttribute('data-merchant-id') || '',
45
+ apiUrl: script.getAttribute('data-api-url') || origin,
46
+ portalUrl: script.getAttribute('data-portal-url') || script.getAttribute('data-checkout-url') || origin,
47
+ currency: script.getAttribute('data-currency') || 'INR',
48
+ name: script.getAttribute('data-name') || 'Merchant Store',
49
+ themeColor: script.getAttribute('data-theme-color') || '#0b63e5',
50
+ successUrl: script.getAttribute('data-success-url') || '',
51
+ failedUrl: script.getAttribute('data-failed-url') || '',
52
+ suspiciousUrl: script.getAttribute('data-suspicious-url') || '',
53
+ expiredUrl: script.getAttribute('data-expired-url') || '',
54
+ cancelledUrl: script.getAttribute('data-cancelled-url') || ''
55
+ };
56
+ }
57
+
58
+ const scriptConfig = getScriptConfig();
59
+
60
+ /**
61
+ * ElpayCheckout class
62
+ */
63
+ class ElpayCheckout {
64
+ /**
65
+ * @param {{
66
+ * key?: string,
67
+ * order_id?: string,
68
+ * orderId?: string,
69
+ * amount?: number | string,
70
+ * currency?: string,
71
+ * name?: string,
72
+ * description?: string,
73
+ * customer?: { name?: string, email?: string, phone?: string },
74
+ * metadata?: Record<string, any>,
75
+ * paymentId?: string,
76
+ * checkoutUrl?: string,
77
+ * portalUrl?: string,
78
+ * apiUrl?: string,
79
+ * theme?: { color?: string },
80
+ * handler?: (result: any) => void,
81
+ * onSuccess?: (result: any) => void,
82
+ * onFailed?: (result: any) => void,
83
+ * onPaymentFailed?: (result: any) => void,
84
+ * onSuspicious?: (result: any) => void,
85
+ * onExpired?: (result: any) => void,
86
+ * onCancelled?: (result: any) => void,
87
+ * onClose?: () => void,
88
+ * successUrl?: string,
89
+ * failedUrl?: string,
90
+ * suspiciousUrl?: string,
91
+ * expiredUrl?: string,
92
+ * cancelledUrl?: string
93
+ * }} options
94
+ */
95
+ constructor(options = {}) {
96
+ const defaultPortal = isBrowser
97
+ ? (scriptConfig.portalUrl || (window.location.protocol + '//' + window.location.hostname + ':7000'))
98
+ : 'http://localhost:7000';
99
+
100
+ const defaultApi = isBrowser
101
+ ? (scriptConfig.apiUrl || defaultPortal)
102
+ : 'http://localhost:6100';
103
+
104
+ this.options = Object.assign({
105
+ key: scriptConfig.key || '',
106
+ order_id: '',
107
+ orderId: '',
108
+ amount: 0,
109
+ currency: scriptConfig.currency || 'INR',
110
+ name: scriptConfig.name || 'Merchant Store',
111
+ description: 'Order Payment',
112
+ customer: {},
113
+ metadata: {},
114
+ paymentId: '',
115
+ checkoutUrl: '',
116
+ portalUrl: defaultPortal,
117
+ apiUrl: defaultApi,
118
+ theme: { color: scriptConfig.themeColor || '#0b63e5' },
119
+ handler: null,
120
+ onSuccess: null,
121
+ onFailed: null,
122
+ onPaymentFailed: null,
123
+ onSuspicious: null,
124
+ onExpired: null,
125
+ onCancelled: null,
126
+ onClose: null,
127
+ successUrl: scriptConfig.successUrl || '',
128
+ failedUrl: scriptConfig.failedUrl || '',
129
+ suspiciousUrl: scriptConfig.suspiciousUrl || '',
130
+ expiredUrl: scriptConfig.expiredUrl || '',
131
+ cancelledUrl: scriptConfig.cancelledUrl || ''
132
+ }, options);
133
+
134
+ // Normalize order ID
135
+ if (!this.options.orderId && this.options.order_id) {
136
+ this.options.orderId = this.options.order_id;
137
+ }
138
+ if (!this.options.order_id && this.options.orderId) {
139
+ this.options.order_id = this.options.orderId;
140
+ }
141
+
142
+ this.isOpen = false;
143
+ this.backdrop = null;
144
+ this.container = null;
145
+ this.iframe = null;
146
+
147
+ this._handleMessage = this._handleMessage.bind(this);
148
+ }
149
+
150
+ /**
151
+ * Primary static programmatic entry point: Elpay.pay(options)
152
+ */
153
+ static async pay(options = {}) {
154
+ const instance = new ElpayCheckout(options);
155
+
156
+ // If checkoutUrl is already provided, open modal directly
157
+ if (instance.options.checkoutUrl) {
158
+ instance.open();
159
+ return instance;
160
+ }
161
+
162
+ // If paymentId is already provided, construct checkoutUrl and open
163
+ if (instance.options.paymentId) {
164
+ instance.options.checkoutUrl = `${instance.options.portalUrl}/checkout/${instance.options.paymentId}`;
165
+ instance.open();
166
+ return instance;
167
+ }
168
+
169
+ // Otherwise, call ELPAY PG to create the payment session
170
+ try {
171
+ const orderId = instance.options.orderId || `ORDER-${Date.now()}`;
172
+ instance.options.orderId = orderId;
173
+ instance.options.order_id = orderId;
174
+
175
+ const customer = instance.options.customer || {};
176
+ const custPayload = {
177
+ fullName: customer.name || customer.fullName || '',
178
+ email: customer.email || '',
179
+ phoneNumber: customer.phone || customer.phoneNumber || customer.mobileNumber || ''
180
+ };
181
+
182
+ const createRes = await fetch(`${instance.options.apiUrl}/api/v1/payments`, {
183
+ method: 'POST',
184
+ headers: {
185
+ 'Content-Type': 'application/json',
186
+ ...(instance.options.key ? { 'X-Merchant-Id': instance.options.key } : {})
187
+ },
188
+ body: JSON.stringify({
189
+ orderId: orderId,
190
+ amount: instance.options.amount,
191
+ currency: instance.options.currency || 'INR',
192
+ customer: (custPayload.email || custPayload.phoneNumber || custPayload.fullName) ? custPayload : undefined,
193
+ description: instance.options.description,
194
+ metadata: instance.options.metadata,
195
+ returnUrl: instance.options.successUrl || undefined
196
+ })
197
+ });
198
+
199
+ const data = await createRes.json();
200
+ if (!createRes.ok || !data.checkoutUrl) {
201
+ const errMsg = data.error || data.message || `Failed to create payment session (HTTP ${createRes.status})`;
202
+ const failResult = {
203
+ status: 'FAILED',
204
+ order_id: orderId,
205
+ reason: errMsg,
206
+ error: errMsg
207
+ };
208
+ ElpayCheckout._emitEvent('elpay:payment:failed', failResult);
209
+ ElpayCheckout._emitEvent('elpay:payment:result', failResult);
210
+ if (typeof instance.options.onFailed === 'function') instance.options.onFailed(failResult);
211
+ if (typeof instance.options.onPaymentFailed === 'function') instance.options.onPaymentFailed(failResult);
212
+ throw new Error(errMsg);
213
+ }
214
+
215
+ instance.options.paymentId = data.paymentId;
216
+ instance.options.checkoutUrl = data.checkoutUrl;
217
+ instance.open();
218
+ return instance;
219
+ } catch (err) {
220
+ console.error('[ELPAY JS] Payment initialization error:', err.message);
221
+ throw err;
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Backward-compatible static entry point: ElpayCheckout.open(options)
227
+ */
228
+ static open(options) {
229
+ const instance = new ElpayCheckout(options);
230
+ instance.open();
231
+ return instance;
232
+ }
233
+
234
+ /**
235
+ * Opens the responsive iframe checkout modal
236
+ */
237
+ open() {
238
+ if (!isBrowser) {
239
+ console.warn('ElpayCheckout.open() can only be invoked in a browser DOM environment.');
240
+ return;
241
+ }
242
+ if (this.isOpen) return;
243
+ this.isOpen = true;
244
+
245
+ ElpayCheckout._ensureStyles();
246
+
247
+ let targetUrl = '';
248
+ if (this.options.checkoutUrl) {
249
+ targetUrl = this.options.checkoutUrl;
250
+ } else if (this.options.paymentId) {
251
+ targetUrl = `${this.options.portalUrl}/checkout/${this.options.paymentId}`;
252
+ } else {
253
+ targetUrl = `${this.options.portalUrl}/checkout/demo`;
254
+ }
255
+
256
+ // If running on localhost / 127.0.0.1 and targetUrl points to remote external origin, keep path and point to local host
257
+ if (isBrowser && (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1')) {
258
+ try {
259
+ const parsed = new URL(targetUrl, window.location.origin);
260
+ if (parsed.hostname !== 'localhost' && parsed.hostname !== '127.0.0.1' && !this.options.portalUrl) {
261
+ targetUrl = `${window.location.protocol}//${window.location.host}${parsed.pathname}${parsed.search}`;
262
+ }
263
+ } catch {}
264
+ }
265
+
266
+ // Add embedded flag and custom params
267
+ const sep = targetUrl.includes('?') ? '&' : '?';
268
+ targetUrl = `${targetUrl}${sep}embedded=1`;
269
+
270
+ if (this.options.amount) {
271
+ targetUrl += `&customAmount=${encodeURIComponent(this.options.amount)}`;
272
+ }
273
+ if (this.options.name) {
274
+ targetUrl += `&customName=${encodeURIComponent(this.options.name)}`;
275
+ }
276
+
277
+ this._injectModal(targetUrl);
278
+ window.addEventListener('message', this._handleMessage);
279
+ }
280
+
281
+ /**
282
+ * Closes the checkout modal
283
+ */
284
+ close(callback) {
285
+ if (!isBrowser || !this.isOpen) return;
286
+ this.isOpen = false;
287
+
288
+ window.removeEventListener('message', this._handleMessage);
289
+
290
+ if (this.container) {
291
+ this.container.classList.remove('active');
292
+ this.container.classList.add('closing');
293
+ }
294
+ if (this.backdrop) {
295
+ this.backdrop.classList.remove('active');
296
+ }
297
+
298
+ setTimeout(() => {
299
+ if (this.backdrop && this.backdrop.parentNode) {
300
+ this.backdrop.parentNode.removeChild(this.backdrop);
301
+ }
302
+ this.backdrop = null;
303
+ this.container = null;
304
+ this.iframe = null;
305
+
306
+ if (typeof this.options.onClose === 'function') {
307
+ this.options.onClose();
308
+ }
309
+ if (typeof callback === 'function') {
310
+ callback();
311
+ }
312
+ }, 350);
313
+ }
314
+
315
+ _injectModal(targetUrl) {
316
+ const backdrop = document.createElement('div');
317
+ backdrop.className = 'elpay-checkout-backdrop';
318
+
319
+ const container = document.createElement('div');
320
+ container.className = 'elpay-checkout-container';
321
+
322
+ const closeBtn = document.createElement('button');
323
+ closeBtn.className = 'elpay-checkout-close-btn';
324
+ closeBtn.innerHTML = '✕';
325
+ closeBtn.title = 'Close Checkout';
326
+ closeBtn.type = 'button';
327
+ closeBtn.onclick = () => {
328
+ try {
329
+ if (iframe && iframe.contentWindow) {
330
+ if (this._iframeOrigin) {
331
+ iframe.contentWindow.postMessage({ event: 'elpay.request_cancel' }, this._iframeOrigin);
332
+ }
333
+ return;
334
+ }
335
+ } catch (err) { }
336
+ this._dispatchCancelled();
337
+ this.close();
338
+ };
339
+
340
+ const iframe = document.createElement('iframe');
341
+ iframe.className = 'elpay-checkout-iframe';
342
+ iframe.src = targetUrl;
343
+ // Only messages from this exact iframe/origin are trusted (see _handleMessage).
344
+ this._iframe = iframe;
345
+ try { this._iframeOrigin = new URL(targetUrl, window.location.href).origin; } catch { this._iframeOrigin = null; }
346
+ iframe.allow = 'payment';
347
+
348
+ container.appendChild(closeBtn);
349
+ container.appendChild(iframe);
350
+ backdrop.appendChild(container);
351
+ document.body.appendChild(backdrop);
352
+
353
+ requestAnimationFrame(() => {
354
+ backdrop.classList.add('active');
355
+ container.classList.add('active');
356
+ });
357
+
358
+ this.backdrop = backdrop;
359
+ this.container = container;
360
+ this.iframe = iframe;
361
+
362
+ // Invariant: ONLY the cross (X) button exits the payment gateway!
363
+ // Prevent outside touch/click on backdrop from closing the checkout modal.
364
+ const stopOutsideDismiss = (e) => {
365
+ if (e.target === backdrop) {
366
+ e.preventDefault();
367
+ e.stopPropagation();
368
+ if (e.stopImmediatePropagation) e.stopImmediatePropagation();
369
+ return false;
370
+ }
371
+ };
372
+ backdrop.addEventListener('click', stopOutsideDismiss, true);
373
+ backdrop.addEventListener('touchstart', stopOutsideDismiss, { passive: false, capture: true });
374
+ backdrop.addEventListener('touchend', stopOutsideDismiss, { passive: false, capture: true });
375
+ backdrop.addEventListener('touchmove', stopOutsideDismiss, { passive: false, capture: true });
376
+ backdrop.addEventListener('pointerdown', stopOutsideDismiss, true);
377
+ backdrop.addEventListener('pointerup', stopOutsideDismiss, true);
378
+ backdrop.addEventListener('mousedown', stopOutsideDismiss, true);
379
+ backdrop.addEventListener('mouseup', stopOutsideDismiss, true);
380
+ }
381
+
382
+ _isSafeRedirect(url) {
383
+ try {
384
+ const u = new URL(url, window.location.href);
385
+ return u.protocol === 'http:' || u.protocol === 'https:';
386
+ } catch {
387
+ return false;
388
+ }
389
+ }
390
+
391
+ _handleMessage(e) {
392
+ // Ignore messages from any window other than our checkout iframe (spoofed success events).
393
+ if (!this._iframe || e.source !== this._iframe.contentWindow) return;
394
+ if (!this._iframeOrigin || e.origin !== this._iframeOrigin) return;
395
+ if (!e.data || typeof e.data !== 'object') return;
396
+
397
+ const event = e.data.event;
398
+ const rawData = e.data.data || e.data;
399
+
400
+ if (event === 'elpay.payment_success') {
401
+ const payload = e.data.data || {};
402
+ const result = {
403
+ status: 'SUCCESS',
404
+ order_id: payload.orderId || this.options.orderId || this.options.order_id,
405
+ payment_mode: payload.method || 'UPI',
406
+ amount_received: Number(payload.amount || this.options.amount || 0),
407
+ utr: payload.bankRef || payload.utr || payload.reference || '',
408
+ timestamp: payload.completedAt || new Date().toISOString(),
409
+ metadata: payload.metadata || this.options.metadata || {},
410
+ raw: payload
411
+ };
412
+
413
+ ElpayCheckout._emitEvent('elpay:payment:success', result);
414
+ ElpayCheckout._emitEvent('elpay:payment:result', result);
415
+
416
+ if (typeof this.options.handler === 'function') {
417
+ this.options.handler(result);
418
+ }
419
+ if (typeof this.options.onSuccess === 'function') {
420
+ this.options.onSuccess(result);
421
+ }
422
+
423
+ setTimeout(() => {
424
+ this.close(() => {
425
+ if (this.options.successUrl) {
426
+ const delim = this.options.successUrl.includes('?') ? '&' : '?';
427
+ window.location.href = `${this.options.successUrl}${delim}order_id=${encodeURIComponent(result.order_id)}&amount=${encodeURIComponent(result.amount_received)}&utr=${encodeURIComponent(result.utr)}`;
428
+ }
429
+ });
430
+ }, 1500);
431
+ } else if (event === 'elpay.payment_failed') {
432
+ const payload = e.data.data || e.data || {};
433
+ const result = {
434
+ status: 'FAILED',
435
+ order_id: payload.orderId || this.options.orderId || this.options.order_id,
436
+ payment_mode: payload.method || 'UPI',
437
+ amount_received: 0,
438
+ utr: '',
439
+ reason: payload.reason || payload.status || 'PAYMENT_FAILED',
440
+ timestamp: new Date().toISOString(),
441
+ metadata: payload.metadata || this.options.metadata || {},
442
+ raw: payload
443
+ };
444
+
445
+ ElpayCheckout._emitEvent('elpay:payment:failed', result);
446
+ ElpayCheckout._emitEvent('elpay:payment:result', result);
447
+
448
+ if (typeof this.options.onFailed === 'function') {
449
+ try { this.options.onFailed(result); } catch (err) { }
450
+ }
451
+ if (typeof this.options.onPaymentFailed === 'function') {
452
+ try { this.options.onPaymentFailed(result); } catch (err) { }
453
+ }
454
+
455
+ this.close(() => {
456
+ if (this.options.failedUrl) {
457
+ const delim = this.options.failedUrl.includes('?') ? '&' : '?';
458
+ window.location.href = `${this.options.failedUrl}${delim}order_id=${encodeURIComponent(result.order_id)}&reason=${encodeURIComponent(result.reason)}`;
459
+ } else if (payload.redirectUrl && this._isSafeRedirect(payload.redirectUrl) && payload.redirectUrl !== window.location.href && !payload.redirectUrl.includes('/checkout')) {
460
+ window.location.href = payload.redirectUrl;
461
+ }
462
+ });
463
+ } else if (event === 'elpay.payment_blocked' || event === 'elpay.payment_blocked_exit') {
464
+ const payload = e.data.data || e.data || {};
465
+ const result = {
466
+ status: 'SUSPICIOUS',
467
+ order_id: payload.orderId || this.options.orderId || this.options.order_id,
468
+ payment_mode: payload.method || 'UPI',
469
+ amount_received: 0,
470
+ utr: '',
471
+ reason: 'SECURITY_REVIEW_FLAGGED',
472
+ timestamp: new Date().toISOString(),
473
+ metadata: payload.metadata || this.options.metadata || {},
474
+ raw: payload
475
+ };
476
+
477
+ ElpayCheckout._emitEvent('elpay:payment:suspicious', result);
478
+ ElpayCheckout._emitEvent('elpay:payment:result', result);
479
+
480
+ if (typeof this.options.onSuspicious === 'function') {
481
+ try { this.options.onSuspicious(result); } catch (err) { }
482
+ }
483
+
484
+ this.close(() => {
485
+ if (this.options.suspiciousUrl) {
486
+ const delim = this.options.suspiciousUrl.includes('?') ? '&' : '?';
487
+ window.location.href = `${this.options.suspiciousUrl}${delim}order_id=${encodeURIComponent(result.order_id)}&reason=SUSPICIOUS`;
488
+ }
489
+ });
490
+ } else if (event === 'elpay.payment_expired') {
491
+ const payload = e.data.data || e.data || {};
492
+ const result = {
493
+ status: 'EXPIRED',
494
+ order_id: payload.orderId || this.options.orderId || this.options.order_id,
495
+ timestamp: new Date().toISOString()
496
+ };
497
+
498
+ ElpayCheckout._emitEvent('elpay:payment:expired', result);
499
+ ElpayCheckout._emitEvent('elpay:payment:result', result);
500
+
501
+ if (typeof this.options.onExpired === 'function') {
502
+ try { this.options.onExpired(result); } catch (err) { }
503
+ }
504
+
505
+ this.close(() => {
506
+ if (this.options.expiredUrl) {
507
+ const delim = this.options.expiredUrl.includes('?') ? '&' : '?';
508
+ window.location.href = `${this.options.expiredUrl}${delim}order_id=${encodeURIComponent(result.order_id)}`;
509
+ }
510
+ });
511
+ } else if (event === 'elpay.modal_close' || event === 'elpay.close' || event === 'elpay.cancel' || event === 'elpay.payment_cancelled') {
512
+ this._dispatchCancelled();
513
+ const redirectUrl = e.data.redirectUrl;
514
+ this.close(() => {
515
+ if (this.options.cancelledUrl) {
516
+ const delim = this.options.cancelledUrl.includes('?') ? '&' : '?';
517
+ window.location.href = `${this.options.cancelledUrl}${delim}order_id=${encodeURIComponent(this.options.orderId || '')}`;
518
+ } else if (redirectUrl && this._isSafeRedirect(redirectUrl) && redirectUrl !== window.location.href && !redirectUrl.includes('/checkout')) {
519
+ window.location.href = redirectUrl;
520
+ }
521
+ });
522
+ }
523
+ }
524
+
525
+ _dispatchCancelled() {
526
+ const result = {
527
+ status: 'CANCELLED',
528
+ order_id: this.options.orderId || this.options.order_id || '',
529
+ timestamp: new Date().toISOString()
530
+ };
531
+ ElpayCheckout._emitEvent('elpay:payment:cancelled', result);
532
+ ElpayCheckout._emitEvent('elpay:payment:result', result);
533
+
534
+ if (typeof this.options.onCancelled === 'function') {
535
+ try { this.options.onCancelled(result); } catch (e) { }
536
+ }
537
+ }
538
+
539
+ static _emitEvent(name, detail) {
540
+ if (!isBrowser) return;
541
+ try {
542
+ const ev = new CustomEvent(name, { detail, bubbles: true, cancelable: true });
543
+ window.dispatchEvent(ev);
544
+ document.dispatchEvent(ev);
545
+ } catch (e) { }
546
+ }
547
+
548
+ /**
549
+ * Handles clicks on elements with [data-elpay-pay] attribute
550
+ */
551
+ static handleButtonClick(btn) {
552
+ if (!btn) return;
553
+
554
+ const orderId = btn.getAttribute('data-order-id') || `ORDER-${Date.now()}`;
555
+ const amount = btn.getAttribute('data-amount') || '0';
556
+ const currency = btn.getAttribute('data-currency') || scriptConfig.currency || 'INR';
557
+ const name = btn.getAttribute('data-name') || scriptConfig.name || 'Merchant Store';
558
+ const description = btn.getAttribute('data-description') || 'Order Payment';
559
+
560
+ // Customer info
561
+ const customer = {
562
+ name: btn.getAttribute('data-customer-name') || '',
563
+ email: btn.getAttribute('data-customer-email') || '',
564
+ phone: btn.getAttribute('data-customer-phone') || btn.getAttribute('data-customer-mobile') || ''
565
+ };
566
+
567
+ // Metadata parsing
568
+ let metadata = {};
569
+ const rawMeta = btn.getAttribute('data-metadata');
570
+ if (rawMeta) {
571
+ try {
572
+ metadata = JSON.parse(rawMeta);
573
+ } catch (e) {
574
+ metadata = { raw: rawMeta };
575
+ }
576
+ }
577
+ // Gather any data-meta-* attributes
578
+ for (const attr of btn.attributes) {
579
+ if (attr.name.startsWith('data-meta-')) {
580
+ const key = attr.name.slice(10);
581
+ metadata[key] = attr.value;
582
+ }
583
+ }
584
+
585
+ const checkoutUrl = btn.getAttribute('data-checkout-url') || '';
586
+ const paymentId = btn.getAttribute('data-payment-id') || '';
587
+ const successUrl = btn.getAttribute('data-success-url') || scriptConfig.successUrl || '';
588
+ const failedUrl = btn.getAttribute('data-failed-url') || scriptConfig.failedUrl || '';
589
+ const suspiciousUrl = btn.getAttribute('data-suspicious-url') || scriptConfig.suspiciousUrl || '';
590
+ const expiredUrl = btn.getAttribute('data-expired-url') || scriptConfig.expiredUrl || '';
591
+ const cancelledUrl = btn.getAttribute('data-cancelled-url') || scriptConfig.cancelledUrl || '';
592
+
593
+ ElpayCheckout.pay({
594
+ order_id: orderId,
595
+ orderId: orderId,
596
+ amount: amount,
597
+ currency: currency,
598
+ name: name,
599
+ description: description,
600
+ customer: customer,
601
+ metadata: metadata,
602
+ checkoutUrl: checkoutUrl,
603
+ paymentId: paymentId,
604
+ successUrl: successUrl,
605
+ failedUrl: failedUrl,
606
+ suspiciousUrl: suspiciousUrl,
607
+ expiredUrl: expiredUrl,
608
+ cancelledUrl: cancelledUrl
609
+ }).catch(err => {
610
+ console.error('[ELPAY JS] Pay error:', err);
611
+ });
612
+ }
613
+
614
+ static _ensureStyles() {
615
+ if (!isBrowser) return;
616
+ let styleEl = document.getElementById('elpay-checkout-styles');
617
+ if (!styleEl) {
618
+ styleEl = document.createElement('style');
619
+ styleEl.id = 'elpay-checkout-styles';
620
+ document.head.appendChild(styleEl);
621
+ }
622
+ styleEl.textContent = `
623
+ .elpay-checkout-backdrop {
624
+ position: fixed;
625
+ top: 0;
626
+ left: 0;
627
+ width: 100vw;
628
+ width: 100%;
629
+ height: 100vh;
630
+ height: 100dvh;
631
+ height: 100%;
632
+ background: rgba(12, 35, 64, 0.76);
633
+ backdrop-filter: blur(6px);
634
+ -webkit-backdrop-filter: blur(6px);
635
+ z-index: 2147483640;
636
+ display: flex;
637
+ align-items: center;
638
+ justify-content: center;
639
+ opacity: 0;
640
+ transition: opacity 0.25s ease-out;
641
+ touch-action: pan-y;
642
+ overscroll-behavior: contain;
643
+ user-select: none;
644
+ -webkit-user-select: none;
645
+ -webkit-tap-highlight-color: transparent;
646
+ }
647
+ .elpay-checkout-backdrop.active {
648
+ opacity: 1;
649
+ }
650
+ .elpay-checkout-container {
651
+ position: relative;
652
+ width: 440px;
653
+ max-width: 95vw;
654
+ height: 680px;
655
+ max-height: 92vh;
656
+ background: #ffffff;
657
+ border-radius: 14px;
658
+ box-shadow: 0 25px 65px rgba(0, 0, 0, 0.5);
659
+ overflow: hidden;
660
+ transform: translateY(20px) scale(0.98);
661
+ transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
662
+ display: flex;
663
+ flex-direction: column;
664
+ touch-action: auto !important;
665
+ user-select: auto !important;
666
+ -webkit-user-select: auto !important;
667
+ }
668
+ .elpay-checkout-container.active {
669
+ transform: translateY(0) scale(1);
670
+ }
671
+ .elpay-checkout-container.closing {
672
+ transform: translateY(20px) scale(0.96);
673
+ }
674
+ .elpay-checkout-iframe {
675
+ width: 100%;
676
+ height: 100%;
677
+ border: none;
678
+ flex: 1;
679
+ background: #ffffff;
680
+ touch-action: auto !important;
681
+ }
682
+ .elpay-checkout-close-btn {
683
+ position: absolute;
684
+ top: 14px;
685
+ right: 14px;
686
+ width: 28px;
687
+ height: 28px;
688
+ border-radius: 50%;
689
+ background: rgba(255, 255, 255, 0.2);
690
+ border: none;
691
+ color: #ffffff;
692
+ font-size: 14px;
693
+ cursor: pointer;
694
+ z-index: 10;
695
+ display: flex;
696
+ align-items: center;
697
+ justify-content: center;
698
+ transition: background 0.15s;
699
+ }
700
+ .elpay-checkout-close-btn:hover {
701
+ background: rgba(255, 255, 255, 0.4);
702
+ }
703
+
704
+ /* DESKTOP / PC MODAL STYLES (Razorpay 2-Column Exact Layout) */
705
+ @media (min-width: 768px) {
706
+ .elpay-checkout-container {
707
+ width: 900px;
708
+ max-width: 95vw;
709
+ height: 600px;
710
+ max-height: 92vh;
711
+ border-radius: 16px;
712
+ box-shadow: 0 30px 80px rgba(0, 0, 0, 0.45);
713
+ }
714
+ .elpay-checkout-close-btn {
715
+ display: none; /* Native in-modal close button inside the checkout header bar */
716
+ }
717
+ }
718
+
719
+ /* MOBILE BOTTOM DRAWER / BOTTOM SHEET STYLES */
720
+ @media (max-width: 767px) {
721
+ .elpay-checkout-backdrop {
722
+ align-items: flex-end;
723
+ }
724
+ .elpay-checkout-container {
725
+ width: 100vw;
726
+ max-width: 100vw;
727
+ height: 100dvh;
728
+ height: 100vh;
729
+ height: 100%;
730
+ max-height: 100dvh;
731
+ border-bottom-left-radius: 0;
732
+ border-bottom-right-radius: 0;
733
+ border-top-left-radius: 18px;
734
+ border-top-right-radius: 18px;
735
+ transform: translateY(100%);
736
+ touch-action: auto !important;
737
+ overflow: hidden;
738
+ }
739
+ .elpay-checkout-container.active {
740
+ transform: translateY(0);
741
+ }
742
+ .elpay-checkout-container.closing {
743
+ transform: translateY(100%);
744
+ }
745
+ }
746
+ `;
747
+ }
748
+ }
749
+
750
+ // Auto-bind click listener for all [data-elpay-pay] buttons on page
751
+ if (isBrowser) {
752
+ document.addEventListener('click', function (e) {
753
+ const btn = e.target && e.target.closest && e.target.closest('[data-elpay-pay]');
754
+ if (btn) {
755
+ e.preventDefault();
756
+ ElpayCheckout.handleButtonClick(btn);
757
+ }
758
+ });
759
+ }
760
+
761
+ // Export globals
762
+ globalScope.ElpayCheckout = ElpayCheckout;
763
+ globalScope.Elpay = ElpayCheckout;
764
+ if (typeof module !== 'undefined' && module.exports) {
765
+ module.exports = ElpayCheckout;
766
+ module.exports.ElpayCheckout = ElpayCheckout;
767
+ module.exports.Elpay = ElpayCheckout;
768
+ }
769
+ })(typeof window !== 'undefined' ? window : globalThis);