elpaypg 0.0.0-stage → 2.0.1
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/.env.example +8 -0
- package/README.md +176 -2
- package/package.json +21 -4
- package/public/elpay-checkout.js +769 -0
- package/public/elpay-sms-gateway.apk +0 -0
- package/public/elpay-sms-gateway.apk.sha256 +1 -0
- package/public/elpay.js +785 -0
- package/public/fonts/dm-sans.woff2 +0 -0
- package/public/fonts/sora.woff2 +0 -0
- package/public/views/checkout.html +2839 -0
- package/public/views/partials/auth.html +110 -0
- package/public/views/partials/footer.html +54 -0
- package/public/views/partials/header.html +101 -0
- package/public/views/partials/modals.html +179 -0
- package/public/views/partials/payment-methods.html +526 -0
- package/public/views/partials/sidebar.html +75 -0
- package/public/views/partials/splash.html +35 -0
- package/src/__tests__/smoke.test.js +8 -0
- package/src/auth-guard.js +95 -0
- package/src/index.js +137 -0
- package/src/router.js +46 -0
- package/src/routes/admin.routes.js +2424 -0
- package/src/routes/checkout.routes.js +2000 -0
- package/src/routes/customer.routes.js +650 -0
- package/src/routes/device.routes.js +303 -0
- package/src/routes/payments.routes.js +446 -0
- package/src/routes/pollers.routes.js +653 -0
- package/src/services/observer-poller.service.js +369 -0
- package/src/services/tunnel-manager.js +130 -0
- package/src/services/whatsapp-berry.service.js +743 -0
- package/src/vendor/common/__tests__/common.test.js +297 -0
- package/src/vendor/common/__tests__/common.test.ts +160 -0
- package/src/vendor/common/contracts.js +64 -0
- package/src/vendor/common/contracts.ts +188 -0
- package/src/vendor/common/crypto.js +132 -0
- package/src/vendor/common/crypto.ts +112 -0
- package/src/vendor/common/enums.js +91 -0
- package/src/vendor/common/enums.ts +91 -0
- package/src/vendor/common/index.js +12 -0
- package/src/vendor/common/index.ts +8 -0
- package/src/vendor/common/money.js +62 -0
- package/src/vendor/common/money.ts +56 -0
- package/src/vendor/common/observation.js +27 -0
- package/src/vendor/common/observation.ts +52 -0
- package/src/vendor/common/runtime-config.js +246 -0
- package/src/vendor/common/secrets.js +374 -0
- package/src/vendor/common/state-machine.d.ts +1 -0
- package/src/vendor/common/state-machine.js +1 -0
- package/src/vendor/common/state-transitions.js +85 -0
- package/src/vendor/common/tunnel-command.js +59 -0
- package/src/vendor/common/upi-providers.js +389 -0
- package/src/vendor/common/upi-providers.ts +400 -0
- package/src/vendor/common/upi.js +69 -0
- package/src/vendor/common/upi.ts +61 -0
- package/src/vendor/core/__tests__/core.test.js +683 -0
- package/src/vendor/core/__tests__/queue-timing.test.js +237 -0
- package/src/vendor/core/__tests__/webhook-engine.test.js +243 -0
- package/src/vendor/core/attribution-engine.js +855 -0
- package/src/vendor/core/bank-email-parser.js +105 -0
- package/src/vendor/core/bank-sms-parser.js +183 -0
- package/src/vendor/core/device-security.js +506 -0
- package/src/vendor/core/index.js +13 -0
- package/src/vendor/core/normalizer.js +165 -0
- package/src/vendor/core/qr-lib.js +8 -0
- package/src/vendor/core/qr.js +62 -0
- package/src/vendor/core/queue-engine.js +153 -0
- package/src/vendor/core/recovery-engine.js +159 -0
- package/src/vendor/core/state-machine.js +42 -0
- package/src/vendor/core/timing-engine.js +272 -0
- package/src/vendor/core/verification-pipeline.js +1164 -0
- package/src/vendor/core/webhook-engine.js +280 -0
- package/src/vendor/db/__tests__/auth-matrix.test.js +564 -0
- package/src/vendor/db/__tests__/customer-reset.test.js +95 -0
- package/src/vendor/db/__tests__/db.test.js +265 -0
- package/src/vendor/db/__tests__/schema-parity.test.js +137 -0
- package/src/vendor/db/__tests__/sqlite_verify.test.js +15 -0
- package/src/vendor/db/database.js +181 -0
- package/src/vendor/db/index.js +12 -0
- package/src/vendor/db/migrate.js +210 -0
- package/src/vendor/db/repositories/audit-log.repository.js +68 -0
- package/src/vendor/db/repositories/customer-session.repository.js +181 -0
- package/src/vendor/db/repositories/customer.repository.js +636 -0
- package/src/vendor/db/repositories/device.repository.js +285 -0
- package/src/vendor/db/repositories/merchant.repository.js +158 -0
- package/src/vendor/db/repositories/observation.repository.js +124 -0
- package/src/vendor/db/repositories/payment-session.repository.js +618 -0
- package/src/vendor/db/repositories/receiving-account.repository.js +244 -0
- package/src/vendor/db/repositories/support-ticket.repository.js +140 -0
- package/src/vendor/db/schema.sql +266 -0
- package/src/vendor/db/sqlite-schema.sql +270 -0
package/public/elpay.js
ADDED
|
@@ -0,0 +1,785 @@
|
|
|
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
|
+
// Inject theme stylesheet (CSS variables) if not already present
|
|
317
|
+
if (!document.getElementById('elpay-theme')) {
|
|
318
|
+
const link = document.createElement('link');
|
|
319
|
+
link.id = 'elpay-theme';
|
|
320
|
+
link.rel = 'stylesheet';
|
|
321
|
+
link.href = '/elpay/theme.css';
|
|
322
|
+
document.head.appendChild(link);
|
|
323
|
+
}
|
|
324
|
+
const backdrop = document.createElement('div');
|
|
325
|
+
backdrop.className = 'elpay-checkout-backdrop';
|
|
326
|
+
|
|
327
|
+
const container = document.createElement('div');
|
|
328
|
+
container.className = 'elpay-checkout-container';
|
|
329
|
+
|
|
330
|
+
const closeBtn = document.createElement('button');
|
|
331
|
+
closeBtn.className = 'elpay-checkout-close-btn';
|
|
332
|
+
closeBtn.innerHTML = '✕';
|
|
333
|
+
closeBtn.title = 'Close Checkout';
|
|
334
|
+
closeBtn.type = 'button';
|
|
335
|
+
closeBtn.onclick = () => {
|
|
336
|
+
try {
|
|
337
|
+
if (iframe && iframe.contentWindow) {
|
|
338
|
+
if (this._iframeOrigin) {
|
|
339
|
+
iframe.contentWindow.postMessage({ event: 'elpay.request_cancel' }, this._iframeOrigin);
|
|
340
|
+
}
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
} catch (err) { }
|
|
344
|
+
this._dispatchCancelled();
|
|
345
|
+
this.close();
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const iframe = document.createElement('iframe');
|
|
349
|
+
iframe.className = 'elpay-checkout-iframe';
|
|
350
|
+
iframe.src = targetUrl;
|
|
351
|
+
// Only messages from this exact iframe/origin are trusted (see _handleMessage).
|
|
352
|
+
this._iframe = iframe;
|
|
353
|
+
try { this._iframeOrigin = new URL(targetUrl, window.location.href).origin; } catch { this._iframeOrigin = null; }
|
|
354
|
+
iframe.allow = 'payment';
|
|
355
|
+
|
|
356
|
+
container.appendChild(closeBtn);
|
|
357
|
+
container.appendChild(iframe);
|
|
358
|
+
backdrop.appendChild(container);
|
|
359
|
+
document.body.appendChild(backdrop);
|
|
360
|
+
|
|
361
|
+
backdrop.classList.add('active');
|
|
362
|
+
container.classList.add('active');
|
|
363
|
+
|
|
364
|
+
this.backdrop = backdrop;
|
|
365
|
+
this.container = container;
|
|
366
|
+
this.iframe = iframe;
|
|
367
|
+
|
|
368
|
+
// Invariant: ONLY the cross (X) button exits the payment gateway!
|
|
369
|
+
// Prevent outside touch/click on backdrop from closing the checkout modal.
|
|
370
|
+
const stopOutsideDismiss = (e) => {
|
|
371
|
+
if (e.target === backdrop) {
|
|
372
|
+
e.preventDefault();
|
|
373
|
+
e.stopPropagation();
|
|
374
|
+
if (e.stopImmediatePropagation) e.stopImmediatePropagation();
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
backdrop.addEventListener('click', stopOutsideDismiss, true);
|
|
379
|
+
backdrop.addEventListener('touchstart', stopOutsideDismiss, { passive: false, capture: true });
|
|
380
|
+
backdrop.addEventListener('touchend', stopOutsideDismiss, { passive: false, capture: true });
|
|
381
|
+
backdrop.addEventListener('touchmove', stopOutsideDismiss, { passive: false, capture: true });
|
|
382
|
+
backdrop.addEventListener('pointerdown', stopOutsideDismiss, true);
|
|
383
|
+
backdrop.addEventListener('pointerup', stopOutsideDismiss, true);
|
|
384
|
+
backdrop.addEventListener('mousedown', stopOutsideDismiss, true);
|
|
385
|
+
backdrop.addEventListener('mouseup', stopOutsideDismiss, true);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
_isSafeRedirect(url) {
|
|
389
|
+
try {
|
|
390
|
+
const u = new URL(url, window.location.href);
|
|
391
|
+
return u.protocol === 'http:' || u.protocol === 'https:';
|
|
392
|
+
} catch {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
_handleMessage(e) {
|
|
398
|
+
// Ignore messages from any window other than our checkout iframe (spoofed success events).
|
|
399
|
+
if (!this._iframe || e.source !== this._iframe.contentWindow) return;
|
|
400
|
+
if (!this._iframeOrigin || e.origin !== this._iframeOrigin) return;
|
|
401
|
+
if (!e.data || typeof e.data !== 'object') return;
|
|
402
|
+
|
|
403
|
+
const event = e.data.event;
|
|
404
|
+
const rawData = e.data.data || e.data;
|
|
405
|
+
|
|
406
|
+
if (event === 'elpay.payment_success') {
|
|
407
|
+
const payload = e.data.data || {};
|
|
408
|
+
const result = {
|
|
409
|
+
status: 'SUCCESS',
|
|
410
|
+
order_id: payload.orderId || this.options.orderId || this.options.order_id,
|
|
411
|
+
payment_mode: payload.method || 'UPI',
|
|
412
|
+
amount_received: Number(payload.amount || this.options.amount || 0),
|
|
413
|
+
utr: payload.bankRef || payload.utr || payload.reference || '',
|
|
414
|
+
timestamp: payload.completedAt || new Date().toISOString(),
|
|
415
|
+
metadata: payload.metadata || this.options.metadata || {},
|
|
416
|
+
raw: payload
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
ElpayCheckout._emitEvent('elpay:payment:success', result);
|
|
420
|
+
ElpayCheckout._emitEvent('elpay:payment:result', result);
|
|
421
|
+
|
|
422
|
+
if (typeof this.options.handler === 'function') {
|
|
423
|
+
this.options.handler(result);
|
|
424
|
+
}
|
|
425
|
+
if (typeof this.options.onSuccess === 'function') {
|
|
426
|
+
this.options.onSuccess(result);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
setTimeout(() => {
|
|
430
|
+
this.close(() => {
|
|
431
|
+
if (this.options.successUrl) {
|
|
432
|
+
const delim = this.options.successUrl.includes('?') ? '&' : '?';
|
|
433
|
+
window.location.href = `${this.options.successUrl}${delim}order_id=${encodeURIComponent(result.order_id)}&amount=${encodeURIComponent(result.amount_received)}&utr=${encodeURIComponent(result.utr)}`;
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
}, 1500);
|
|
437
|
+
} else if (event === 'elpay.payment_failed') {
|
|
438
|
+
const payload = e.data.data || e.data || {};
|
|
439
|
+
const result = {
|
|
440
|
+
status: 'FAILED',
|
|
441
|
+
order_id: payload.orderId || this.options.orderId || this.options.order_id,
|
|
442
|
+
payment_mode: payload.method || 'UPI',
|
|
443
|
+
amount_received: 0,
|
|
444
|
+
utr: '',
|
|
445
|
+
reason: payload.reason || payload.status || 'PAYMENT_FAILED',
|
|
446
|
+
timestamp: new Date().toISOString(),
|
|
447
|
+
metadata: payload.metadata || this.options.metadata || {},
|
|
448
|
+
raw: payload
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
ElpayCheckout._emitEvent('elpay:payment:failed', result);
|
|
452
|
+
ElpayCheckout._emitEvent('elpay:payment:result', result);
|
|
453
|
+
|
|
454
|
+
if (typeof this.options.onFailed === 'function') {
|
|
455
|
+
try { this.options.onFailed(result); } catch (err) { }
|
|
456
|
+
}
|
|
457
|
+
if (typeof this.options.onPaymentFailed === 'function') {
|
|
458
|
+
try { this.options.onPaymentFailed(result); } catch (err) { }
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
this.close(() => {
|
|
462
|
+
if (this.options.failedUrl) {
|
|
463
|
+
const delim = this.options.failedUrl.includes('?') ? '&' : '?';
|
|
464
|
+
window.location.href = `${this.options.failedUrl}${delim}order_id=${encodeURIComponent(result.order_id)}&reason=${encodeURIComponent(result.reason)}`;
|
|
465
|
+
} else if (payload.redirectUrl && this._isSafeRedirect(payload.redirectUrl) && payload.redirectUrl !== window.location.href && !payload.redirectUrl.includes('/checkout')) {
|
|
466
|
+
window.location.href = payload.redirectUrl;
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
} else if (event === 'elpay.payment_blocked' || event === 'elpay.payment_blocked_exit') {
|
|
470
|
+
const payload = e.data.data || e.data || {};
|
|
471
|
+
const result = {
|
|
472
|
+
status: 'SUSPICIOUS',
|
|
473
|
+
order_id: payload.orderId || this.options.orderId || this.options.order_id,
|
|
474
|
+
payment_mode: payload.method || 'UPI',
|
|
475
|
+
amount_received: 0,
|
|
476
|
+
utr: '',
|
|
477
|
+
reason: 'SECURITY_REVIEW_FLAGGED',
|
|
478
|
+
timestamp: new Date().toISOString(),
|
|
479
|
+
metadata: payload.metadata || this.options.metadata || {},
|
|
480
|
+
raw: payload
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
ElpayCheckout._emitEvent('elpay:payment:suspicious', result);
|
|
484
|
+
ElpayCheckout._emitEvent('elpay:payment:result', result);
|
|
485
|
+
|
|
486
|
+
if (typeof this.options.onSuspicious === 'function') {
|
|
487
|
+
try { this.options.onSuspicious(result); } catch (err) { }
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
this.close(() => {
|
|
491
|
+
if (this.options.suspiciousUrl) {
|
|
492
|
+
const delim = this.options.suspiciousUrl.includes('?') ? '&' : '?';
|
|
493
|
+
window.location.href = `${this.options.suspiciousUrl}${delim}order_id=${encodeURIComponent(result.order_id)}&reason=SUSPICIOUS`;
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
} else if (event === 'elpay.payment_expired') {
|
|
497
|
+
const payload = e.data.data || e.data || {};
|
|
498
|
+
const result = {
|
|
499
|
+
status: 'EXPIRED',
|
|
500
|
+
order_id: payload.orderId || this.options.orderId || this.options.order_id,
|
|
501
|
+
timestamp: new Date().toISOString()
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
ElpayCheckout._emitEvent('elpay:payment:expired', result);
|
|
505
|
+
ElpayCheckout._emitEvent('elpay:payment:result', result);
|
|
506
|
+
|
|
507
|
+
if (typeof this.options.onExpired === 'function') {
|
|
508
|
+
try { this.options.onExpired(result); } catch (err) { }
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
this.close(() => {
|
|
512
|
+
if (this.options.expiredUrl) {
|
|
513
|
+
const delim = this.options.expiredUrl.includes('?') ? '&' : '?';
|
|
514
|
+
window.location.href = `${this.options.expiredUrl}${delim}order_id=${encodeURIComponent(result.order_id)}`;
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
} else if (event === 'elpay.modal_close' || event === 'elpay.close' || event === 'elpay.cancel' || event === 'elpay.payment_cancelled') {
|
|
518
|
+
this._dispatchCancelled();
|
|
519
|
+
const redirectUrl = e.data.redirectUrl;
|
|
520
|
+
this.close(() => {
|
|
521
|
+
if (this.options.cancelledUrl) {
|
|
522
|
+
const delim = this.options.cancelledUrl.includes('?') ? '&' : '?';
|
|
523
|
+
window.location.href = `${this.options.cancelledUrl}${delim}order_id=${encodeURIComponent(this.options.orderId || '')}`;
|
|
524
|
+
} else if (redirectUrl && this._isSafeRedirect(redirectUrl) && redirectUrl !== window.location.href && !redirectUrl.includes('/checkout')) {
|
|
525
|
+
window.location.href = redirectUrl;
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
_dispatchCancelled() {
|
|
532
|
+
const result = {
|
|
533
|
+
status: 'CANCELLED',
|
|
534
|
+
order_id: this.options.orderId || this.options.order_id || '',
|
|
535
|
+
timestamp: new Date().toISOString()
|
|
536
|
+
};
|
|
537
|
+
ElpayCheckout._emitEvent('elpay:payment:cancelled', result);
|
|
538
|
+
ElpayCheckout._emitEvent('elpay:payment:result', result);
|
|
539
|
+
|
|
540
|
+
if (typeof this.options.onCancelled === 'function') {
|
|
541
|
+
try { this.options.onCancelled(result); } catch (e) { }
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
static _emitEvent(name, detail) {
|
|
546
|
+
if (!isBrowser) return;
|
|
547
|
+
try {
|
|
548
|
+
const ev = new CustomEvent(name, { detail, bubbles: true, cancelable: true });
|
|
549
|
+
window.dispatchEvent(ev);
|
|
550
|
+
document.dispatchEvent(ev);
|
|
551
|
+
} catch (e) { }
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Handles clicks on elements with [data-elpay-pay] attribute
|
|
556
|
+
*/
|
|
557
|
+
static handleButtonClick(btn) {
|
|
558
|
+
if (!btn) return;
|
|
559
|
+
|
|
560
|
+
const orderId = btn.getAttribute('data-order-id') || `ORDER-${Date.now()}`;
|
|
561
|
+
const amount = btn.getAttribute('data-amount') || '0';
|
|
562
|
+
const currency = btn.getAttribute('data-currency') || scriptConfig.currency || 'INR';
|
|
563
|
+
const name = btn.getAttribute('data-name') || scriptConfig.name || 'Merchant Store';
|
|
564
|
+
const description = btn.getAttribute('data-description') || 'Order Payment';
|
|
565
|
+
|
|
566
|
+
// Customer info
|
|
567
|
+
const customer = {
|
|
568
|
+
name: btn.getAttribute('data-customer-name') || '',
|
|
569
|
+
email: btn.getAttribute('data-customer-email') || '',
|
|
570
|
+
phone: btn.getAttribute('data-customer-phone') || btn.getAttribute('data-customer-mobile') || ''
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
// Metadata parsing
|
|
574
|
+
let metadata = {};
|
|
575
|
+
const rawMeta = btn.getAttribute('data-metadata');
|
|
576
|
+
if (rawMeta) {
|
|
577
|
+
try {
|
|
578
|
+
metadata = JSON.parse(rawMeta);
|
|
579
|
+
} catch (e) {
|
|
580
|
+
metadata = { raw: rawMeta };
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
// Gather any data-meta-* attributes
|
|
584
|
+
for (const attr of btn.attributes) {
|
|
585
|
+
if (attr.name.startsWith('data-meta-')) {
|
|
586
|
+
const key = attr.name.slice(10);
|
|
587
|
+
metadata[key] = attr.value;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const checkoutUrl = btn.getAttribute('data-checkout-url') || '';
|
|
592
|
+
const paymentId = btn.getAttribute('data-payment-id') || '';
|
|
593
|
+
const successUrl = btn.getAttribute('data-success-url') || scriptConfig.successUrl || '';
|
|
594
|
+
const failedUrl = btn.getAttribute('data-failed-url') || scriptConfig.failedUrl || '';
|
|
595
|
+
const suspiciousUrl = btn.getAttribute('data-suspicious-url') || scriptConfig.suspiciousUrl || '';
|
|
596
|
+
const expiredUrl = btn.getAttribute('data-expired-url') || scriptConfig.expiredUrl || '';
|
|
597
|
+
const cancelledUrl = btn.getAttribute('data-cancelled-url') || scriptConfig.cancelledUrl || '';
|
|
598
|
+
|
|
599
|
+
ElpayCheckout.pay({
|
|
600
|
+
order_id: orderId,
|
|
601
|
+
orderId: orderId,
|
|
602
|
+
amount: amount,
|
|
603
|
+
currency: currency,
|
|
604
|
+
name: name,
|
|
605
|
+
description: description,
|
|
606
|
+
customer: customer,
|
|
607
|
+
metadata: metadata,
|
|
608
|
+
checkoutUrl: checkoutUrl,
|
|
609
|
+
paymentId: paymentId,
|
|
610
|
+
successUrl: successUrl,
|
|
611
|
+
failedUrl: failedUrl,
|
|
612
|
+
suspiciousUrl: suspiciousUrl,
|
|
613
|
+
expiredUrl: expiredUrl,
|
|
614
|
+
cancelledUrl: cancelledUrl
|
|
615
|
+
}).catch(err => {
|
|
616
|
+
console.error('[ELPAY JS] Pay error:', err);
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
static _ensureStyles() {
|
|
621
|
+
if (!isBrowser) return;
|
|
622
|
+
let styleEl = document.getElementById('elpay-checkout-styles');
|
|
623
|
+
if (!styleEl) {
|
|
624
|
+
styleEl = document.createElement('style');
|
|
625
|
+
styleEl.id = 'elpay-checkout-styles';
|
|
626
|
+
document.head.appendChild(styleEl);
|
|
627
|
+
}
|
|
628
|
+
styleEl.textContent = `
|
|
629
|
+
.elpay-checkout-backdrop {
|
|
630
|
+
position: fixed;
|
|
631
|
+
top: 0;
|
|
632
|
+
left: 0;
|
|
633
|
+
width: 100vw;
|
|
634
|
+
width: 100%;
|
|
635
|
+
height: 100vh;
|
|
636
|
+
height: 100dvh;
|
|
637
|
+
height: 100%;
|
|
638
|
+
background: rgba(12, 35, 64, 0.76);
|
|
639
|
+
backdrop-filter: blur(6px);
|
|
640
|
+
-webkit-backdrop-filter: blur(6px);
|
|
641
|
+
z-index: 2147483640;
|
|
642
|
+
display: flex;
|
|
643
|
+
align-items: center;
|
|
644
|
+
justify-content: center;
|
|
645
|
+
opacity: 0;
|
|
646
|
+
transition: opacity 0.25s ease-out;
|
|
647
|
+
touch-action: pan-y;
|
|
648
|
+
overscroll-behavior: contain;
|
|
649
|
+
user-select: none;
|
|
650
|
+
-webkit-user-select: none;
|
|
651
|
+
-webkit-tap-highlight-color: transparent;
|
|
652
|
+
}
|
|
653
|
+
.elpay-checkout-backdrop.active {
|
|
654
|
+
opacity: 1;
|
|
655
|
+
}
|
|
656
|
+
.elpay-checkout-container {
|
|
657
|
+
position: relative;
|
|
658
|
+
width: 440px;
|
|
659
|
+
max-width: 95vw;
|
|
660
|
+
height: 680px;
|
|
661
|
+
max-height: 92vh;
|
|
662
|
+
background: #ffffff;
|
|
663
|
+
border-radius: 14px;
|
|
664
|
+
box-shadow: 0 25px 65px rgba(0, 0, 0, 0.5);
|
|
665
|
+
overflow: hidden;
|
|
666
|
+
transform: translateY(20px) scale(0.98);
|
|
667
|
+
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
|
668
|
+
display: flex;
|
|
669
|
+
flex-direction: column;
|
|
670
|
+
touch-action: auto !important;
|
|
671
|
+
user-select: auto !important;
|
|
672
|
+
-webkit-user-select: auto !important;
|
|
673
|
+
}
|
|
674
|
+
.elpay-checkout-container.active {
|
|
675
|
+
transform: translateY(0) scale(1);
|
|
676
|
+
}
|
|
677
|
+
.elpay-checkout-container.closing {
|
|
678
|
+
transform: translateY(20px) scale(0.96);
|
|
679
|
+
}
|
|
680
|
+
.elpay-checkout-iframe {
|
|
681
|
+
width: 100%;
|
|
682
|
+
height: 100%;
|
|
683
|
+
border: none;
|
|
684
|
+
flex: 1;
|
|
685
|
+
background: #ffffff;
|
|
686
|
+
touch-action: auto !important;
|
|
687
|
+
}
|
|
688
|
+
.elpay-checkout-close-btn {
|
|
689
|
+
position: absolute;
|
|
690
|
+
top: 14px;
|
|
691
|
+
right: 14px;
|
|
692
|
+
width: 28px;
|
|
693
|
+
height: 28px;
|
|
694
|
+
border-radius: 50%;
|
|
695
|
+
background: rgba(255, 255, 255, 0.2);
|
|
696
|
+
border: none;
|
|
697
|
+
color: #ffffff;
|
|
698
|
+
font-size: 14px;
|
|
699
|
+
cursor: pointer;
|
|
700
|
+
z-index: 10;
|
|
701
|
+
display: flex;
|
|
702
|
+
align-items: center;
|
|
703
|
+
justify-content: center;
|
|
704
|
+
transition: background 0.15s;
|
|
705
|
+
}
|
|
706
|
+
.elpay-checkout-close-btn:hover {
|
|
707
|
+
background: rgba(255, 255, 255, 0.4);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/* DESKTOP / PC MODAL STYLES (Razorpay 2-Column Exact Layout) */
|
|
711
|
+
@media (min-width: 768px) {
|
|
712
|
+
.elpay-checkout-container {
|
|
713
|
+
width: 900px;
|
|
714
|
+
max-width: 95vw;
|
|
715
|
+
height: 600px;
|
|
716
|
+
max-height: 92vh;
|
|
717
|
+
border-radius: 16px;
|
|
718
|
+
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.45);
|
|
719
|
+
}
|
|
720
|
+
.elpay-checkout-close-btn {
|
|
721
|
+
display: none; /* Native in-modal close button inside the checkout header bar */
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/* MOBILE BOTTOM DRAWER / BOTTOM SHEET STYLES */
|
|
726
|
+
@media (max-width: 767px) {
|
|
727
|
+
.elpay-checkout-backdrop {
|
|
728
|
+
position: fixed !important;
|
|
729
|
+
inset: 0 !important;
|
|
730
|
+
top: 0 !important;
|
|
731
|
+
left: 0 !important;
|
|
732
|
+
right: 0 !important;
|
|
733
|
+
bottom: 0 !important;
|
|
734
|
+
width: 100vw !important;
|
|
735
|
+
height: 100vh !important;
|
|
736
|
+
display: flex !important;
|
|
737
|
+
align-items: flex-end !important;
|
|
738
|
+
justify-content: center !important;
|
|
739
|
+
padding: 0 !important;
|
|
740
|
+
margin: 0 !important;
|
|
741
|
+
}
|
|
742
|
+
.elpay-checkout-container {
|
|
743
|
+
width: 100vw !important;
|
|
744
|
+
max-width: 100vw !important;
|
|
745
|
+
height: 90vh !important;
|
|
746
|
+
height: 90dvh !important;
|
|
747
|
+
max-height: 94vh !important;
|
|
748
|
+
border-bottom-left-radius: 0 !important;
|
|
749
|
+
border-bottom-right-radius: 0 !important;
|
|
750
|
+
border-top-left-radius: 20px !important;
|
|
751
|
+
border-top-right-radius: 20px !important;
|
|
752
|
+
margin: 0 !important;
|
|
753
|
+
transform: translateY(0) !important;
|
|
754
|
+
box-shadow: 0 -10px 40px rgba(0, 0, 0, 0.6) !important;
|
|
755
|
+
touch-action: auto !important;
|
|
756
|
+
overflow: hidden !important;
|
|
757
|
+
}
|
|
758
|
+
.elpay-checkout-container.closing {
|
|
759
|
+
transform: translateY(100%) !important;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
`;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// Auto-bind click listener for all [data-elpay-pay] buttons on page
|
|
767
|
+
if (isBrowser) {
|
|
768
|
+
document.addEventListener('click', function (e) {
|
|
769
|
+
const btn = e.target && e.target.closest && e.target.closest('[data-elpay-pay]');
|
|
770
|
+
if (btn) {
|
|
771
|
+
e.preventDefault();
|
|
772
|
+
ElpayCheckout.handleButtonClick(btn);
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Export globals
|
|
778
|
+
globalScope.ElpayCheckout = ElpayCheckout;
|
|
779
|
+
globalScope.Elpay = ElpayCheckout;
|
|
780
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
781
|
+
module.exports = ElpayCheckout;
|
|
782
|
+
module.exports.ElpayCheckout = ElpayCheckout;
|
|
783
|
+
module.exports.Elpay = ElpayCheckout;
|
|
784
|
+
}
|
|
785
|
+
})(typeof window !== 'undefined' ? window : globalThis);
|