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.
- 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 +2421 -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
|
@@ -0,0 +1,2839 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
7
|
+
<title>{{MERCHANT_NAME}} · {{APP_NAME}} Checkout</title>
|
|
8
|
+
<link rel="stylesheet" href="/css/checkout.css">
|
|
9
|
+
</head>
|
|
10
|
+
|
|
11
|
+
<body>
|
|
12
|
+
|
|
13
|
+
<!-- include:partials/splash.html -->
|
|
14
|
+
|
|
15
|
+
<!-- ELPAY PG CHECKOUT CARD / DRAWER WRAPPER -->
|
|
16
|
+
<div class="ypg-wrapper">
|
|
17
|
+
|
|
18
|
+
<!-- include:partials/sidebar.html -->
|
|
19
|
+
|
|
20
|
+
<!-- include:partials/header.html -->
|
|
21
|
+
|
|
22
|
+
<!-- 3. SCROLLABLE BODY -->
|
|
23
|
+
<div class="ypg-body">
|
|
24
|
+
<!-- include:partials/auth.html -->
|
|
25
|
+
<!-- include:partials/payment-methods.html -->
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<!-- include:partials/footer.html -->
|
|
29
|
+
|
|
30
|
+
<!-- include:partials/modals.html -->
|
|
31
|
+
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<script id="ypgInitialState" type="application/json">{{INITIAL_STATE_JSON}}</script>
|
|
35
|
+
<script>
|
|
36
|
+
const PAYMENT_ID = "{{PAYMENT_ID}}";
|
|
37
|
+
const API_BASE = "{{API_BASE_URL}}";
|
|
38
|
+
|
|
39
|
+
let state = null;
|
|
40
|
+
let countdownTimer = null;
|
|
41
|
+
let queuePollTimer = null;
|
|
42
|
+
let sse = null;
|
|
43
|
+
let activeCustomer = null;
|
|
44
|
+
let activeToken = null;
|
|
45
|
+
|
|
46
|
+
const STORAGE_KEY_IDENTITY = 'elpay.customer.identity';
|
|
47
|
+
const STORAGE_KEY_ACCOUNTS = 'elpay.customer.accounts';
|
|
48
|
+
const STORAGE_KEY_ACTIVE = 'elpay.customer.activeId';
|
|
49
|
+
const MAX_IDENTITY_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days (maximum 1 week)
|
|
50
|
+
|
|
51
|
+
function getPreservedIdentity() {
|
|
52
|
+
try {
|
|
53
|
+
const raw = localStorage.getItem(STORAGE_KEY_IDENTITY);
|
|
54
|
+
if (!raw) return null;
|
|
55
|
+
const data = JSON.parse(raw);
|
|
56
|
+
if (!data || !data.customer) {
|
|
57
|
+
clearPreservedIdentity();
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
const expiry = data.expiresAt || ((data.savedAt || now) + MAX_IDENTITY_AGE_MS);
|
|
62
|
+
if (now > expiry) {
|
|
63
|
+
console.log('[Auth] Preserved identity expired (> 1 week). Purging.');
|
|
64
|
+
clearPreservedIdentity();
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
return data;
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function savePreservedIdentity(cust, token) {
|
|
74
|
+
if (!cust || !cust.id) return;
|
|
75
|
+
try {
|
|
76
|
+
const now = Date.now();
|
|
77
|
+
const cleanCust = {
|
|
78
|
+
id: cust.id,
|
|
79
|
+
name: cust.name || cust.fullName || 'Customer',
|
|
80
|
+
email: cust.email || '',
|
|
81
|
+
mobileNumber: cust.mobileNumber || cust.phoneNumber || '',
|
|
82
|
+
status: cust.status || 'ACTIVE'
|
|
83
|
+
};
|
|
84
|
+
const resolvedToken = token || activeToken || localStorage.getItem('ypg_customer_token') || null;
|
|
85
|
+
const payload = {
|
|
86
|
+
customer: cleanCust,
|
|
87
|
+
token: resolvedToken,
|
|
88
|
+
savedAt: now,
|
|
89
|
+
expiresAt: now + MAX_IDENTITY_AGE_MS
|
|
90
|
+
};
|
|
91
|
+
// 1. Preserve 1-week identity
|
|
92
|
+
localStorage.setItem(STORAGE_KEY_IDENTITY, JSON.stringify(payload));
|
|
93
|
+
// 2. Strict single-user saved account store
|
|
94
|
+
localStorage.setItem(STORAGE_KEY_ACCOUNTS, JSON.stringify([cleanCust]));
|
|
95
|
+
localStorage.setItem(STORAGE_KEY_ACTIVE, cust.id);
|
|
96
|
+
if (resolvedToken) {
|
|
97
|
+
localStorage.setItem('ypg_customer_token', resolvedToken);
|
|
98
|
+
}
|
|
99
|
+
} catch { }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function clearPreservedIdentity() {
|
|
103
|
+
try {
|
|
104
|
+
localStorage.removeItem(STORAGE_KEY_IDENTITY);
|
|
105
|
+
localStorage.removeItem(STORAGE_KEY_ACCOUNTS);
|
|
106
|
+
localStorage.removeItem(STORAGE_KEY_ACTIVE);
|
|
107
|
+
localStorage.removeItem('ypg_customer_token');
|
|
108
|
+
} catch { }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Immediately restore 1-week preserved identity if present
|
|
112
|
+
const initPreserved = getPreservedIdentity();
|
|
113
|
+
if (initPreserved && initPreserved.customer) {
|
|
114
|
+
activeCustomer = initPreserved.customer;
|
|
115
|
+
activeToken = initPreserved.token || null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Notify parent if embedded in iframe
|
|
119
|
+
if (window.parent !== window || location.search.includes('embedded=1')) {
|
|
120
|
+
document.body.classList.add('is-embedded');
|
|
121
|
+
try {
|
|
122
|
+
postToMerchant({ event: 'elpay.modal_ready', paymentId: PAYMENT_ID });
|
|
123
|
+
} catch (e) { }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Dismiss Initial Splash Screen smoothly after brief intro
|
|
127
|
+
function dismissSplashScreen() {
|
|
128
|
+
const splash = document.getElementById('ypgSplashScreen');
|
|
129
|
+
if (splash && !splash.classList.contains('fade-out')) {
|
|
130
|
+
setTimeout(() => {
|
|
131
|
+
splash.classList.add('fade-out');
|
|
132
|
+
}, 550);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Toast helper
|
|
137
|
+
let toastTimer = null;
|
|
138
|
+
function showOptionToast(msg) {
|
|
139
|
+
const toast = document.getElementById('ypgToast');
|
|
140
|
+
if (!toast) return;
|
|
141
|
+
toast.textContent = msg;
|
|
142
|
+
toast.classList.add('show');
|
|
143
|
+
if (toastTimer) clearTimeout(toastTimer);
|
|
144
|
+
toastTimer = setTimeout(() => {
|
|
145
|
+
toast.classList.remove('show');
|
|
146
|
+
}, 2500);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Toggle Order Breakdown Drawer
|
|
150
|
+
function toggleOrderDetails() {
|
|
151
|
+
const drawer = document.getElementById('orderDetailsDrawer');
|
|
152
|
+
const chevron = document.getElementById('chevronDetailsIcon');
|
|
153
|
+
if (!drawer) return;
|
|
154
|
+
const isOpen = drawer.classList.contains('open');
|
|
155
|
+
if (isOpen) {
|
|
156
|
+
drawer.classList.remove('open');
|
|
157
|
+
if (chevron) chevron.innerHTML = '▴';
|
|
158
|
+
} else {
|
|
159
|
+
drawer.classList.add('open');
|
|
160
|
+
if (chevron) chevron.innerHTML = '▾';
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Failure redirect URL resolver
|
|
165
|
+
function getFailedRedirectUrl() {
|
|
166
|
+
const orderId = state?.orderId || '';
|
|
167
|
+
const reason = state?.status || 'FAILED';
|
|
168
|
+
const paymentId = typeof PAYMENT_ID !== 'undefined' ? PAYMENT_ID : (state?.id || '');
|
|
169
|
+
|
|
170
|
+
let baseOrigin = '';
|
|
171
|
+
const safeReturn = sanitizeRedirectUrl(state && state.returnUrl);
|
|
172
|
+
if (safeReturn) {
|
|
173
|
+
baseOrigin = new URL(safeReturn).origin;
|
|
174
|
+
}
|
|
175
|
+
if (!baseOrigin && document.referrer) {
|
|
176
|
+
try {
|
|
177
|
+
baseOrigin = new URL(document.referrer).origin;
|
|
178
|
+
} catch (e) { }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const query = `orderId=${encodeURIComponent(orderId)}&reason=${encodeURIComponent(reason)}&paymentId=${encodeURIComponent(paymentId)}`;
|
|
182
|
+
return baseOrigin ? `${baseOrigin}/order-failed?${query}` : `/order-failed?${query}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Only http(s) URLs may be used as a redirect target. `new URL()` lowercases
|
|
186
|
+
// the scheme, so this rejects `javascript:`, `JaVaScRiPt:`, `data:` and
|
|
187
|
+
// `vbscript:` regardless of the casing or leading whitespace used.
|
|
188
|
+
function sanitizeRedirectUrl(candidate) {
|
|
189
|
+
if (typeof candidate !== 'string') return null;
|
|
190
|
+
const trimmed = candidate.trim();
|
|
191
|
+
if (!trimmed) return null;
|
|
192
|
+
let parsed;
|
|
193
|
+
try {
|
|
194
|
+
parsed = new URL(trimmed, window.location.origin);
|
|
195
|
+
} catch (e) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return null;
|
|
199
|
+
return parsed.href;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// The storefront that embeds this iframe. Used both as the postMessage
|
|
203
|
+
// target origin and to validate messages coming back from the parent.
|
|
204
|
+
// The referrer is the most trustworthy signal; when the browser strips it,
|
|
205
|
+
// elpay.js declares the parent origin in the URL (`elpayParentOrigin`).
|
|
206
|
+
function resolveMerchantOrigin() {
|
|
207
|
+
try {
|
|
208
|
+
if (document.referrer) {
|
|
209
|
+
const parsed = new URL(document.referrer);
|
|
210
|
+
if (parsed.protocol === 'http:' || parsed.protocol === 'https:') return parsed.origin;
|
|
211
|
+
}
|
|
212
|
+
} catch (e) { }
|
|
213
|
+
try {
|
|
214
|
+
const params = new URLSearchParams(window.location.search);
|
|
215
|
+
for (const key of ['elpayParentOrigin', 'origin', 'merchantOrigin']) {
|
|
216
|
+
const declared = params.get(key);
|
|
217
|
+
if (!declared) continue;
|
|
218
|
+
const parsed = new URL(declared, window.location.origin);
|
|
219
|
+
if (parsed.protocol === 'http:' || parsed.protocol === 'https:') return parsed.origin;
|
|
220
|
+
}
|
|
221
|
+
} catch (e) { }
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Posts to the embedding storefront. The message carries payment data, so it
|
|
226
|
+
// is only ever sent to a known merchant origin: when the origin cannot be
|
|
227
|
+
// resolved the message is dropped rather than broadcast to every origin.
|
|
228
|
+
function postToMerchant(message) {
|
|
229
|
+
const target = resolveMerchantOrigin();
|
|
230
|
+
if (!target) return;
|
|
231
|
+
try {
|
|
232
|
+
window.parent.postMessage(message, target);
|
|
233
|
+
} catch (e) { }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Store return URL resolver (used when user cancels payment or returns to merchant store)
|
|
237
|
+
function getStoreReturnUrl() {
|
|
238
|
+
if (typeof state !== 'undefined' && state) {
|
|
239
|
+
const stateCancel = sanitizeRedirectUrl(state.cancelUrl);
|
|
240
|
+
if (stateCancel) return stateCancel;
|
|
241
|
+
if (state.returnUrl) {
|
|
242
|
+
const safeReturn = sanitizeRedirectUrl(state.returnUrl);
|
|
243
|
+
if (safeReturn) {
|
|
244
|
+
try {
|
|
245
|
+
const parsed = new URL(safeReturn);
|
|
246
|
+
return parsed.origin ? `${parsed.origin}/` : safeReturn;
|
|
247
|
+
} catch (e) { }
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
const params = new URLSearchParams(window.location.search);
|
|
253
|
+
const cancelParam = params.get('cancelUrl') || params.get('cancel_url');
|
|
254
|
+
const safeCancel = sanitizeRedirectUrl(cancelParam);
|
|
255
|
+
if (safeCancel) return safeCancel;
|
|
256
|
+
const qUrl = params.get('returnUrl') || params.get('storeUrl') || params.get('redirectUrl');
|
|
257
|
+
const safeQueryUrl = sanitizeRedirectUrl(qUrl);
|
|
258
|
+
if (safeQueryUrl) {
|
|
259
|
+
try {
|
|
260
|
+
const parsed = new URL(safeQueryUrl);
|
|
261
|
+
return parsed.origin ? `${parsed.origin}/` : safeQueryUrl;
|
|
262
|
+
} catch (e) { }
|
|
263
|
+
}
|
|
264
|
+
} catch (e) { }
|
|
265
|
+
if (document.referrer) {
|
|
266
|
+
try {
|
|
267
|
+
const refUrl = new URL(document.referrer);
|
|
268
|
+
if (refUrl.host !== window.location.host) {
|
|
269
|
+
return `${refUrl.origin}/`;
|
|
270
|
+
}
|
|
271
|
+
} catch (e) { }
|
|
272
|
+
}
|
|
273
|
+
if (window.location.port === '7000' || window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
|
|
274
|
+
return 'http://localhost:5000/';
|
|
275
|
+
}
|
|
276
|
+
return '/';
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Retry / Close on Expired or Failed session
|
|
280
|
+
function handleRetryNewPayment() {
|
|
281
|
+
const failRedirectUrl = getFailedRedirectUrl();
|
|
282
|
+
const orderId = state?.orderId || '';
|
|
283
|
+
const reason = state?.status || 'FAILED';
|
|
284
|
+
const paymentId = typeof PAYMENT_ID !== 'undefined' ? PAYMENT_ID : (state?.id || '');
|
|
285
|
+
|
|
286
|
+
const isEmbedded = (window.parent !== window) || location.search.includes('embedded=1');
|
|
287
|
+
|
|
288
|
+
if (isEmbedded) {
|
|
289
|
+
try {
|
|
290
|
+
postToMerchant({
|
|
291
|
+
event: 'elpay.payment_failed',
|
|
292
|
+
data: {
|
|
293
|
+
orderId,
|
|
294
|
+
reason,
|
|
295
|
+
paymentId,
|
|
296
|
+
status: reason,
|
|
297
|
+
redirectUrl: failRedirectUrl
|
|
298
|
+
},
|
|
299
|
+
orderId,
|
|
300
|
+
reason,
|
|
301
|
+
paymentId,
|
|
302
|
+
status: reason,
|
|
303
|
+
redirectUrl: failRedirectUrl
|
|
304
|
+
});
|
|
305
|
+
} catch (e) { }
|
|
306
|
+
|
|
307
|
+
// Fallback postMessage in case parent container only listens for modal_close
|
|
308
|
+
try {
|
|
309
|
+
postToMerchant({
|
|
310
|
+
event: 'elpay.modal_close',
|
|
311
|
+
action: 'payment_failed',
|
|
312
|
+
redirectUrl: failRedirectUrl
|
|
313
|
+
});
|
|
314
|
+
} catch (e) { }
|
|
315
|
+
} else {
|
|
316
|
+
// Standalone full page checkout
|
|
317
|
+
window.location.href = failRedirectUrl;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// --- ACTION HOOK ENGINE (Dispatches triggers to merchant website and backend telemetry) ---
|
|
322
|
+
function emitTriggerHook(action, details = {}) {
|
|
323
|
+
try {
|
|
324
|
+
postToMerchant({
|
|
325
|
+
type: 'ELPAY_PAYMENT_HOOK',
|
|
326
|
+
action,
|
|
327
|
+
paymentId: PAYMENT_ID,
|
|
328
|
+
orderId: state?.orderId,
|
|
329
|
+
details
|
|
330
|
+
});
|
|
331
|
+
} catch { }
|
|
332
|
+
|
|
333
|
+
fetch(`${API_BASE}/api/v1/checkout/${PAYMENT_ID}/trigger-hook`, {
|
|
334
|
+
method: 'POST',
|
|
335
|
+
headers: { 'Content-Type': 'application/json' },
|
|
336
|
+
body: JSON.stringify({ action, details })
|
|
337
|
+
}).catch(() => { });
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// --- SAVED / REMEMBERED CUSTOMER ACCOUNT (STRICT SINGLE USER) ---
|
|
341
|
+
function getRememberedAccounts() {
|
|
342
|
+
try {
|
|
343
|
+
const preserved = getPreservedIdentity();
|
|
344
|
+
if (preserved && preserved.customer) {
|
|
345
|
+
return [preserved.customer];
|
|
346
|
+
}
|
|
347
|
+
const raw = localStorage.getItem(STORAGE_KEY_ACCOUNTS);
|
|
348
|
+
if (!raw) return [];
|
|
349
|
+
const parsed = JSON.parse(raw);
|
|
350
|
+
if (!Array.isArray(parsed) || parsed.length === 0) return [];
|
|
351
|
+
return [parsed[0]]; // Strictly single user only
|
|
352
|
+
} catch {
|
|
353
|
+
return [];
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function saveRememberedAccount(cust, token) {
|
|
358
|
+
if (!cust || !cust.id) return;
|
|
359
|
+
savePreservedIdentity(cust, token);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function removeRememberedAccount(customerId, e) {
|
|
363
|
+
if (e) {
|
|
364
|
+
e.stopPropagation();
|
|
365
|
+
e.preventDefault();
|
|
366
|
+
}
|
|
367
|
+
clearPreservedIdentity();
|
|
368
|
+
if (activeCustomer && activeCustomer.id === customerId) {
|
|
369
|
+
activeCustomer = null;
|
|
370
|
+
activeToken = null;
|
|
371
|
+
}
|
|
372
|
+
updateCustomerDisplay(null);
|
|
373
|
+
renderSavedAccountsUI();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function renderSavedAccountsUI() {
|
|
377
|
+
const accounts = getRememberedAccounts();
|
|
378
|
+
const container = document.getElementById('preAuthSavedAccountsContainer');
|
|
379
|
+
const listEl = document.getElementById('savedAccountsList');
|
|
380
|
+
const badge = document.getElementById('savedAccountCountBadge');
|
|
381
|
+
|
|
382
|
+
if (badge) badge.style.display = 'none';
|
|
383
|
+
|
|
384
|
+
if (container && listEl) {
|
|
385
|
+
if (accounts.length > 0) {
|
|
386
|
+
container.hidden = false;
|
|
387
|
+
let html = '';
|
|
388
|
+
accounts.slice(0, 1).forEach(acc => {
|
|
389
|
+
const initial = (acc.name || 'U').charAt(0).toUpperCase();
|
|
390
|
+
const isActive = activeCustomer && activeCustomer.id === acc.id;
|
|
391
|
+
html += `
|
|
392
|
+
<div class="saved-account-row ${isActive ? 'active' : ''}" onclick="handleSelectSavedAccount('${acc.id}', event)">
|
|
393
|
+
<div style="display:flex; align-items:center; gap:8px;">
|
|
394
|
+
<div style="width:28px; height:28px; border-radius:50%; background:#e0f2fe; color:#0369a1; font-weight:800; font-size:12px; display:flex; align-items:center; justify-content:center;">${initial}</div>
|
|
395
|
+
<div>
|
|
396
|
+
<div style="font-size:13px; font-weight:700;">${escapeHtml(acc.name)}</div>
|
|
397
|
+
<div style="font-size:11px; color:#64748b;">${escapeHtml(acc.email || '')} ${acc.mobileNumber ? '• ' + escapeHtml(acc.mobileNumber) : ''}</div>
|
|
398
|
+
</div>
|
|
399
|
+
</div>
|
|
400
|
+
<div style="display:flex; align-items:center; gap:6px;">
|
|
401
|
+
<button type="button" class="ypg-btn-select-account" style="background:#eff6ff; border:1px solid #bfdbfe; color:var(--ypg-blue); font-size:11px; font-weight:700; padding:4px 10px; border-radius:6px; cursor:pointer; display:inline-flex; align-items:center; gap:2px; transition:all 0.15s;" onclick="handleSelectSavedAccount('${acc.id}', event)">
|
|
402
|
+
<span>Select</span> ›
|
|
403
|
+
</button>
|
|
404
|
+
<button type="button" style="background:none; border:none; color:#94a3b8; font-size:14px; cursor:pointer; padding:2px 6px;" onclick="removeRememberedAccount('${acc.id}', event)" title="Remove Account">✕</button>
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
407
|
+
`;
|
|
408
|
+
});
|
|
409
|
+
listEl.innerHTML = html;
|
|
410
|
+
} else {
|
|
411
|
+
container.hidden = true;
|
|
412
|
+
listEl.innerHTML = '';
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
let currentAuthMobile = '';
|
|
418
|
+
let currentAuthName = '';
|
|
419
|
+
let currentAuthEmail = '';
|
|
420
|
+
let currentAuthStep = 'mobile';
|
|
421
|
+
let hasAttemptedAutoLogin = false;
|
|
422
|
+
let otpCooldownTimer = null;
|
|
423
|
+
let otpCooldownSec = 0;
|
|
424
|
+
|
|
425
|
+
function setGatewayAuthMode(isAuthView) {
|
|
426
|
+
const authEl = document.getElementById('preGatewayAuthView');
|
|
427
|
+
const payEl = document.getElementById('paymentGatewayInterface');
|
|
428
|
+
const footerEl = document.getElementById('ypgStickyFooter');
|
|
429
|
+
const pcTopbarTitle = document.querySelector('.ypg-pc-topbar-title');
|
|
430
|
+
|
|
431
|
+
if (isAuthView) {
|
|
432
|
+
if (authEl) {
|
|
433
|
+
authEl.hidden = false;
|
|
434
|
+
authEl.style.removeProperty('display');
|
|
435
|
+
}
|
|
436
|
+
if (payEl) {
|
|
437
|
+
payEl.hidden = true;
|
|
438
|
+
payEl.style.setProperty('display', 'none', 'important');
|
|
439
|
+
}
|
|
440
|
+
if (footerEl) {
|
|
441
|
+
footerEl.style.display = 'none';
|
|
442
|
+
}
|
|
443
|
+
if (pcTopbarTitle) {
|
|
444
|
+
pcTopbarTitle.textContent = 'Sign in to Pay';
|
|
445
|
+
}
|
|
446
|
+
} else {
|
|
447
|
+
if (authEl) {
|
|
448
|
+
authEl.hidden = true;
|
|
449
|
+
authEl.style.setProperty('display', 'none', 'important');
|
|
450
|
+
}
|
|
451
|
+
if (payEl) {
|
|
452
|
+
payEl.hidden = false;
|
|
453
|
+
payEl.style.removeProperty('display');
|
|
454
|
+
}
|
|
455
|
+
if (footerEl) {
|
|
456
|
+
footerEl.style.display = '';
|
|
457
|
+
}
|
|
458
|
+
if (pcTopbarTitle) {
|
|
459
|
+
pcTopbarTitle.textContent = 'Payment Options';
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function goToAuthStep(step) {
|
|
465
|
+
currentAuthStep = step;
|
|
466
|
+
const stepMobile = document.getElementById('authStepMobile');
|
|
467
|
+
const stepSignup = document.getElementById('authStepSignup');
|
|
468
|
+
const stepOtp = document.getElementById('authStepOtp');
|
|
469
|
+
const title = document.getElementById('authViewMainTitle');
|
|
470
|
+
const subtitle = document.getElementById('authViewSubTitle');
|
|
471
|
+
const savedAccountsBox = document.getElementById('preAuthSavedAccountsContainer');
|
|
472
|
+
|
|
473
|
+
if (step === 'mobile') {
|
|
474
|
+
if (stepMobile) stepMobile.hidden = false;
|
|
475
|
+
if (stepSignup) stepSignup.hidden = true;
|
|
476
|
+
if (stepOtp) stepOtp.hidden = true;
|
|
477
|
+
if (savedAccountsBox && getRememberedAccounts().length > 0) savedAccountsBox.hidden = false;
|
|
478
|
+
if (title) title.innerText = 'Sign in to Pay';
|
|
479
|
+
if (subtitle) subtitle.innerText = 'Enter your 10-digit WhatsApp mobile number to continue';
|
|
480
|
+
const input = document.getElementById('authMobileInput');
|
|
481
|
+
if (input) {
|
|
482
|
+
if (currentAuthMobile && !input.value) {
|
|
483
|
+
input.value = currentAuthMobile;
|
|
484
|
+
}
|
|
485
|
+
if (document.activeElement !== input) {
|
|
486
|
+
setTimeout(() => input.focus(), 50);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
} else if (step === 'signup') {
|
|
490
|
+
if (stepMobile) stepMobile.hidden = true;
|
|
491
|
+
if (stepSignup) stepSignup.hidden = false;
|
|
492
|
+
if (stepOtp) stepOtp.hidden = true;
|
|
493
|
+
if (savedAccountsBox) savedAccountsBox.hidden = true;
|
|
494
|
+
if (title) title.innerText = 'Create Account';
|
|
495
|
+
if (subtitle) subtitle.innerText = 'Complete your profile to proceed with instant checkout';
|
|
496
|
+
const badge = document.getElementById('authSignupPhoneBadge');
|
|
497
|
+
if (badge) badge.innerText = '+91 ' + currentAuthMobile;
|
|
498
|
+
const nameInput = document.getElementById('authSignupName');
|
|
499
|
+
const emailInput = document.getElementById('authSignupEmail');
|
|
500
|
+
if (nameInput && currentAuthName && !nameInput.value) nameInput.value = currentAuthName;
|
|
501
|
+
if (emailInput && currentAuthEmail && !emailInput.value) emailInput.value = currentAuthEmail;
|
|
502
|
+
if (nameInput && document.activeElement !== nameInput && document.activeElement !== emailInput) {
|
|
503
|
+
setTimeout(() => nameInput.focus(), 50);
|
|
504
|
+
}
|
|
505
|
+
} else if (step === 'otp') {
|
|
506
|
+
if (stepMobile) stepMobile.hidden = true;
|
|
507
|
+
if (stepSignup) stepSignup.hidden = true;
|
|
508
|
+
if (stepOtp) stepOtp.hidden = false;
|
|
509
|
+
if (savedAccountsBox) savedAccountsBox.hidden = true;
|
|
510
|
+
if (title) title.innerText = 'Verify Mobile OTP';
|
|
511
|
+
if (subtitle) subtitle.innerText = 'Enter the 6-digit code sent to your WhatsApp number';
|
|
512
|
+
const badge = document.getElementById('authOtpPhoneBadge');
|
|
513
|
+
if (badge) badge.innerText = '+91 ' + currentAuthMobile;
|
|
514
|
+
const otpInput = document.getElementById('authOtpInput');
|
|
515
|
+
if (otpInput) {
|
|
516
|
+
otpInput.value = '';
|
|
517
|
+
setTimeout(() => otpInput.focus(), 50);
|
|
518
|
+
}
|
|
519
|
+
startOtpCooldown(30);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function startOtpCooldown(sec = 30) {
|
|
524
|
+
clearInterval(otpCooldownTimer);
|
|
525
|
+
otpCooldownSec = sec;
|
|
526
|
+
const resendBtn = document.getElementById('btnResendOtp');
|
|
527
|
+
const countdownEl = document.getElementById('otpCountdownTimer');
|
|
528
|
+
|
|
529
|
+
const updateUI = () => {
|
|
530
|
+
if (otpCooldownSec > 0) {
|
|
531
|
+
if (resendBtn) {
|
|
532
|
+
resendBtn.disabled = true;
|
|
533
|
+
resendBtn.style.opacity = '0.5';
|
|
534
|
+
resendBtn.style.cursor = 'not-allowed';
|
|
535
|
+
}
|
|
536
|
+
if (countdownEl) countdownEl.innerText = `(${otpCooldownSec}s)`;
|
|
537
|
+
otpCooldownSec--;
|
|
538
|
+
} else {
|
|
539
|
+
clearInterval(otpCooldownTimer);
|
|
540
|
+
if (resendBtn) {
|
|
541
|
+
resendBtn.disabled = false;
|
|
542
|
+
resendBtn.style.opacity = '1';
|
|
543
|
+
resendBtn.style.cursor = 'pointer';
|
|
544
|
+
}
|
|
545
|
+
if (countdownEl) countdownEl.innerText = '';
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
updateUI();
|
|
550
|
+
otpCooldownTimer = setInterval(updateUI, 1000);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
async function handleAuthMobileSubmit() {
|
|
554
|
+
const input = document.getElementById('authMobileInput');
|
|
555
|
+
const btn = document.getElementById('btnAuthContinue');
|
|
556
|
+
const raw = input ? input.value : '';
|
|
557
|
+
const clean = String(raw).replace(/\D/g, '').slice(-10);
|
|
558
|
+
|
|
559
|
+
if (!clean || clean.length !== 10) {
|
|
560
|
+
showOptionToast('Please enter a valid 10-digit mobile number');
|
|
561
|
+
input?.focus();
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
currentAuthMobile = clean;
|
|
566
|
+
const oldHtml = btn.innerHTML;
|
|
567
|
+
btn.disabled = true;
|
|
568
|
+
btn.innerHTML = '<span>Checking account...</span>';
|
|
569
|
+
|
|
570
|
+
try {
|
|
571
|
+
const res = await fetch(`${API_BASE}/api/v1/customer/check-user`, {
|
|
572
|
+
method: 'POST',
|
|
573
|
+
headers: { 'Content-Type': 'application/json' },
|
|
574
|
+
body: JSON.stringify({ mobileNumber: clean })
|
|
575
|
+
});
|
|
576
|
+
const data = await res.json();
|
|
577
|
+
|
|
578
|
+
if (!res.ok) {
|
|
579
|
+
showOptionToast(data.error || 'Failed to check mobile number');
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const accounts = getRememberedAccounts();
|
|
584
|
+
const savedMatch = accounts.find(a => String(a.mobileNumber || '').replace(/\D/g, '').slice(-10) === clean);
|
|
585
|
+
|
|
586
|
+
if (data.exists && data.customer) {
|
|
587
|
+
// Existing customer in DB -> automatically send OTP and show OTP screen
|
|
588
|
+
currentAuthName = data.customer.name;
|
|
589
|
+
currentAuthEmail = data.customer.email;
|
|
590
|
+
btn.innerHTML = '<span>Sending WhatsApp OTP...</span>';
|
|
591
|
+
|
|
592
|
+
const sendRes = await fetch(`${API_BASE}/api/v1/customer/send-otp`, {
|
|
593
|
+
method: 'POST',
|
|
594
|
+
headers: { 'Content-Type': 'application/json' },
|
|
595
|
+
body: JSON.stringify({
|
|
596
|
+
mobileNumber: clean
|
|
597
|
+
})
|
|
598
|
+
});
|
|
599
|
+
const sendData = await sendRes.json();
|
|
600
|
+
if (sendData && sendData.devOtp) lastReceivedDevOtp = sendData.devOtp;
|
|
601
|
+
|
|
602
|
+
const greetingEl = document.getElementById('authOtpGreeting');
|
|
603
|
+
if (greetingEl) {
|
|
604
|
+
greetingEl.innerHTML = `Welcome back, <strong>${escapeHtml(data.customer.name)}</strong>! Enter the 6-digit WhatsApp OTP:`;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
goToAuthStep('otp');
|
|
608
|
+
showOptionToast('OTP sent to your WhatsApp number!');
|
|
609
|
+
} else if (savedMatch) {
|
|
610
|
+
// Known customer in browser saved accounts -> dispatch OTP directly with saved profile
|
|
611
|
+
currentAuthName = savedMatch.name || 'Customer';
|
|
612
|
+
currentAuthEmail = savedMatch.email || '';
|
|
613
|
+
btn.innerHTML = '<span>Sending WhatsApp OTP...</span>';
|
|
614
|
+
|
|
615
|
+
const sendRes = await fetch(`${API_BASE}/api/v1/customer/send-otp`, {
|
|
616
|
+
method: 'POST',
|
|
617
|
+
headers: { 'Content-Type': 'application/json' },
|
|
618
|
+
body: JSON.stringify({
|
|
619
|
+
mobileNumber: clean,
|
|
620
|
+
name: currentAuthName,
|
|
621
|
+
email: currentAuthEmail
|
|
622
|
+
})
|
|
623
|
+
});
|
|
624
|
+
const sendData = await sendRes.json();
|
|
625
|
+
if (sendData && sendData.devOtp) lastReceivedDevOtp = sendData.devOtp;
|
|
626
|
+
|
|
627
|
+
const greetingEl = document.getElementById('authOtpGreeting');
|
|
628
|
+
if (greetingEl) {
|
|
629
|
+
greetingEl.innerHTML = `Welcome back, <strong>${escapeHtml(currentAuthName)}</strong>! Enter the 6-digit WhatsApp OTP:`;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
goToAuthStep('otp');
|
|
633
|
+
showOptionToast('OTP sent to your WhatsApp number!');
|
|
634
|
+
} else {
|
|
635
|
+
// New customer -> prompt for name and email
|
|
636
|
+
goToAuthStep('signup');
|
|
637
|
+
}
|
|
638
|
+
} catch (e) {
|
|
639
|
+
showOptionToast('Network error checking mobile number.');
|
|
640
|
+
} finally {
|
|
641
|
+
btn.disabled = false;
|
|
642
|
+
btn.innerHTML = oldHtml;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
async function handleAuthSignupSubmit() {
|
|
647
|
+
const nameInput = document.getElementById('authSignupName');
|
|
648
|
+
const emailInput = document.getElementById('authSignupEmail');
|
|
649
|
+
const btn = document.getElementById('btnAuthSignup');
|
|
650
|
+
|
|
651
|
+
const name = nameInput ? nameInput.value.trim() : '';
|
|
652
|
+
const email = emailInput ? emailInput.value.trim() : '';
|
|
653
|
+
|
|
654
|
+
if (!name) {
|
|
655
|
+
showOptionToast('Please enter your full name');
|
|
656
|
+
nameInput?.focus();
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
if (!email || !email.includes('@') || !email.includes('.')) {
|
|
660
|
+
showOptionToast('Please enter a valid email address');
|
|
661
|
+
emailInput?.focus();
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
currentAuthName = name;
|
|
666
|
+
currentAuthEmail = email;
|
|
667
|
+
|
|
668
|
+
const oldHtml = btn.innerHTML;
|
|
669
|
+
btn.disabled = true;
|
|
670
|
+
btn.innerHTML = '<span>Sending WhatsApp OTP...</span>';
|
|
671
|
+
|
|
672
|
+
try {
|
|
673
|
+
const res = await fetch(`${API_BASE}/api/v1/customer/send-otp`, {
|
|
674
|
+
method: 'POST',
|
|
675
|
+
headers: { 'Content-Type': 'application/json' },
|
|
676
|
+
body: JSON.stringify({
|
|
677
|
+
mobileNumber: currentAuthMobile,
|
|
678
|
+
name,
|
|
679
|
+
email,
|
|
680
|
+
isNewUser: true
|
|
681
|
+
})
|
|
682
|
+
});
|
|
683
|
+
const data = await res.json();
|
|
684
|
+
if (data && data.devOtp) lastReceivedDevOtp = data.devOtp;
|
|
685
|
+
|
|
686
|
+
if (res.ok) {
|
|
687
|
+
const greetingEl = document.getElementById('authOtpGreeting');
|
|
688
|
+
if (greetingEl) {
|
|
689
|
+
greetingEl.innerHTML = `Hello <strong>${escapeHtml(name)}</strong>! Enter the 6-digit WhatsApp OTP:`;
|
|
690
|
+
}
|
|
691
|
+
goToAuthStep('otp');
|
|
692
|
+
showOptionToast('WhatsApp verification code sent!');
|
|
693
|
+
} else {
|
|
694
|
+
showOptionToast(data.error || 'Failed to send WhatsApp OTP.');
|
|
695
|
+
}
|
|
696
|
+
} catch (e) {
|
|
697
|
+
showOptionToast('Network error sending WhatsApp OTP.');
|
|
698
|
+
} finally {
|
|
699
|
+
btn.disabled = false;
|
|
700
|
+
btn.innerHTML = oldHtml;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
async function handleAuthVerifyOtpSubmit() {
|
|
705
|
+
const otpInput = document.getElementById('authOtpInput');
|
|
706
|
+
const btn = document.getElementById('btnAuthVerifyOtp');
|
|
707
|
+
const otp = otpInput ? otpInput.value.trim() : '';
|
|
708
|
+
|
|
709
|
+
if (!otp || otp.length < 6) {
|
|
710
|
+
showOptionToast('Please enter the 6-digit WhatsApp OTP');
|
|
711
|
+
otpInput?.focus();
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const oldHtml = btn.innerHTML;
|
|
716
|
+
btn.disabled = true;
|
|
717
|
+
btn.innerHTML = '<span>Verifying OTP...</span>';
|
|
718
|
+
|
|
719
|
+
try {
|
|
720
|
+
const res = await fetch(`${API_BASE}/api/v1/customer/verify-otp`, {
|
|
721
|
+
method: 'POST',
|
|
722
|
+
headers: { 'Content-Type': 'application/json' },
|
|
723
|
+
body: JSON.stringify({
|
|
724
|
+
mobileNumber: currentAuthMobile,
|
|
725
|
+
otp,
|
|
726
|
+
name: currentAuthName,
|
|
727
|
+
email: currentAuthEmail
|
|
728
|
+
})
|
|
729
|
+
});
|
|
730
|
+
const data = await res.json();
|
|
731
|
+
|
|
732
|
+
if (res.ok && data.customer) {
|
|
733
|
+
activeCustomer = data.customer;
|
|
734
|
+
activeToken = data.token;
|
|
735
|
+
saveRememberedAccount(activeCustomer, activeToken);
|
|
736
|
+
|
|
737
|
+
// Bind session
|
|
738
|
+
await fetch(`${API_BASE}/api/v1/checkout/${PAYMENT_ID}/bind-customer`, {
|
|
739
|
+
method: 'POST',
|
|
740
|
+
headers: getApiHeaders({
|
|
741
|
+
'Content-Type': 'application/json',
|
|
742
|
+
'Authorization': `Bearer ${activeToken}`
|
|
743
|
+
}),
|
|
744
|
+
body: JSON.stringify({ token: activeToken, customerId: activeCustomer.id })
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
emitTriggerHook('AUTH_COMPLETED', { customerId: activeCustomer.id });
|
|
748
|
+
|
|
749
|
+
setGatewayAuthMode(false);
|
|
750
|
+
updateCustomerDisplay(activeCustomer);
|
|
751
|
+
loadState();
|
|
752
|
+
showOptionToast(`Welcome, ${activeCustomer.name}! Verified via WhatsApp.`);
|
|
753
|
+
} else {
|
|
754
|
+
showOptionToast(data.error || 'Invalid or expired OTP. Please try again.');
|
|
755
|
+
otpInput.select();
|
|
756
|
+
}
|
|
757
|
+
} catch (e) {
|
|
758
|
+
showOptionToast('Network error verifying OTP.');
|
|
759
|
+
} finally {
|
|
760
|
+
btn.disabled = false;
|
|
761
|
+
btn.innerHTML = oldHtml;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
async function handleResendOtp() {
|
|
766
|
+
if (otpCooldownSec > 0) return;
|
|
767
|
+
showOptionToast('Resending WhatsApp OTP...');
|
|
768
|
+
try {
|
|
769
|
+
const res = await fetch(`${API_BASE}/api/v1/customer/send-otp`, {
|
|
770
|
+
method: 'POST',
|
|
771
|
+
headers: { 'Content-Type': 'application/json' },
|
|
772
|
+
body: JSON.stringify({
|
|
773
|
+
mobileNumber: currentAuthMobile,
|
|
774
|
+
name: currentAuthName,
|
|
775
|
+
email: currentAuthEmail
|
|
776
|
+
})
|
|
777
|
+
});
|
|
778
|
+
const data = await res.json();
|
|
779
|
+
if (res.ok) {
|
|
780
|
+
startOtpCooldown(30);
|
|
781
|
+
showOptionToast('Fresh OTP dispatched to your WhatsApp number!');
|
|
782
|
+
const otpInput = document.getElementById('authOtpInput');
|
|
783
|
+
if (otpInput) {
|
|
784
|
+
otpInput.value = '';
|
|
785
|
+
otpInput.focus();
|
|
786
|
+
}
|
|
787
|
+
} else {
|
|
788
|
+
showOptionToast(data.error || 'Failed to resend OTP.');
|
|
789
|
+
}
|
|
790
|
+
} catch (e) {
|
|
791
|
+
showOptionToast('Network error resending OTP.');
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
async function handleSelectSavedAccount(accountId, event) {
|
|
796
|
+
if (event) {
|
|
797
|
+
event.stopPropagation();
|
|
798
|
+
}
|
|
799
|
+
const accounts = getRememberedAccounts();
|
|
800
|
+
const account = accounts.find(a => a.id === accountId);
|
|
801
|
+
if (!account) return;
|
|
802
|
+
|
|
803
|
+
const rawPhone = account.mobileNumber || '';
|
|
804
|
+
const clean = String(rawPhone).replace(/\D/g, '').slice(-10);
|
|
805
|
+
|
|
806
|
+
if (!clean || clean.length !== 10) {
|
|
807
|
+
showOptionToast('Saved account does not have a valid 10-digit mobile number.');
|
|
808
|
+
const input = document.getElementById('authMobileInput');
|
|
809
|
+
if (input) {
|
|
810
|
+
input.value = rawPhone;
|
|
811
|
+
input.focus();
|
|
812
|
+
}
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
currentAuthMobile = clean;
|
|
817
|
+
currentAuthName = account.name || 'Customer';
|
|
818
|
+
currentAuthEmail = account.email || '';
|
|
819
|
+
|
|
820
|
+
const input = document.getElementById('authMobileInput');
|
|
821
|
+
if (input) input.value = clean;
|
|
822
|
+
|
|
823
|
+
// Provide visual loading feedback on the clicked button if present
|
|
824
|
+
const selectBtn = event?.currentTarget?.querySelector?.('.ypg-btn-select-account') ||
|
|
825
|
+
(event?.target?.classList?.contains('ypg-btn-select-account') ? event.target : null);
|
|
826
|
+
const oldBtnHtml = selectBtn ? selectBtn.innerHTML : null;
|
|
827
|
+
if (selectBtn) {
|
|
828
|
+
selectBtn.disabled = true;
|
|
829
|
+
selectBtn.innerHTML = '<span>Sending OTP...</span>';
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
showOptionToast(`Sending WhatsApp OTP to +91 ${clean}...`);
|
|
833
|
+
|
|
834
|
+
try {
|
|
835
|
+
const res = await fetch(`${API_BASE}/api/v1/customer/send-otp`, {
|
|
836
|
+
method: 'POST',
|
|
837
|
+
headers: { 'Content-Type': 'application/json' },
|
|
838
|
+
body: JSON.stringify({
|
|
839
|
+
mobileNumber: clean,
|
|
840
|
+
name: currentAuthName,
|
|
841
|
+
email: currentAuthEmail
|
|
842
|
+
})
|
|
843
|
+
});
|
|
844
|
+
const data = await res.json();
|
|
845
|
+
if (data && data.devOtp) lastReceivedDevOtp = data.devOtp;
|
|
846
|
+
|
|
847
|
+
if (!res.ok) {
|
|
848
|
+
showOptionToast(data.error || 'Failed to send WhatsApp OTP.');
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
const greetingEl = document.getElementById('authOtpGreeting');
|
|
853
|
+
if (greetingEl) {
|
|
854
|
+
greetingEl.innerHTML = `Welcome back, <strong>${escapeHtml(account.name || 'Customer')}</strong>! Enter the 6-digit WhatsApp OTP:`;
|
|
855
|
+
}
|
|
856
|
+
const badge = document.getElementById('authOtpPhoneBadge');
|
|
857
|
+
if (badge) badge.innerText = '+91 ' + clean;
|
|
858
|
+
|
|
859
|
+
goToAuthStep('otp');
|
|
860
|
+
showOptionToast('WhatsApp verification code sent!');
|
|
861
|
+
} catch (err) {
|
|
862
|
+
console.error('[Select Saved Account Error]:', err);
|
|
863
|
+
showOptionToast('Network error sending WhatsApp OTP.');
|
|
864
|
+
} finally {
|
|
865
|
+
if (selectBtn && oldBtnHtml) {
|
|
866
|
+
selectBtn.disabled = false;
|
|
867
|
+
selectBtn.innerHTML = oldBtnHtml;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function updateCustomerDisplay(cust) {
|
|
873
|
+
const displayName = cust
|
|
874
|
+
? `${cust.fullName || cust.name || 'Customer'} (${cust.mobileNumber || cust.phoneNumber || cust.email || ''})`
|
|
875
|
+
: 'Guest User';
|
|
876
|
+
const nameElMobile = document.getElementById('popoverCustomerName');
|
|
877
|
+
if (nameElMobile) nameElMobile.textContent = displayName;
|
|
878
|
+
const nameElPc = document.getElementById('popoverCustomerNamePc');
|
|
879
|
+
if (nameElPc) nameElPc.textContent = displayName;
|
|
880
|
+
|
|
881
|
+
const pcCustomerText = document.getElementById('pcCustomerProfileText');
|
|
882
|
+
if (pcCustomerText) {
|
|
883
|
+
if (!cust) {
|
|
884
|
+
pcCustomerText.textContent = 'Using as Guest User';
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
const name = cust.fullName || cust.name || cust.customerName || '';
|
|
888
|
+
const phone = cust.mobileNumber || cust.phoneNumber || '';
|
|
889
|
+
const clean = phone ? String(phone).replace(/^\+91/, '').replace(/\D/g, '').slice(-10) : '';
|
|
890
|
+
if (name && clean) {
|
|
891
|
+
pcCustomerText.textContent = `Using as ${name} (+91 ${clean})`;
|
|
892
|
+
} else if (name) {
|
|
893
|
+
pcCustomerText.textContent = `Using as ${name}`;
|
|
894
|
+
} else if (clean) {
|
|
895
|
+
pcCustomerText.textContent = `Using as +91 ${clean}`;
|
|
896
|
+
} else if (cust.email) {
|
|
897
|
+
pcCustomerText.textContent = `Using as ${cust.email}`;
|
|
898
|
+
} else {
|
|
899
|
+
pcCustomerText.textContent = 'Using as Guest User';
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function toggleAccountMenu(e) {
|
|
905
|
+
if (e) {
|
|
906
|
+
e.stopPropagation();
|
|
907
|
+
e.preventDefault();
|
|
908
|
+
}
|
|
909
|
+
const isPc = window.innerWidth > 600 && !document.body.classList.contains('is-embedded-mobile');
|
|
910
|
+
const popover = isPc
|
|
911
|
+
? (document.getElementById('accountMenuPopoverPc') || document.getElementById('accountMenuPopover'))
|
|
912
|
+
: (document.getElementById('accountMenuPopover') || document.getElementById('accountMenuPopoverPc'));
|
|
913
|
+
if (!popover) return;
|
|
914
|
+
|
|
915
|
+
const isHidden = popover.hidden || popover.style.display === 'none' || popover.hasAttribute('hidden');
|
|
916
|
+
if (isHidden) {
|
|
917
|
+
popover.removeAttribute('hidden');
|
|
918
|
+
popover.hidden = false;
|
|
919
|
+
popover.style.display = 'block';
|
|
920
|
+
const dismissHandler = (evt) => {
|
|
921
|
+
if (!popover.contains(evt.target) &&
|
|
922
|
+
!evt.target.closest('#btnProfileMenu') &&
|
|
923
|
+
!evt.target.closest('.ypg-pc-btn-more') &&
|
|
924
|
+
!evt.target.closest('#btnPcMore') &&
|
|
925
|
+
!evt.target.closest('#pcCustomerProfileRow')) {
|
|
926
|
+
popover.setAttribute('hidden', '');
|
|
927
|
+
popover.hidden = true;
|
|
928
|
+
popover.style.display = 'none';
|
|
929
|
+
document.removeEventListener('click', dismissHandler);
|
|
930
|
+
}
|
|
931
|
+
};
|
|
932
|
+
setTimeout(() => document.addEventListener('click', dismissHandler), 10);
|
|
933
|
+
} else {
|
|
934
|
+
popover.setAttribute('hidden', '');
|
|
935
|
+
popover.hidden = true;
|
|
936
|
+
popover.style.display = 'none';
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
async function handleCustomerLogout() {
|
|
941
|
+
const popoverMobile = document.getElementById('accountMenuPopover');
|
|
942
|
+
if (popoverMobile) { popoverMobile.setAttribute('hidden', ''); popoverMobile.hidden = true; popoverMobile.style.display = 'none'; }
|
|
943
|
+
const popoverPc = document.getElementById('accountMenuPopoverPc');
|
|
944
|
+
if (popoverPc) { popoverPc.setAttribute('hidden', ''); popoverPc.hidden = true; popoverPc.style.display = 'none'; }
|
|
945
|
+
|
|
946
|
+
if (activeToken) {
|
|
947
|
+
try {
|
|
948
|
+
await fetch(`${API_BASE}/api/v1/customer/logout`, {
|
|
949
|
+
method: 'POST',
|
|
950
|
+
headers: { 'Authorization': `Bearer ${activeToken}` }
|
|
951
|
+
});
|
|
952
|
+
} catch { }
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
clearPreservedIdentity();
|
|
956
|
+
activeCustomer = null;
|
|
957
|
+
activeToken = null;
|
|
958
|
+
hasAttemptedAutoLogin = false;
|
|
959
|
+
updateCustomerDisplay(null);
|
|
960
|
+
setGatewayAuthMode(true);
|
|
961
|
+
goToAuthStep('mobile');
|
|
962
|
+
renderSavedAccountsUI();
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function escapeHtml(str) {
|
|
966
|
+
if (!str) return '';
|
|
967
|
+
return String(str).replace(/[&<>"']/g, m => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[m]);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
let currentSelectedMethod = 'UPI';
|
|
971
|
+
|
|
972
|
+
function hideAllContent() {
|
|
973
|
+
document.getElementById('viewUpiContent').hidden = true;
|
|
974
|
+
const vCod = document.getElementById('viewCodContent');
|
|
975
|
+
if (vCod) vCod.hidden = true;
|
|
976
|
+
const vCash = document.getElementById('viewCashContent');
|
|
977
|
+
if (vCash) vCash.hidden = true;
|
|
978
|
+
const vCard = document.getElementById('viewCardContent');
|
|
979
|
+
if (vCard) vCard.hidden = true;
|
|
980
|
+
document.getElementById('viewProcessingContent').hidden = true;
|
|
981
|
+
document.getElementById('viewSuccessContent').hidden = true;
|
|
982
|
+
document.getElementById('viewFailedContent').hidden = true;
|
|
983
|
+
document.getElementById('viewQueuedContent').hidden = true;
|
|
984
|
+
const vBlocked = document.getElementById('viewBlockedContent');
|
|
985
|
+
if (vBlocked) vBlocked.hidden = true;
|
|
986
|
+
const vDup = document.getElementById('viewDuplicateSessionContent');
|
|
987
|
+
if (vDup) vDup.hidden = true;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function formatTime(totalSec) {
|
|
991
|
+
const m = Math.floor(totalSec / 60);
|
|
992
|
+
const s = totalSec % 60;
|
|
993
|
+
return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
let _activeCountdownTargetTime = null;
|
|
997
|
+
|
|
998
|
+
function runCountdown(seconds, onTick, onDone, targetExpiresAt) {
|
|
999
|
+
const targetTime = targetExpiresAt
|
|
1000
|
+
? new Date(targetExpiresAt).getTime()
|
|
1001
|
+
: (_activeCountdownTargetTime || (Date.now() + (seconds || 300) * 1000));
|
|
1002
|
+
|
|
1003
|
+
if (countdownTimer && _activeCountdownTargetTime && Math.abs(_activeCountdownTargetTime - targetTime) <= 2500) {
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
_activeCountdownTargetTime = targetTime;
|
|
1008
|
+
if (countdownTimer) clearInterval(countdownTimer);
|
|
1009
|
+
|
|
1010
|
+
const calcRemaining = () => {
|
|
1011
|
+
return Math.max(0, Math.ceil((targetTime - Date.now()) / 1000));
|
|
1012
|
+
};
|
|
1013
|
+
|
|
1014
|
+
const doTick = (sec) => {
|
|
1015
|
+
onTick(sec);
|
|
1016
|
+
const tStr = formatTime(sec);
|
|
1017
|
+
document.querySelectorAll('.ypg-banner-running-timer').forEach(el => {
|
|
1018
|
+
el.textContent = tStr;
|
|
1019
|
+
});
|
|
1020
|
+
};
|
|
1021
|
+
doTick(calcRemaining());
|
|
1022
|
+
|
|
1023
|
+
countdownTimer = setInterval(() => {
|
|
1024
|
+
const remaining = calcRemaining();
|
|
1025
|
+
if (remaining <= 0) {
|
|
1026
|
+
clearInterval(countdownTimer);
|
|
1027
|
+
countdownTimer = null;
|
|
1028
|
+
_activeCountdownTargetTime = null;
|
|
1029
|
+
doTick(0);
|
|
1030
|
+
if (onDone) onDone();
|
|
1031
|
+
} else {
|
|
1032
|
+
doTick(remaining);
|
|
1033
|
+
}
|
|
1034
|
+
}, 1000);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function renderMethodSelectorBar(data) {
|
|
1038
|
+
const bar = document.getElementById('paymentMethodSelectorBar');
|
|
1039
|
+
const sellingMode = data.sellingMode || 'ONLINE';
|
|
1040
|
+
const enabledMethods = data.enabledMethods || ['UPI'];
|
|
1041
|
+
|
|
1042
|
+
// Populate authenticated customer into COD view
|
|
1043
|
+
const c = data.customer || activeCustomer;
|
|
1044
|
+
if (c) {
|
|
1045
|
+
const nameEl = document.getElementById('codCustomerName');
|
|
1046
|
+
const phoneEl = document.getElementById('codCustomerPhone');
|
|
1047
|
+
if (nameEl) nameEl.textContent = c.fullName || c.name || 'Customer';
|
|
1048
|
+
if (phoneEl) phoneEl.textContent = c.phoneNumber || c.mobileNumber || '-';
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// Maintain authentic 3-column desktop layout
|
|
1052
|
+
const pcMethodsList = document.getElementById('pcMethodsList');
|
|
1053
|
+
const pcActiveView = document.querySelector('.ypg-pc-active-view');
|
|
1054
|
+
const pcTopbarTitle = document.querySelector('.ypg-pc-topbar-title');
|
|
1055
|
+
|
|
1056
|
+
if (pcMethodsList) pcMethodsList.style.removeProperty('display');
|
|
1057
|
+
if (pcActiveView) pcActiveView.style.width = '';
|
|
1058
|
+
if (pcTopbarTitle) pcTopbarTitle.textContent = 'Payment Options';
|
|
1059
|
+
|
|
1060
|
+
// Mobile selector bar is visible only when more than 1 method is available
|
|
1061
|
+
if (bar) {
|
|
1062
|
+
bar.style.display = (!enabledMethods || enabledMethods.length <= 1) ? 'none' : 'flex';
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
const tabUpi = document.getElementById('tabMethodUpi');
|
|
1066
|
+
const tabCod = document.getElementById('tabMethodCod');
|
|
1067
|
+
const tabCash = document.getElementById('tabMethodCash');
|
|
1068
|
+
const tabCard = document.getElementById('tabMethodCard');
|
|
1069
|
+
|
|
1070
|
+
if (tabUpi) tabUpi.style.display = 'inline-flex';
|
|
1071
|
+
|
|
1072
|
+
const pcUpi = document.getElementById('pcMethodCardUpi');
|
|
1073
|
+
const pcCod = document.getElementById('pcMethodCardCod');
|
|
1074
|
+
const pcCash = document.getElementById('pcMethodCardCash');
|
|
1075
|
+
const pcCard = document.getElementById('pcMethodCardCard');
|
|
1076
|
+
|
|
1077
|
+
if (pcUpi) pcUpi.style.display = 'block';
|
|
1078
|
+
|
|
1079
|
+
if (sellingMode === 'ONLINE') {
|
|
1080
|
+
// Online Selling Mode:
|
|
1081
|
+
// COD works (configurable ON/OFF).
|
|
1082
|
+
// Cash on counter and Card on counter do NOT exist (hidden).
|
|
1083
|
+
if (tabCod) tabCod.style.display = enabledMethods.includes('COD') ? 'inline-flex' : 'none';
|
|
1084
|
+
if (tabCash) tabCash.style.display = 'none';
|
|
1085
|
+
if (tabCard) tabCard.style.display = 'none';
|
|
1086
|
+
|
|
1087
|
+
if (pcCod) pcCod.style.display = enabledMethods.includes('COD') ? 'block' : 'none';
|
|
1088
|
+
if (pcCash) pcCash.style.display = 'none';
|
|
1089
|
+
if (pcCard) pcCard.style.display = 'none';
|
|
1090
|
+
} else {
|
|
1091
|
+
// Offline Selling Mode:
|
|
1092
|
+
// Cash works (configurable ON/OFF).
|
|
1093
|
+
// Card works (configurable ON/OFF).
|
|
1094
|
+
// COD does NOT exist (hidden).
|
|
1095
|
+
if (tabCod) tabCod.style.display = 'none';
|
|
1096
|
+
if (tabCash) tabCash.style.display = enabledMethods.includes('CASH_COUNTER') ? 'inline-flex' : 'none';
|
|
1097
|
+
if (tabCard) tabCard.style.display = enabledMethods.includes('CARD_COUNTER') ? 'inline-flex' : 'none';
|
|
1098
|
+
|
|
1099
|
+
if (pcCod) pcCod.style.display = 'none';
|
|
1100
|
+
if (pcCash) pcCash.style.display = enabledMethods.includes('CASH_COUNTER') ? 'block' : 'none';
|
|
1101
|
+
if (pcCard) pcCard.style.display = enabledMethods.includes('CARD_COUNTER') ? 'block' : 'none';
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
if (!enabledMethods.includes(currentSelectedMethod)) {
|
|
1105
|
+
currentSelectedMethod = 'UPI';
|
|
1106
|
+
}
|
|
1107
|
+
updateMethodTabsUI(currentSelectedMethod);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
function updateMethodTabsUI(method) {
|
|
1111
|
+
const tabs = [
|
|
1112
|
+
{ id: 'tabMethodUpi', method: 'UPI' },
|
|
1113
|
+
{ id: 'tabMethodCod', method: 'COD' },
|
|
1114
|
+
{ id: 'tabMethodCash', method: 'CASH_COUNTER' },
|
|
1115
|
+
{ id: 'tabMethodCard', method: 'CARD_COUNTER' }
|
|
1116
|
+
];
|
|
1117
|
+
tabs.forEach(t => {
|
|
1118
|
+
const el = document.getElementById(t.id);
|
|
1119
|
+
if (!el) return;
|
|
1120
|
+
if (t.method === method) {
|
|
1121
|
+
el.classList.add('active');
|
|
1122
|
+
} else {
|
|
1123
|
+
el.classList.remove('active');
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
const pcCards = [
|
|
1128
|
+
{ id: 'pcMethodCardUpi', method: 'UPI' },
|
|
1129
|
+
{ id: 'pcMethodCardCod', method: 'COD' },
|
|
1130
|
+
{ id: 'pcMethodCardCash', method: 'CASH_COUNTER' },
|
|
1131
|
+
{ id: 'pcMethodCardCard', method: 'CARD_COUNTER' }
|
|
1132
|
+
];
|
|
1133
|
+
pcCards.forEach(c => {
|
|
1134
|
+
const el = document.getElementById(c.id);
|
|
1135
|
+
if (!el) return;
|
|
1136
|
+
if (c.method === method) {
|
|
1137
|
+
el.classList.add('is-active');
|
|
1138
|
+
} else {
|
|
1139
|
+
el.classList.remove('is-active');
|
|
1140
|
+
}
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function switchMethodFromDowntime() {
|
|
1145
|
+
const enabled = state?.enabledMethods || state?.branding?.enabledPaymentMethods || [];
|
|
1146
|
+
const sellingMode = state?.sellingMode || state?.branding?.sellingMode || 'OFFLINE';
|
|
1147
|
+
if (sellingMode === 'ONLINE' && enabled.includes('COD')) {
|
|
1148
|
+
selectPaymentMethod('COD');
|
|
1149
|
+
} else if (enabled.includes('CASH_COUNTER')) {
|
|
1150
|
+
selectPaymentMethod('CASH_COUNTER');
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
async function selectPaymentMethod(method) {
|
|
1155
|
+
currentSelectedMethod = method;
|
|
1156
|
+
updateMethodTabsUI(method);
|
|
1157
|
+
hideAllContent();
|
|
1158
|
+
|
|
1159
|
+
if (method === 'UPI') {
|
|
1160
|
+
document.getElementById('viewUpiContent').hidden = false;
|
|
1161
|
+
document.getElementById('ypgStickyFooter').style.display = 'flex';
|
|
1162
|
+
} else if (method === 'COD') {
|
|
1163
|
+
const vCod = document.getElementById('viewCodContent');
|
|
1164
|
+
if (vCod) vCod.hidden = false;
|
|
1165
|
+
document.getElementById('ypgStickyFooter').style.display = 'none';
|
|
1166
|
+
} else if (method === 'CASH_COUNTER') {
|
|
1167
|
+
const vCash = document.getElementById('viewCashContent');
|
|
1168
|
+
if (vCash) vCash.hidden = false;
|
|
1169
|
+
document.getElementById('ypgStickyFooter').style.display = 'none';
|
|
1170
|
+
} else if (method === 'CARD_COUNTER') {
|
|
1171
|
+
const vCard = document.getElementById('viewCardContent');
|
|
1172
|
+
if (vCard) vCard.hidden = false;
|
|
1173
|
+
document.getElementById('ypgStickyFooter').style.display = 'none';
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
try {
|
|
1177
|
+
const res = await fetch(`${API_BASE}/api/v1/checkout/${PAYMENT_ID}/select-method`, {
|
|
1178
|
+
method: 'POST',
|
|
1179
|
+
headers: getApiHeaders({ 'Content-Type': 'application/json' }),
|
|
1180
|
+
body: JSON.stringify({ method, deviceId: DeviceIdentity?.currentDeviceId })
|
|
1181
|
+
});
|
|
1182
|
+
const resData = await res.json();
|
|
1183
|
+
if (res.ok && resData) {
|
|
1184
|
+
state.method = method;
|
|
1185
|
+
if (resData.timeRemainingSeconds !== undefined) {
|
|
1186
|
+
state.timeRemainingSeconds = resData.timeRemainingSeconds;
|
|
1187
|
+
}
|
|
1188
|
+
if (resData.status) {
|
|
1189
|
+
state.status = resData.status;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
if (method === 'COD') {
|
|
1193
|
+
runCountdown(resData.timeRemainingSeconds || 300, sec => {
|
|
1194
|
+
const el = document.getElementById('codTimerText');
|
|
1195
|
+
if (el) el.textContent = formatTime(sec);
|
|
1196
|
+
}, () => {
|
|
1197
|
+
const el = document.getElementById('codTimerText');
|
|
1198
|
+
if (el) el.textContent = 'Expired';
|
|
1199
|
+
fetch(`${API_BASE}/api/v1/checkout/${PAYMENT_ID}/expire`, { method: 'POST', headers: getApiHeaders() }).catch(() => { });
|
|
1200
|
+
loadState();
|
|
1201
|
+
});
|
|
1202
|
+
} else if (method === 'UPI') {
|
|
1203
|
+
if (resData.status === 'ACTIVE') {
|
|
1204
|
+
loadQrSvg();
|
|
1205
|
+
runCountdown(resData.timeRemainingSeconds || 300, sec => {
|
|
1206
|
+
const el = document.getElementById('activeTimerText');
|
|
1207
|
+
if (el) el.textContent = formatTime(sec);
|
|
1208
|
+
}, () => {
|
|
1209
|
+
const el = document.getElementById('activeTimerText');
|
|
1210
|
+
if (el) el.textContent = 'Expired';
|
|
1211
|
+
fetch(`${API_BASE}/api/v1/checkout/${PAYMENT_ID}/expire`, { method: 'POST', headers: getApiHeaders() }).catch(() => { });
|
|
1212
|
+
loadState();
|
|
1213
|
+
});
|
|
1214
|
+
} else if (resData.status === 'QUEUED') {
|
|
1215
|
+
loadState();
|
|
1216
|
+
}
|
|
1217
|
+
} else {
|
|
1218
|
+
// Offline Counter mode: no timer countdown
|
|
1219
|
+
if (countdownTimer) clearInterval(countdownTimer);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
} catch (e) {
|
|
1223
|
+
console.warn('selectPaymentMethod network error:', e);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
async function promptConfirmCodOrder() {
|
|
1228
|
+
const amt = state?.amount || '{{AMOUNT}}';
|
|
1229
|
+
if (!confirm(`Are you sure you want to confirm and place this Cash on Delivery (COD) order for ₹${amt}? You will pay in cash upon doorstep delivery.`)) {
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
await executeConfirmCodOrder();
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
async function executeConfirmCodOrder() {
|
|
1236
|
+
const btn = document.getElementById('btnConfirmCod');
|
|
1237
|
+
if (btn) {
|
|
1238
|
+
btn.disabled = true;
|
|
1239
|
+
btn.innerText = 'Placing Cash on Delivery Order...';
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
try {
|
|
1243
|
+
const res = await fetch(`${API_BASE}/api/v1/checkout/${PAYMENT_ID}/confirm-cod`, {
|
|
1244
|
+
method: 'POST',
|
|
1245
|
+
headers: getApiHeaders({ 'Content-Type': 'application/json' })
|
|
1246
|
+
});
|
|
1247
|
+
const data = await res.json();
|
|
1248
|
+
if (res.ok) {
|
|
1249
|
+
loadState();
|
|
1250
|
+
} else {
|
|
1251
|
+
alert(data.error || 'Failed to place COD order');
|
|
1252
|
+
if (btn) {
|
|
1253
|
+
btn.disabled = false;
|
|
1254
|
+
btn.innerHTML = '<span>Confirm & Place COD Order (₹' + (state?.amount || '{{AMOUNT}}') + ')</span> ›';
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
} catch (e) {
|
|
1258
|
+
alert('Network error confirming COD order.');
|
|
1259
|
+
if (btn) {
|
|
1260
|
+
btn.disabled = false;
|
|
1261
|
+
btn.innerHTML = '<span>Confirm & Place COD Order (₹' + (state?.amount || '{{AMOUNT}}') + ')</span> ›';
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// --- DEVICE IDENTITY & HARDWARE CONTEXT BINDING LAYER ---
|
|
1267
|
+
const DeviceIdentity = {
|
|
1268
|
+
dbName: 'elpay_device_keystore_v1',
|
|
1269
|
+
storeName: 'keys',
|
|
1270
|
+
currentDeviceId: null,
|
|
1271
|
+
currentDeviceToken: null,
|
|
1272
|
+
|
|
1273
|
+
async openDb() {
|
|
1274
|
+
return new Promise((resolve) => {
|
|
1275
|
+
if (!window.indexedDB) {
|
|
1276
|
+
resolve(null);
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
const req = window.indexedDB.open(this.dbName, 1);
|
|
1280
|
+
req.onupgradeneeded = (e) => {
|
|
1281
|
+
const db = e.target.result;
|
|
1282
|
+
if (!db.objectStoreNames.contains(this.storeName)) {
|
|
1283
|
+
db.createObjectStore(this.storeName, { keyPath: 'id' });
|
|
1284
|
+
}
|
|
1285
|
+
};
|
|
1286
|
+
req.onsuccess = () => resolve(req.result);
|
|
1287
|
+
req.onerror = () => resolve(null);
|
|
1288
|
+
});
|
|
1289
|
+
},
|
|
1290
|
+
|
|
1291
|
+
async getStoredRecord() {
|
|
1292
|
+
try {
|
|
1293
|
+
const db = await this.openDb();
|
|
1294
|
+
if (!db) return null;
|
|
1295
|
+
return new Promise((resolve) => {
|
|
1296
|
+
const tx = db.transaction(this.storeName, 'readonly');
|
|
1297
|
+
const store = tx.objectStore(this.storeName);
|
|
1298
|
+
const req = store.get('primary_device');
|
|
1299
|
+
req.onsuccess = () => resolve(req.result || null);
|
|
1300
|
+
req.onerror = () => resolve(null);
|
|
1301
|
+
});
|
|
1302
|
+
} catch {
|
|
1303
|
+
return null;
|
|
1304
|
+
}
|
|
1305
|
+
},
|
|
1306
|
+
|
|
1307
|
+
async saveRecord(record) {
|
|
1308
|
+
try {
|
|
1309
|
+
const db = await this.openDb();
|
|
1310
|
+
if (!db) return;
|
|
1311
|
+
return new Promise((resolve) => {
|
|
1312
|
+
const tx = db.transaction(this.storeName, 'readwrite');
|
|
1313
|
+
const store = tx.objectStore(this.storeName);
|
|
1314
|
+
store.put({ id: 'primary_device', ...record });
|
|
1315
|
+
tx.oncomplete = () => resolve();
|
|
1316
|
+
tx.onerror = () => resolve();
|
|
1317
|
+
});
|
|
1318
|
+
} catch {}
|
|
1319
|
+
},
|
|
1320
|
+
|
|
1321
|
+
collectSignals() {
|
|
1322
|
+
let canvasSig = '';
|
|
1323
|
+
try {
|
|
1324
|
+
const canvas = document.createElement('canvas');
|
|
1325
|
+
canvas.width = 120;
|
|
1326
|
+
canvas.height = 30;
|
|
1327
|
+
const ctx = canvas.getContext('2d');
|
|
1328
|
+
ctx.textBaseline = 'top';
|
|
1329
|
+
ctx.font = "14px 'Arial'";
|
|
1330
|
+
ctx.fillText('ELPAY-PG-Device-Binding-2026', 2, 2);
|
|
1331
|
+
canvasSig = canvas.toDataURL().slice(-32);
|
|
1332
|
+
} catch {}
|
|
1333
|
+
|
|
1334
|
+
let webglRenderer = '';
|
|
1335
|
+
try {
|
|
1336
|
+
const gl = document.createElement('canvas').getContext('webgl');
|
|
1337
|
+
if (gl) {
|
|
1338
|
+
const dbg = gl.getExtension('WEBGL_debug_renderer_info');
|
|
1339
|
+
if (dbg) webglRenderer = gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL);
|
|
1340
|
+
}
|
|
1341
|
+
} catch {}
|
|
1342
|
+
|
|
1343
|
+
return {
|
|
1344
|
+
userAgent: navigator.userAgent || '',
|
|
1345
|
+
platform: navigator.platform || '',
|
|
1346
|
+
language: navigator.language || '',
|
|
1347
|
+
timezone: (Intl && Intl.DateTimeFormat) ? Intl.DateTimeFormat().resolvedOptions().timeZone : '',
|
|
1348
|
+
screenSignature: `${window.screen?.width || 0}x${window.screen?.height || 0}x${window.screen?.colorDepth || 24}`,
|
|
1349
|
+
hardwareConcurrency: navigator.hardwareConcurrency || 0,
|
|
1350
|
+
deviceMemory: navigator.deviceMemory || 0,
|
|
1351
|
+
webglRenderer,
|
|
1352
|
+
canvasSignature: canvasSig
|
|
1353
|
+
};
|
|
1354
|
+
},
|
|
1355
|
+
|
|
1356
|
+
async getOrRegister() {
|
|
1357
|
+
if (this.currentDeviceId) {
|
|
1358
|
+
return { deviceId: this.currentDeviceId, deviceToken: this.currentDeviceToken };
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// 1. Check local storage / session cache first
|
|
1362
|
+
let cachedId = localStorage.getItem('ypg_device_id');
|
|
1363
|
+
let cachedToken = localStorage.getItem('ypg_device_token');
|
|
1364
|
+
|
|
1365
|
+
// 2. Check IndexedDB persistent store
|
|
1366
|
+
const stored = await this.getStoredRecord();
|
|
1367
|
+
if (stored?.deviceId) {
|
|
1368
|
+
this.currentDeviceId = stored.deviceId;
|
|
1369
|
+
this.currentDeviceToken = stored.deviceToken;
|
|
1370
|
+
localStorage.setItem('ypg_device_id', stored.deviceId);
|
|
1371
|
+
if (stored.deviceToken) localStorage.setItem('ypg_device_token', stored.deviceToken);
|
|
1372
|
+
return { deviceId: stored.deviceId, deviceToken: stored.deviceToken };
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
if (cachedId) {
|
|
1376
|
+
this.currentDeviceId = cachedId;
|
|
1377
|
+
this.currentDeviceToken = cachedToken;
|
|
1378
|
+
return { deviceId: cachedId, deviceToken: cachedToken };
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
// 3. Generate new Web Crypto key pair
|
|
1382
|
+
let publicKeyBase64 = '';
|
|
1383
|
+
try {
|
|
1384
|
+
if (window.crypto && window.crypto.subtle) {
|
|
1385
|
+
const keyPair = await window.crypto.subtle.generateKey(
|
|
1386
|
+
{ name: 'ECDSA', namedCurve: 'P-256' },
|
|
1387
|
+
true,
|
|
1388
|
+
['sign', 'verify']
|
|
1389
|
+
);
|
|
1390
|
+
const spki = await window.crypto.subtle.exportKey('spki', keyPair.publicKey);
|
|
1391
|
+
const bytes = new Uint8Array(spki);
|
|
1392
|
+
let binary = '';
|
|
1393
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
1394
|
+
binary += String.fromCharCode(bytes[i]);
|
|
1395
|
+
}
|
|
1396
|
+
publicKeyBase64 = btoa(binary);
|
|
1397
|
+
}
|
|
1398
|
+
} catch (e) {
|
|
1399
|
+
console.warn('[DeviceIdentity] Web Crypto keygen fallback:', e.message);
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
if (!publicKeyBase64) {
|
|
1403
|
+
publicKeyBase64 = btoa(`fallback-device-key-${Math.random()}-${Date.now()}`);
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
// 4. Register with ELPAY backend
|
|
1407
|
+
const signals = this.collectSignals();
|
|
1408
|
+
try {
|
|
1409
|
+
const resp = await fetch(`${API_BASE}/api/v1/devices/register`, {
|
|
1410
|
+
method: 'POST',
|
|
1411
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1412
|
+
body: JSON.stringify({ publicKey: publicKeyBase64, signals })
|
|
1413
|
+
});
|
|
1414
|
+
const data = await resp.json();
|
|
1415
|
+
if (data?.deviceId) {
|
|
1416
|
+
this.currentDeviceId = data.deviceId;
|
|
1417
|
+
this.currentDeviceToken = data.deviceToken;
|
|
1418
|
+
localStorage.setItem('ypg_device_id', data.deviceId);
|
|
1419
|
+
if (data.deviceToken) localStorage.setItem('ypg_device_token', data.deviceToken);
|
|
1420
|
+
|
|
1421
|
+
await this.saveRecord({
|
|
1422
|
+
deviceId: data.deviceId,
|
|
1423
|
+
deviceToken: data.deviceToken,
|
|
1424
|
+
publicKeyBase64,
|
|
1425
|
+
registeredAt: new Date().toISOString()
|
|
1426
|
+
});
|
|
1427
|
+
|
|
1428
|
+
return { deviceId: data.deviceId, deviceToken: data.deviceToken };
|
|
1429
|
+
}
|
|
1430
|
+
} catch (err) {
|
|
1431
|
+
console.error('[DeviceIdentity] Registration error:', err);
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
return { deviceId: null, deviceToken: null };
|
|
1435
|
+
}
|
|
1436
|
+
};
|
|
1437
|
+
|
|
1438
|
+
function getApiHeaders(customHeaders = {}) {
|
|
1439
|
+
const headers = { ...customHeaders };
|
|
1440
|
+
if (typeof DeviceIdentity !== 'undefined' && DeviceIdentity?.currentDeviceId) {
|
|
1441
|
+
headers['X-Device-Id'] = DeviceIdentity.currentDeviceId;
|
|
1442
|
+
}
|
|
1443
|
+
if (typeof DeviceIdentity !== 'undefined' && DeviceIdentity?.currentDeviceToken) {
|
|
1444
|
+
headers['X-Device-Token'] = DeviceIdentity.currentDeviceToken;
|
|
1445
|
+
}
|
|
1446
|
+
const token = activeToken || (getPreservedIdentity()?.token) || localStorage.getItem('ypg_customer_token');
|
|
1447
|
+
if (token) {
|
|
1448
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
1449
|
+
headers['x-customer-token'] = token;
|
|
1450
|
+
}
|
|
1451
|
+
return headers;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
async function loadState() {
|
|
1455
|
+
try {
|
|
1456
|
+
await DeviceIdentity.getOrRegister();
|
|
1457
|
+
const headers = getApiHeaders();
|
|
1458
|
+
|
|
1459
|
+
const res = await fetch(`${API_BASE}/api/v1/checkout/${PAYMENT_ID}/state`, { headers });
|
|
1460
|
+
const data = await res.json().catch(() => ({}));
|
|
1461
|
+
|
|
1462
|
+
if (res.status === 409 || data.error === 'DUPLICATE_SESSION_DETECTED') {
|
|
1463
|
+
renderDuplicateSession(data);
|
|
1464
|
+
dismissSplashScreen();
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
if (!res.ok) {
|
|
1469
|
+
dismissSplashScreen();
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
renderState(data);
|
|
1474
|
+
dismissSplashScreen();
|
|
1475
|
+
} catch (e) {
|
|
1476
|
+
console.warn('loadState error:', e);
|
|
1477
|
+
dismissSplashScreen();
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
function adjustHexColor(col, amt) {
|
|
1482
|
+
if (!col) return '#0957cb';
|
|
1483
|
+
try {
|
|
1484
|
+
let usePound = false;
|
|
1485
|
+
if (col[0] === '#') {
|
|
1486
|
+
col = col.slice(1);
|
|
1487
|
+
usePound = true;
|
|
1488
|
+
}
|
|
1489
|
+
if (col.length === 3) col = col[0] + col[0] + col[1] + col[1] + col[2] + col[2];
|
|
1490
|
+
const num = parseInt(col, 16);
|
|
1491
|
+
let r = (num >> 16) + amt;
|
|
1492
|
+
if (r > 255) r = 255; else if (r < 0) r = 0;
|
|
1493
|
+
let b = ((num >> 8) & 0x00FF) + amt;
|
|
1494
|
+
if (b > 255) b = 255; else if (b < 0) b = 0;
|
|
1495
|
+
let g = (num & 0x0000FF) + amt;
|
|
1496
|
+
if (g > 255) g = 255; else if (g < 0) g = 0;
|
|
1497
|
+
return (usePound ? '#' : '') + (g | (b << 8) | (r << 16)).toString(16).padStart(6, '0');
|
|
1498
|
+
} catch {
|
|
1499
|
+
return col;
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
function applyDynamicTheme(branding) {
|
|
1504
|
+
if (!branding) return;
|
|
1505
|
+
const primary = branding.primaryColor || '#0b63e5';
|
|
1506
|
+
const darkPrimary = adjustHexColor(primary, -25);
|
|
1507
|
+
const root = document.documentElement;
|
|
1508
|
+
|
|
1509
|
+
root.style.setProperty('--ypg-blue', primary);
|
|
1510
|
+
root.style.setProperty('--ypg-blue-dark', darkPrimary);
|
|
1511
|
+
|
|
1512
|
+
const theme = branding.headerTheme || 'gradient';
|
|
1513
|
+
const headerEl = document.querySelector('.ypg-header');
|
|
1514
|
+
const sidebarEl = document.querySelector('.ypg-pc-sidebar');
|
|
1515
|
+
|
|
1516
|
+
if (theme === 'solid') {
|
|
1517
|
+
root.style.setProperty('--ypg-header-gradient', primary);
|
|
1518
|
+
if (headerEl) headerEl.style.background = primary;
|
|
1519
|
+
if (sidebarEl) sidebarEl.style.background = primary;
|
|
1520
|
+
} else if (theme === 'dark') {
|
|
1521
|
+
root.style.setProperty('--ypg-header-gradient', '#0b1329');
|
|
1522
|
+
if (headerEl) headerEl.style.background = '#0b1329';
|
|
1523
|
+
if (sidebarEl) sidebarEl.style.background = 'linear-gradient(180deg, #0b1329 0%, #030712 100%)';
|
|
1524
|
+
} else {
|
|
1525
|
+
const grad = `linear-gradient(180deg, ${primary} 0%, ${darkPrimary} 100%)`;
|
|
1526
|
+
root.style.setProperty('--ypg-header-gradient', grad);
|
|
1527
|
+
if (headerEl) headerEl.style.background = grad;
|
|
1528
|
+
if (sidebarEl) sidebarEl.style.background = grad;
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
// Policy links are merchant branding and land in an href, so only https is
|
|
1533
|
+
// accepted and the value is escaped for attribute context.
|
|
1534
|
+
function safeLegalUrl(candidate) {
|
|
1535
|
+
if (typeof candidate !== 'string') return null;
|
|
1536
|
+
const trimmed = candidate.trim();
|
|
1537
|
+
if (!trimmed) return null;
|
|
1538
|
+
let parsed;
|
|
1539
|
+
try {
|
|
1540
|
+
parsed = new URL(trimmed);
|
|
1541
|
+
} catch (e) {
|
|
1542
|
+
return null;
|
|
1543
|
+
}
|
|
1544
|
+
if (parsed.protocol !== 'https:') return null;
|
|
1545
|
+
return parsed.href;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
function renderHelpLegalLinks(branding) {
|
|
1549
|
+
const container = document.getElementById('helpModalLegalLinks');
|
|
1550
|
+
if (!container) return;
|
|
1551
|
+
let html = '';
|
|
1552
|
+
const privacyUrl = safeLegalUrl(branding.privacyPolicyUrl);
|
|
1553
|
+
if (privacyUrl) {
|
|
1554
|
+
html += `<a href="${escapeHtml(privacyUrl)}" target="_blank" rel="noopener noreferrer" style="color:var(--ypg-blue); text-decoration:underline; font-weight:600;">Privacy Policy ↗</a>`;
|
|
1555
|
+
}
|
|
1556
|
+
const termsHref = safeLegalUrl(branding.termsUrl);
|
|
1557
|
+
if (termsHref) {
|
|
1558
|
+
html += `<a href="${escapeHtml(termsHref)}" target="_blank" rel="noopener noreferrer" style="color:var(--ypg-blue); text-decoration:underline; font-weight:600;">Terms of Service ↗</a>`;
|
|
1559
|
+
}
|
|
1560
|
+
container.innerHTML = html;
|
|
1561
|
+
container.style.display = html ? 'flex' : 'none';
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
function renderState(data) {
|
|
1565
|
+
state = data;
|
|
1566
|
+
const b = data.branding || {};
|
|
1567
|
+
const appName = b.appName || 'ELPAY PG';
|
|
1568
|
+
const merchantName = data.merchantName || 'Merchant Checkout';
|
|
1569
|
+
|
|
1570
|
+
// 1. Dynamic Title
|
|
1571
|
+
document.title = `${merchantName} · ${appName} Checkout`;
|
|
1572
|
+
|
|
1573
|
+
// 2. Dynamic Colors & Header Theme
|
|
1574
|
+
applyDynamicTheme(b);
|
|
1575
|
+
|
|
1576
|
+
// 3. Merchant Name & Avatar
|
|
1577
|
+
if (data.merchantName) {
|
|
1578
|
+
const nameEl = document.getElementById('headerMerchantName');
|
|
1579
|
+
if (nameEl) nameEl.textContent = data.merchantName;
|
|
1580
|
+
const letterEl = document.getElementById('merchantAvatarLetter');
|
|
1581
|
+
if (letterEl) letterEl.textContent = data.merchantName.charAt(0).toUpperCase();
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
// 4. Verified Trust Badge
|
|
1585
|
+
const trustBadgeEl = document.getElementById('headerTrustBadge');
|
|
1586
|
+
const trustBadgeTextEl = document.getElementById('headerTrustBadgeText');
|
|
1587
|
+
if (trustBadgeEl) {
|
|
1588
|
+
if (b.hideTrustBadge) {
|
|
1589
|
+
trustBadgeEl.style.display = 'none';
|
|
1590
|
+
} else {
|
|
1591
|
+
trustBadgeEl.style.display = 'inline-flex';
|
|
1592
|
+
if (trustBadgeTextEl) {
|
|
1593
|
+
trustBadgeTextEl.textContent = b.trustBadgeText || `${appName} Verified Business`;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// 5. Header Sub-banner
|
|
1599
|
+
const subbannerEl = document.getElementById('headerSubbanner');
|
|
1600
|
+
const subbannerTextEl = document.getElementById('subbannerText');
|
|
1601
|
+
if (subbannerEl) {
|
|
1602
|
+
if (b.hideSubbanner) {
|
|
1603
|
+
subbannerEl.style.display = 'none';
|
|
1604
|
+
} else {
|
|
1605
|
+
subbannerEl.style.display = 'flex';
|
|
1606
|
+
if (subbannerTextEl) {
|
|
1607
|
+
subbannerTextEl.textContent = b.subbannerText || b.sidebarNotice || `Verified & Protected by ${appName}`;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
// 6. Time-Saving Conversion Banner
|
|
1613
|
+
if (b.hideTimeSavingBanner) {
|
|
1614
|
+
document.querySelectorAll('.ypg-time-saving-banner').forEach(el => el.style.setProperty('display', 'none', 'important'));
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
|
|
1618
|
+
// Update PC Sidebar Details & Whitelabeling
|
|
1619
|
+
const pcMerchant = document.getElementById('pcMerchantName');
|
|
1620
|
+
if (pcMerchant) pcMerchant.textContent = data.merchantName || 'Merchant Name';
|
|
1621
|
+
const pcPrice = document.getElementById('pcPriceSummaryAmount');
|
|
1622
|
+
if (pcPrice && data.amount) pcPrice.textContent = `₹${data.amount}`;
|
|
1623
|
+
const pcAppBrand = document.getElementById('pcAppNameBrand');
|
|
1624
|
+
if (pcAppBrand) pcAppBrand.textContent = appName;
|
|
1625
|
+
|
|
1626
|
+
// PC Avatar Logo Whitelabel
|
|
1627
|
+
const pcLogoImg = document.getElementById('pcMerchantAvatarImg');
|
|
1628
|
+
const pcLetterSpan = document.getElementById('pcMerchantAvatarLetter');
|
|
1629
|
+
if (b.logoUrl) {
|
|
1630
|
+
if (pcLogoImg) {
|
|
1631
|
+
pcLogoImg.src = b.logoUrl;
|
|
1632
|
+
pcLogoImg.style.display = 'block';
|
|
1633
|
+
}
|
|
1634
|
+
if (pcLetterSpan) pcLetterSpan.style.display = 'none';
|
|
1635
|
+
} else {
|
|
1636
|
+
if (pcLogoImg) pcLogoImg.style.display = 'none';
|
|
1637
|
+
if (pcLetterSpan) {
|
|
1638
|
+
pcLetterSpan.style.display = 'block';
|
|
1639
|
+
pcLetterSpan.textContent = (data.merchantName || 'M').charAt(0).toUpperCase();
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
// PC Trust Badge Whitelabel
|
|
1644
|
+
const pcTrust = document.getElementById('pcTrustBadge');
|
|
1645
|
+
const pcTrustText = document.getElementById('pcTrustBadgeText');
|
|
1646
|
+
if (pcTrust) {
|
|
1647
|
+
if (b.hideTrustBadge) {
|
|
1648
|
+
pcTrust.style.display = 'none';
|
|
1649
|
+
} else {
|
|
1650
|
+
pcTrust.style.display = 'inline-flex';
|
|
1651
|
+
if (pcTrustText) {
|
|
1652
|
+
pcTrustText.textContent = b.trustBadgeText || `${appName} Trusted Business`;
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
// PC Social Proof / Subbanner Whitelabel
|
|
1658
|
+
const pcSocialWrap = document.getElementById('pcSocialProofWrap');
|
|
1659
|
+
const pcSocialProof = document.getElementById('pcSocialProofText');
|
|
1660
|
+
if (pcSocialWrap) {
|
|
1661
|
+
if (b.hideSubbanner) {
|
|
1662
|
+
pcSocialWrap.style.display = 'none';
|
|
1663
|
+
} else {
|
|
1664
|
+
pcSocialWrap.style.display = 'flex';
|
|
1665
|
+
if (pcSocialProof) {
|
|
1666
|
+
pcSocialProof.textContent = b.subbannerText || b.sidebarNotice || `500+ orders last month from ${data.merchantName || 'Merchant'}`;
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
const pcCustomerText = document.getElementById('pcCustomerProfileText');
|
|
1672
|
+
if (pcCustomerText) {
|
|
1673
|
+
const c = data.customer || activeCustomer || (typeof getPreservedIdentity === 'function' ? getPreservedIdentity()?.customer : null);
|
|
1674
|
+
if (c) {
|
|
1675
|
+
const name = c.fullName || c.name || c.customerName || '';
|
|
1676
|
+
const mobile = c.mobileNumber || c.phoneNumber || '';
|
|
1677
|
+
const clean = mobile ? String(mobile).replace(/^\+91/, '').replace(/\D/g, '').slice(-10) : '';
|
|
1678
|
+
if (name && clean) {
|
|
1679
|
+
pcCustomerText.textContent = `Using as ${name} (+91 ${clean})`;
|
|
1680
|
+
} else if (name) {
|
|
1681
|
+
pcCustomerText.textContent = `Using as ${name}`;
|
|
1682
|
+
} else if (clean) {
|
|
1683
|
+
pcCustomerText.textContent = `Using as +91 ${clean}`;
|
|
1684
|
+
} else if (c.email) {
|
|
1685
|
+
pcCustomerText.textContent = `Using as ${c.email}`;
|
|
1686
|
+
} else {
|
|
1687
|
+
pcCustomerText.textContent = 'Using as Guest User';
|
|
1688
|
+
}
|
|
1689
|
+
} else {
|
|
1690
|
+
pcCustomerText.textContent = 'Using as Guest User';
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
document.querySelectorAll('.cancelMerchantNameSpan').forEach(el => {
|
|
1694
|
+
el.textContent = data.merchantName || 'Merchant';
|
|
1695
|
+
});
|
|
1696
|
+
|
|
1697
|
+
// Update UPI Server Health Indicator (3-Tier Model: Green, Yellow, Red)
|
|
1698
|
+
const serverIndicator = data.upiServerIndicator || { status: 'WORKING', tier: 'GREEN', label: 'Server Is Working', isWorking: true };
|
|
1699
|
+
const healthPill = document.getElementById('upiServerHealthIndicator');
|
|
1700
|
+
const healthLabel = document.getElementById('serverStatusLabel');
|
|
1701
|
+
const downtimeAlert = document.getElementById('serverDowntimeAlert');
|
|
1702
|
+
const downtimeHeading = document.querySelector('#serverDowntimeAlert .ypg-downtime-heading');
|
|
1703
|
+
const downtimeMsg = document.getElementById('serverDowntimeMsg');
|
|
1704
|
+
const btnCashAlt = document.getElementById('btnDowntimeCashAlt');
|
|
1705
|
+
|
|
1706
|
+
const tier = serverIndicator.tier || (serverIndicator.status === 'WORKING' ? 'GREEN' : (serverIndicator.status === 'BUSY' ? 'YELLOW' : 'RED'));
|
|
1707
|
+
|
|
1708
|
+
if (healthPill) {
|
|
1709
|
+
if (tier === 'GREEN') {
|
|
1710
|
+
healthPill.className = 'ypg-server-health-indicator is-working';
|
|
1711
|
+
} else if (tier === 'YELLOW') {
|
|
1712
|
+
healthPill.className = 'ypg-server-health-indicator is-busy';
|
|
1713
|
+
} else {
|
|
1714
|
+
healthPill.className = 'ypg-server-health-indicator is-issues';
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
if (healthLabel) {
|
|
1718
|
+
healthLabel.textContent = serverIndicator.label || (tier === 'GREEN' ? 'Server Is Working' : (tier === 'YELLOW' ? 'UPI Server Busy' : 'Server Facing Issues'));
|
|
1719
|
+
}
|
|
1720
|
+
if (downtimeAlert) {
|
|
1721
|
+
if (tier === 'GREEN') {
|
|
1722
|
+
downtimeAlert.style.display = 'none';
|
|
1723
|
+
} else if (tier === 'YELLOW') {
|
|
1724
|
+
downtimeAlert.style.display = 'flex';
|
|
1725
|
+
downtimeAlert.className = 'ypg-downtime-alert is-warning';
|
|
1726
|
+
if (downtimeHeading) downtimeHeading.textContent = 'UPI Server Busy';
|
|
1727
|
+
if (downtimeMsg) {
|
|
1728
|
+
downtimeMsg.textContent = serverIndicator.warning || 'Your payment may go through extra processing seconds.';
|
|
1729
|
+
}
|
|
1730
|
+
if (btnCashAlt) {
|
|
1731
|
+
btnCashAlt.style.display = (serverIndicator.recommendedAlternative === 'CASH') ? 'inline-block' : 'none';
|
|
1732
|
+
}
|
|
1733
|
+
} else {
|
|
1734
|
+
// RED
|
|
1735
|
+
downtimeAlert.style.display = 'flex';
|
|
1736
|
+
downtimeAlert.className = 'ypg-downtime-alert';
|
|
1737
|
+
if (downtimeHeading) downtimeHeading.textContent = 'UPI Server Facing Issues';
|
|
1738
|
+
if (downtimeMsg) {
|
|
1739
|
+
downtimeMsg.textContent = serverIndicator.warning || 'It is not recommended to pay by UPI right now as the server is facing downtime. We recommend paying by cash instead.';
|
|
1740
|
+
}
|
|
1741
|
+
if (btnCashAlt) {
|
|
1742
|
+
btnCashAlt.style.display = (serverIndicator.recommendedAlternative === 'CASH') ? 'inline-block' : 'none';
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
// CRITICAL INVARIANT: In all 3 states (Green, Yellow, Red), ALWAYS display the dynamic UPI QR code
|
|
1748
|
+
const upiCardContainer = document.querySelector('.ypg-qr-card-container');
|
|
1749
|
+
if (upiCardContainer) upiCardContainer.style.display = 'flex';
|
|
1750
|
+
const qrContainerBox = document.getElementById('qrImageContainer');
|
|
1751
|
+
if (qrContainerBox && (!qrContainerBox.querySelector('svg') || qrContainerBox.textContent.includes('Loading QR'))) {
|
|
1752
|
+
loadQrSvg();
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
// 7. Amount & Order IDs
|
|
1756
|
+
if (data.amount) {
|
|
1757
|
+
const formatted = `₹${data.amount}`;
|
|
1758
|
+
const bAmt = document.getElementById('bottomAmountDisplay');
|
|
1759
|
+
if (bAmt) bAmt.textContent = formatted;
|
|
1760
|
+
const dAmt = document.getElementById('drawerTotalAmount');
|
|
1761
|
+
if (dAmt) dAmt.textContent = formatted;
|
|
1762
|
+
const sAmt = document.getElementById('successAmountVal');
|
|
1763
|
+
if (sAmt) sAmt.textContent = formatted;
|
|
1764
|
+
}
|
|
1765
|
+
if (data.orderId || data.id) {
|
|
1766
|
+
const oId = data.orderId || data.id;
|
|
1767
|
+
const dOrder = document.getElementById('drawerOrderId');
|
|
1768
|
+
if (dOrder) dOrder.textContent = oId;
|
|
1769
|
+
const formattedOId = String(oId).startsWith('#') ? oId : `#${oId}`;
|
|
1770
|
+
document.querySelectorAll('.cashierOrderIdDisplay').forEach(el => {
|
|
1771
|
+
el.textContent = formattedOId;
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
// Note: Payee VPA / UPI ID is strictly hidden from customer view in all screens
|
|
1775
|
+
|
|
1776
|
+
// 8. In-Store Offline Counter Instructions
|
|
1777
|
+
const cashNoticeEl = document.getElementById('cashCounterDeskNotice');
|
|
1778
|
+
if (cashNoticeEl && b.counterInstructions) {
|
|
1779
|
+
cashNoticeEl.textContent = b.counterInstructions;
|
|
1780
|
+
}
|
|
1781
|
+
const cardNoticeEl = document.getElementById('cardCounterDeskNotice');
|
|
1782
|
+
if (cardNoticeEl && b.cardCounterInstructions) {
|
|
1783
|
+
cardNoticeEl.textContent = b.cardCounterInstructions;
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
// 9. Dynamic Logo
|
|
1787
|
+
const logoImg = document.getElementById('merchantAvatarImg');
|
|
1788
|
+
const letterSpan = document.getElementById('merchantAvatarLetter');
|
|
1789
|
+
if (b.logoUrl) {
|
|
1790
|
+
if (logoImg) {
|
|
1791
|
+
logoImg.src = b.logoUrl;
|
|
1792
|
+
logoImg.style.display = 'block';
|
|
1793
|
+
}
|
|
1794
|
+
if (letterSpan) letterSpan.style.display = 'none';
|
|
1795
|
+
} else {
|
|
1796
|
+
if (logoImg) logoImg.style.display = 'none';
|
|
1797
|
+
if (letterSpan) letterSpan.style.display = 'block';
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
// 10. Splash Screen Branding
|
|
1801
|
+
const splashLogo = document.getElementById('splashBrandLogo');
|
|
1802
|
+
const splashDefaultShield = document.getElementById('splashDefaultShield');
|
|
1803
|
+
const splashSecuredText = document.getElementById('splashSecuredByText');
|
|
1804
|
+
if (b.logoUrl && splashLogo) {
|
|
1805
|
+
splashLogo.src = b.logoUrl;
|
|
1806
|
+
splashLogo.style.display = 'block';
|
|
1807
|
+
if (splashDefaultShield) splashDefaultShield.style.display = 'none';
|
|
1808
|
+
}
|
|
1809
|
+
if (splashSecuredText) {
|
|
1810
|
+
splashSecuredText.textContent = b.hidePoweredBy ? (b.appName || '256-Bit Bank Encryption') : (b.appName || 'ELPAY PG');
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
// 12. Legal Disclaimer Strip
|
|
1814
|
+
const legalDisclaimerEl = document.getElementById('footerLegalDisclaimer');
|
|
1815
|
+
if (legalDisclaimerEl) {
|
|
1816
|
+
if (b.hidePoweredBy) {
|
|
1817
|
+
legalDisclaimerEl.innerHTML = `Secured by 256-Bit Bank Encryption • <span style="text-decoration:underline; cursor:pointer;" onclick="openHelpModal()">Privacy & Terms</span>`;
|
|
1818
|
+
} else {
|
|
1819
|
+
legalDisclaimerEl.innerHTML = `Secured by ${escapeHtml(appName)} & 256-Bit Bank Encryption • <span style="text-decoration:underline; cursor:pointer;" onclick="openHelpModal()">Privacy Notice</span>`;
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
// 13. Success Screen Description
|
|
1824
|
+
const successDesc = document.getElementById('successDescText');
|
|
1825
|
+
if (successDesc) {
|
|
1826
|
+
successDesc.textContent = b.hidePoweredBy
|
|
1827
|
+
? 'Your payment was automatically verified by banking observers.'
|
|
1828
|
+
: `Your payment was automatically verified by ${appName} banking observers.`;
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
// 14. Help Modal Title & Legal Links
|
|
1832
|
+
const helpTitle = document.getElementById('helpModalTitle');
|
|
1833
|
+
if (helpTitle) helpTitle.textContent = `${appName} Help & Support`;
|
|
1834
|
+
renderHelpLegalLinks(b);
|
|
1835
|
+
|
|
1836
|
+
// 1. Conflict detection: single active payment session invariant
|
|
1837
|
+
const conflictModal = document.getElementById('activeConflictModal');
|
|
1838
|
+
if (data.hasActiveSessionConflict) {
|
|
1839
|
+
hideAllContent();
|
|
1840
|
+
if (conflictModal) conflictModal.hidden = false;
|
|
1841
|
+
if (data.conflictingSession && data.conflictingSession.id) {
|
|
1842
|
+
activeConflictSessionId = data.conflictingSession.id;
|
|
1843
|
+
const desc = document.getElementById('conflictModalDesc');
|
|
1844
|
+
if (desc) {
|
|
1845
|
+
desc.textContent = `You already have an active payment session (#${data.conflictingSession.orderId || data.conflictingSession.id.slice(0, 8)}) for ₹${data.conflictingSession.amount}. For security, only one active payment session is allowed per customer.`;
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
if (queuePollTimer) { clearInterval(queuePollTimer); queuePollTimer = null; }
|
|
1849
|
+
if (countdownTimer) { clearInterval(countdownTimer); countdownTimer = null; }
|
|
1850
|
+
return;
|
|
1851
|
+
} else {
|
|
1852
|
+
if (conflictModal) conflictModal.hidden = true;
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
// --- 2. TERMINAL PAYMENT STATES (TERMINAL & OVER) ---
|
|
1856
|
+
if (['SUCCESS', 'CANCELLED', 'EXPIRED', 'FAILED'].includes(data.status)) {
|
|
1857
|
+
if (queuePollTimer) { clearInterval(queuePollTimer); queuePollTimer = null; }
|
|
1858
|
+
if (countdownTimer) { clearInterval(countdownTimer); countdownTimer = null; }
|
|
1859
|
+
setGatewayAuthMode(false);
|
|
1860
|
+
|
|
1861
|
+
if (data.status === 'CANCELLED') {
|
|
1862
|
+
emitTriggerHook('CANCEL_CONFIRMED');
|
|
1863
|
+
closeProcessingModal();
|
|
1864
|
+
closeCancelPaymentModal();
|
|
1865
|
+
const bar = document.getElementById('paymentMethodSelectorBar');
|
|
1866
|
+
if (bar) bar.style.display = 'none';
|
|
1867
|
+
|
|
1868
|
+
hideAllContent();
|
|
1869
|
+
const failedView = document.getElementById('viewFailedContent');
|
|
1870
|
+
if (failedView) failedView.hidden = false;
|
|
1871
|
+
const footer = document.getElementById('ypgStickyFooter');
|
|
1872
|
+
if (footer) footer.style.display = 'none';
|
|
1873
|
+
|
|
1874
|
+
const titleEl = document.getElementById('failedTitleText');
|
|
1875
|
+
if (titleEl) titleEl.textContent = 'Payment Cancelled';
|
|
1876
|
+
const descEl = document.getElementById('failedDescriptionText');
|
|
1877
|
+
if (descEl) descEl.textContent = 'You cancelled this payment session. Click below to return to the store.';
|
|
1878
|
+
|
|
1879
|
+
const btnAction = document.getElementById('btnFailedAction');
|
|
1880
|
+
if (btnAction) {
|
|
1881
|
+
btnAction.textContent = 'Back to Store';
|
|
1882
|
+
btnAction.onclick = () => {
|
|
1883
|
+
const storeUrl = getStoreReturnUrl();
|
|
1884
|
+
const isEmbedded = (window.parent && window.parent !== window) || location.search.includes('embedded=1');
|
|
1885
|
+
if (isEmbedded) {
|
|
1886
|
+
try {
|
|
1887
|
+
postToMerchant({ event: 'elpay.modal_close', redirectUrl: storeUrl });
|
|
1888
|
+
postToMerchant({ event: 'elpay.close', redirectUrl: storeUrl });
|
|
1889
|
+
postToMerchant({ event: 'elpay.cancel', redirectUrl: storeUrl });
|
|
1890
|
+
postToMerchant({ event: 'elpay.payment_cancelled', redirectUrl: storeUrl });
|
|
1891
|
+
} catch (e) {}
|
|
1892
|
+
} else if (storeUrl && storeUrl !== '#' && sanitizeRedirectUrl(storeUrl)) {
|
|
1893
|
+
window.location.href = sanitizeRedirectUrl(storeUrl);
|
|
1894
|
+
} else {
|
|
1895
|
+
window.history.back();
|
|
1896
|
+
}
|
|
1897
|
+
};
|
|
1898
|
+
}
|
|
1899
|
+
return;
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
if (data.status === 'FAILED' || data.status === 'EXPIRED') {
|
|
1903
|
+
closeProcessingModal();
|
|
1904
|
+
closeCancelPaymentModal();
|
|
1905
|
+
const bar = document.getElementById('paymentMethodSelectorBar');
|
|
1906
|
+
if (bar) bar.style.display = 'none';
|
|
1907
|
+
|
|
1908
|
+
hideAllContent();
|
|
1909
|
+
const failedView = document.getElementById('viewFailedContent');
|
|
1910
|
+
if (failedView) failedView.hidden = false;
|
|
1911
|
+
const footer = document.getElementById('ypgStickyFooter');
|
|
1912
|
+
if (footer) footer.style.display = 'none';
|
|
1913
|
+
emitTriggerHook('PAYMENT_FAILED', { status: data.status });
|
|
1914
|
+
|
|
1915
|
+
const isExpired = data.status === 'EXPIRED';
|
|
1916
|
+
const titleEl = document.getElementById('failedTitleText');
|
|
1917
|
+
if (titleEl) titleEl.textContent = isExpired ? 'Payment Session Expired' : 'Payment Not Confirmed';
|
|
1918
|
+
const descEl = document.getElementById('failedDescriptionText');
|
|
1919
|
+
if (descEl) {
|
|
1920
|
+
descEl.textContent = isExpired
|
|
1921
|
+
? 'The 5-minute active payment window elapsed before verification was completed. For security, please return to the store and initiate a new order.'
|
|
1922
|
+
: 'No matching UPI bank confirmation was detected during the verification window.';
|
|
1923
|
+
}
|
|
1924
|
+
const btnAction = document.getElementById('btnFailedAction');
|
|
1925
|
+
if (btnAction) {
|
|
1926
|
+
btnAction.textContent = isExpired ? 'Start New Payment' : 'Try Again';
|
|
1927
|
+
btnAction.onclick = () => retryNewPayment();
|
|
1928
|
+
}
|
|
1929
|
+
return;
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
if (data.status === 'SUCCESS') {
|
|
1933
|
+
closeProcessingModal();
|
|
1934
|
+
closeCancelPaymentModal();
|
|
1935
|
+
const bar = document.getElementById('paymentMethodSelectorBar');
|
|
1936
|
+
if (bar) bar.style.display = 'none';
|
|
1937
|
+
|
|
1938
|
+
hideAllContent();
|
|
1939
|
+
const successView = document.getElementById('viewSuccessContent');
|
|
1940
|
+
if (successView) successView.hidden = false;
|
|
1941
|
+
const footer = document.getElementById('ypgStickyFooter');
|
|
1942
|
+
if (footer) footer.style.display = 'none';
|
|
1943
|
+
const amtEl = document.getElementById('successAmountVal');
|
|
1944
|
+
if (amtEl) amtEl.textContent = `₹${data.amount}`;
|
|
1945
|
+
|
|
1946
|
+
const isCodOrder = data.method === 'COD' || data.verificationTag === 'COD_ORDER_PLACED';
|
|
1947
|
+
const titleEl = document.getElementById('successTitleText');
|
|
1948
|
+
const descEl = document.getElementById('successDescText');
|
|
1949
|
+
const codApiBox = document.getElementById('successCodDevApiBox');
|
|
1950
|
+
const codApiUrl = document.getElementById('successCodDevApiUrl');
|
|
1951
|
+
|
|
1952
|
+
if (isCodOrder) {
|
|
1953
|
+
if (titleEl) titleEl.textContent = 'Order Placed (Cash on Delivery)!';
|
|
1954
|
+
if (descEl) descEl.textContent = `Your order #${data.orderId} was confirmed. Please keep ₹${data.amount} in cash ready for the courier agent upon arrival.`;
|
|
1955
|
+
if (codApiBox && codApiUrl) {
|
|
1956
|
+
codApiBox.style.display = 'block';
|
|
1957
|
+
codApiUrl.textContent = `${API_BASE}/api/v1/orders/${data.orderId}/cod`;
|
|
1958
|
+
}
|
|
1959
|
+
} else {
|
|
1960
|
+
if (titleEl) titleEl.textContent = 'Payment Successful!';
|
|
1961
|
+
if (descEl) descEl.textContent = 'Your payment was automatically verified by ELPAY PG banking observers.';
|
|
1962
|
+
if (codApiBox) codApiBox.style.display = 'none';
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
emitTriggerHook('PAYMENT_COMPLETED', { amount: data.amount, method: data.method, isCod: isCodOrder });
|
|
1966
|
+
|
|
1967
|
+
try {
|
|
1968
|
+
postToMerchant({
|
|
1969
|
+
event: 'elpay.payment_success',
|
|
1970
|
+
data: data
|
|
1971
|
+
});
|
|
1972
|
+
} catch (e) { }
|
|
1973
|
+
|
|
1974
|
+
const btnReturn = document.getElementById('btnReturnStore') || document.getElementById('btnReturnToStore');
|
|
1975
|
+
const safeReturnUrl = sanitizeRedirectUrl(data.returnUrl);
|
|
1976
|
+
if (btnReturn && safeReturnUrl) {
|
|
1977
|
+
btnReturn.hidden = false;
|
|
1978
|
+
btnReturn.href = safeReturnUrl;
|
|
1979
|
+
if (window.parent === window) {
|
|
1980
|
+
setTimeout(() => { window.location.href = safeReturnUrl; }, 2000);
|
|
1981
|
+
}
|
|
1982
|
+
} else if (btnReturn) {
|
|
1983
|
+
btnReturn.hidden = true;
|
|
1984
|
+
}
|
|
1985
|
+
return;
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
// Sync payment methods & modality layout immediately to match merchant settings
|
|
1990
|
+
renderMethodSelectorBar(data);
|
|
1991
|
+
|
|
1992
|
+
// Auto-bind customer to payment session helper
|
|
1993
|
+
if (typeof autoBindCustomerToSession !== 'function') {
|
|
1994
|
+
window.autoBindCustomerToSession = async function(customerId, token) {
|
|
1995
|
+
if (window.isAutoBindingSession || !PAYMENT_ID || !token || !customerId) return;
|
|
1996
|
+
window.isAutoBindingSession = true;
|
|
1997
|
+
try {
|
|
1998
|
+
const res = await fetch(`${API_BASE}/api/v1/checkout/${PAYMENT_ID}/bind-customer`, {
|
|
1999
|
+
method: 'POST',
|
|
2000
|
+
headers: getApiHeaders({
|
|
2001
|
+
'Content-Type': 'application/json',
|
|
2002
|
+
'Authorization': `Bearer ${token}`
|
|
2003
|
+
}),
|
|
2004
|
+
body: JSON.stringify({ token, customerId })
|
|
2005
|
+
});
|
|
2006
|
+
if (res.ok) {
|
|
2007
|
+
const boundData = await res.json().catch(() => null);
|
|
2008
|
+
if (boundData && boundData.status && boundData.status !== 'INITIATED') {
|
|
2009
|
+
loadState();
|
|
2010
|
+
}
|
|
2011
|
+
} else if (res.status === 401) {
|
|
2012
|
+
clearPreservedIdentity();
|
|
2013
|
+
activeCustomer = null;
|
|
2014
|
+
activeToken = null;
|
|
2015
|
+
setGatewayAuthMode(true);
|
|
2016
|
+
goToAuthStep('mobile');
|
|
2017
|
+
renderSavedAccountsUI();
|
|
2018
|
+
}
|
|
2019
|
+
} catch (e) {
|
|
2020
|
+
} finally {
|
|
2021
|
+
window.isAutoBindingSession = false;
|
|
2022
|
+
}
|
|
2023
|
+
};
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
// 3. Pre-Gateway Customer Authentication Gate (Preserves identity up to 1 week)
|
|
2027
|
+
if (data.customer) {
|
|
2028
|
+
activeCustomer = data.customer;
|
|
2029
|
+
} else if (!activeCustomer) {
|
|
2030
|
+
const preserved = getPreservedIdentity();
|
|
2031
|
+
if (preserved && preserved.customer) {
|
|
2032
|
+
activeCustomer = preserved.customer;
|
|
2033
|
+
activeToken = preserved.token || activeToken;
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
const hasCustomer = Boolean(data.customer || activeCustomer);
|
|
2037
|
+
|
|
2038
|
+
if (!hasCustomer) {
|
|
2039
|
+
setGatewayAuthMode(true);
|
|
2040
|
+
const stepMob = document.getElementById('authStepMobile');
|
|
2041
|
+
const isAlreadyOnMobile = stepMob && !stepMob.hidden;
|
|
2042
|
+
if (!currentAuthStep || (currentAuthStep === 'mobile' && !isAlreadyOnMobile)) {
|
|
2043
|
+
goToAuthStep('mobile');
|
|
2044
|
+
}
|
|
2045
|
+
renderSavedAccountsUI();
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
// Authenticated Customer
|
|
2050
|
+
if (activeCustomer) {
|
|
2051
|
+
updateCustomerDisplay(activeCustomer);
|
|
2052
|
+
// Automatically bind customer to the payment session if session is INITIATED or unbound
|
|
2053
|
+
if (activeToken && PAYMENT_ID && (!data.customer || data.status === 'INITIATED')) {
|
|
2054
|
+
window.autoBindCustomerToSession(activeCustomer.id, activeToken);
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
setGatewayAuthMode(false);
|
|
2058
|
+
|
|
2059
|
+
// Render psychological time-saving conversion banner
|
|
2060
|
+
renderTimeSavingBanners(data);
|
|
2061
|
+
|
|
2062
|
+
// 3. Status Handling: BLOCKED, QUEUED, ACTIVE, PROCESSING, SUCCESS, FAILED, EXPIRED
|
|
2063
|
+
if (data.status === 'BLOCKED' || data.isBanned || data.fraudDetected) {
|
|
2064
|
+
renderBlockedState(data);
|
|
2065
|
+
return;
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
if (data.status === 'QUEUED') {
|
|
2069
|
+
const bar = document.getElementById('paymentMethodSelectorBar');
|
|
2070
|
+
if (bar) bar.style.display = 'none';
|
|
2071
|
+
|
|
2072
|
+
hideAllContent();
|
|
2073
|
+
document.getElementById('viewQueuedContent').hidden = false;
|
|
2074
|
+
document.getElementById('ypgStickyFooter').style.display = 'none';
|
|
2075
|
+
document.getElementById('queueNumberDisplay').textContent = `#${data.queuePosition || 1}`;
|
|
2076
|
+
runCountdown(data.timeRemainingSeconds, sec => {
|
|
2077
|
+
document.getElementById('queueTimerText').textContent = formatTime(sec);
|
|
2078
|
+
}, () => loadState());
|
|
2079
|
+
|
|
2080
|
+
if (!queuePollTimer) {
|
|
2081
|
+
queuePollTimer = setInterval(() => { loadState(); }, 1200);
|
|
2082
|
+
}
|
|
2083
|
+
return;
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
if (queuePollTimer) {
|
|
2087
|
+
clearInterval(queuePollTimer);
|
|
2088
|
+
queuePollTimer = null;
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
if (data.status === 'ACTIVE' || (data.status === 'INITIATED' && (data.timeRemainingSeconds > 0 || data.expiresAt))) {
|
|
2092
|
+
renderMethodSelectorBar(data);
|
|
2093
|
+
|
|
2094
|
+
hideAllContent();
|
|
2095
|
+
if (currentSelectedMethod === 'COD') {
|
|
2096
|
+
const vCod = document.getElementById('viewCodContent');
|
|
2097
|
+
if (vCod) vCod.hidden = false;
|
|
2098
|
+
document.getElementById('ypgStickyFooter').style.display = 'none';
|
|
2099
|
+
|
|
2100
|
+
runCountdown(data.timeRemainingSeconds || 300, sec => {
|
|
2101
|
+
const el = document.getElementById('codTimerText');
|
|
2102
|
+
if (el) el.textContent = formatTime(sec);
|
|
2103
|
+
}, () => {
|
|
2104
|
+
const el = document.getElementById('codTimerText');
|
|
2105
|
+
if (el) el.textContent = 'Expired';
|
|
2106
|
+
fetch(`${API_BASE}/api/v1/checkout/${PAYMENT_ID}/expire`, { method: 'POST', headers: getApiHeaders() }).catch(() => { });
|
|
2107
|
+
loadState();
|
|
2108
|
+
}, data.expiresAt);
|
|
2109
|
+
} else if (currentSelectedMethod === 'CASH_COUNTER') {
|
|
2110
|
+
const vCash = document.getElementById('viewCashContent');
|
|
2111
|
+
if (vCash) vCash.hidden = false;
|
|
2112
|
+
document.getElementById('ypgStickyFooter').style.display = 'none';
|
|
2113
|
+
} else if (currentSelectedMethod === 'CARD_COUNTER') {
|
|
2114
|
+
const vCard = document.getElementById('viewCardContent');
|
|
2115
|
+
if (vCard) vCard.hidden = false;
|
|
2116
|
+
document.getElementById('ypgStickyFooter').style.display = 'none';
|
|
2117
|
+
} else {
|
|
2118
|
+
// Default: UPI QR (CRITICAL INVARIANT: ALWAYS SHOWS QR AND RUNS 5:00 TIMER REGARDLESS OF GREEN/YELLOW/RED)
|
|
2119
|
+
document.getElementById('viewUpiContent').hidden = false;
|
|
2120
|
+
document.getElementById('ypgStickyFooter').style.display = 'flex';
|
|
2121
|
+
loadQrSvg();
|
|
2122
|
+
|
|
2123
|
+
const btnContinue = document.getElementById('btnContinuePayment');
|
|
2124
|
+
|
|
2125
|
+
const remainingSec = (data.timeRemainingSeconds !== undefined && data.timeRemainingSeconds !== null) ? Number(data.timeRemainingSeconds) : 300;
|
|
2126
|
+
const targetExpiry = data.expiresAt || new Date(Date.now() + remainingSec * 1000).toISOString();
|
|
2127
|
+
|
|
2128
|
+
if (remainingSec <= 0 && data.status === 'ACTIVE') {
|
|
2129
|
+
const tText = document.getElementById('activeTimerText');
|
|
2130
|
+
if (tText) tText.textContent = 'Expired';
|
|
2131
|
+
if (btnContinue) btnContinue.style.display = 'none';
|
|
2132
|
+
return;
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
// Initialize button hidden until 4:00 has elapsed
|
|
2136
|
+
if (btnContinue && remainingSec > 240) {
|
|
2137
|
+
btnContinue.style.display = 'none';
|
|
2138
|
+
} else if (btnContinue) {
|
|
2139
|
+
btnContinue.style.display = 'block';
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
runCountdown(remainingSec, sec => {
|
|
2143
|
+
const tText = document.getElementById('activeTimerText');
|
|
2144
|
+
if (tText) tText.textContent = formatTime(sec);
|
|
2145
|
+
|
|
2146
|
+
// "Payment Done — Check My Payment" button appears after 4:00
|
|
2147
|
+
if (sec <= 240 && btnContinue) {
|
|
2148
|
+
btnContinue.style.display = 'block';
|
|
2149
|
+
} else if (btnContinue) {
|
|
2150
|
+
btnContinue.style.display = 'none';
|
|
2151
|
+
}
|
|
2152
|
+
}, () => {
|
|
2153
|
+
const tText = document.getElementById('activeTimerText');
|
|
2154
|
+
if (tText) tText.textContent = 'Expired';
|
|
2155
|
+
if (btnContinue) btnContinue.style.display = 'none';
|
|
2156
|
+
closeProcessingModal();
|
|
2157
|
+
fetch(`${API_BASE}/api/v1/checkout/${PAYMENT_ID}/expire`, { method: 'POST', headers: getApiHeaders() }).catch(() => { });
|
|
2158
|
+
loadState();
|
|
2159
|
+
}, targetExpiry);
|
|
2160
|
+
}
|
|
2161
|
+
return;
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
if (data.status === 'PROCESSING') {
|
|
2165
|
+
const bar = document.getElementById('paymentMethodSelectorBar');
|
|
2166
|
+
if (bar) bar.style.display = 'none';
|
|
2167
|
+
|
|
2168
|
+
hideAllContent();
|
|
2169
|
+
document.getElementById('viewProcessingContent').hidden = false;
|
|
2170
|
+
document.getElementById('ypgStickyFooter').style.display = 'none';
|
|
2171
|
+
runCountdown(data.timeRemainingSeconds, sec => {
|
|
2172
|
+
document.getElementById('procTimerText').textContent = `${sec}s`;
|
|
2173
|
+
}, () => loadState(), data.expiresAt);
|
|
2174
|
+
return;
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
// --- PSYCHOLOGICAL TIME-SAVING CONVERSION BANNER ---
|
|
2181
|
+
// Invariant: ONLY display when the customer is actively waiting in the UPI queue!
|
|
2182
|
+
function renderTimeSavingBanners(data) {
|
|
2183
|
+
const qBanner = document.getElementById('queueTimeSavingBanner');
|
|
2184
|
+
const uBanner = document.getElementById('upiTimeSavingBanner');
|
|
2185
|
+
if (uBanner) uBanner.style.display = 'none'; // NEVER show on active UPI session
|
|
2186
|
+
|
|
2187
|
+
const isQueued = (data.status === 'QUEUED') || (Number(data.queuePosition || 0) > 0);
|
|
2188
|
+
if (!isQueued || !qBanner) {
|
|
2189
|
+
if (qBanner) qBanner.style.display = 'none';
|
|
2190
|
+
return;
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
const enabled = data.enabledMethods || ['UPI'];
|
|
2194
|
+
const sellingMode = data.sellingMode || 'OFFLINE';
|
|
2195
|
+
|
|
2196
|
+
let altMethodCode = null;
|
|
2197
|
+
let altMethodName = null;
|
|
2198
|
+
|
|
2199
|
+
if (sellingMode === 'ONLINE') {
|
|
2200
|
+
if (enabled.includes('COD')) {
|
|
2201
|
+
altMethodCode = 'COD';
|
|
2202
|
+
altMethodName = 'Cash on Delivery';
|
|
2203
|
+
} else if (enabled.includes('CARD')) {
|
|
2204
|
+
altMethodCode = 'CARD';
|
|
2205
|
+
altMethodName = 'Credit / Debit Card';
|
|
2206
|
+
} else if (enabled.includes('NETBANKING')) {
|
|
2207
|
+
altMethodCode = 'NETBANKING';
|
|
2208
|
+
altMethodName = 'Net Banking';
|
|
2209
|
+
} else if (enabled.includes('CARD_COUNTER')) {
|
|
2210
|
+
altMethodCode = 'CARD_COUNTER';
|
|
2211
|
+
altMethodName = 'Card Payment';
|
|
2212
|
+
}
|
|
2213
|
+
} else {
|
|
2214
|
+
// Offline selling mode (retail till / counter)
|
|
2215
|
+
if (enabled.includes('CASH_COUNTER')) {
|
|
2216
|
+
altMethodCode = 'CASH_COUNTER';
|
|
2217
|
+
altMethodName = 'Cash at Till';
|
|
2218
|
+
} else if (enabled.includes('CARD_COUNTER')) {
|
|
2219
|
+
altMethodCode = 'CARD_COUNTER';
|
|
2220
|
+
altMethodName = 'Card at Counter';
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
// CRITICAL REQUIREMENT: If both cash and card are disabled by merchant, psychological conversion MUST NOT BE SHOWN!
|
|
2225
|
+
if (!altMethodCode || (!enabled.includes('COD') && !enabled.includes('CASH_COUNTER') && !enabled.includes('CARD') && !enabled.includes('CARD_COUNTER'))) {
|
|
2226
|
+
qBanner.style.display = 'none';
|
|
2227
|
+
return;
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
const timeSec = data.timeRemainingSeconds || (data.queuePosition ? data.queuePosition * 30 : 30);
|
|
2231
|
+
const initialTimerStr = formatTime(timeSec);
|
|
2232
|
+
|
|
2233
|
+
const actionText = sellingMode === 'ONLINE'
|
|
2234
|
+
? `Skip the UPI wait! Pay with <strong>${altMethodName}</strong> instantly without waiting for a UPI slot.`
|
|
2235
|
+
: `Don't wait in the UPI line! Pay with <strong>${altMethodName}</strong> right now to complete your purchase instantly.`;
|
|
2236
|
+
|
|
2237
|
+
qBanner.innerHTML = `
|
|
2238
|
+
<div class="ypg-queue-conversion-card">
|
|
2239
|
+
<div class="ypg-queue-conversion-header">
|
|
2240
|
+
<div class="ypg-queue-conversion-badge"><svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" style="display:inline-block; vertical-align:middle; margin-right:3px;"><path d="M12 2l2.4 7.2h7.6l-6 4.8 2.4 7.2-6.4-4.8-6.4 4.8 2.4-7.2-6-4.8h7.6z"/></svg>Instant Alternative</div>
|
|
2241
|
+
<div class="ypg-queue-conversion-pill">Save <span class="ypg-banner-running-timer">${initialTimerStr}</span></div>
|
|
2242
|
+
</div>
|
|
2243
|
+
<div class="ypg-queue-conversion-text">
|
|
2244
|
+
${actionText}
|
|
2245
|
+
</div>
|
|
2246
|
+
<button type="button" class="ypg-btn-queue-switch" onclick="selectPaymentMethod('${altMethodCode}')">
|
|
2247
|
+
<span>Pay with ${altMethodName}</span>
|
|
2248
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="9 18 15 12 9 6"/></svg>
|
|
2249
|
+
</button>
|
|
2250
|
+
</div>
|
|
2251
|
+
`;
|
|
2252
|
+
qBanner.style.display = 'block';
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
// --- PURE QR SVG LOADER (ALWAYS LOADS & SHOWS QR CODE REGARDLESS OF SERVER STATUS) ---
|
|
2256
|
+
async function loadQrSvg() {
|
|
2257
|
+
const container = document.getElementById('qrImageContainer');
|
|
2258
|
+
if (!container) return;
|
|
2259
|
+
try {
|
|
2260
|
+
const res = await fetch(`/checkout/${PAYMENT_ID}/qr.svg`);
|
|
2261
|
+
if (res.ok) {
|
|
2262
|
+
const svg = await res.text();
|
|
2263
|
+
if (svg && svg.includes('<svg')) {
|
|
2264
|
+
container.innerHTML = svg;
|
|
2265
|
+
return;
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
} catch (e) {
|
|
2269
|
+
console.warn('loadQrSvg error:', e);
|
|
2270
|
+
}
|
|
2271
|
+
// Automatic retry to guarantee QR code is loaded
|
|
2272
|
+
setTimeout(async () => {
|
|
2273
|
+
try {
|
|
2274
|
+
const res = await fetch(`/checkout/${PAYMENT_ID}/qr.svg`);
|
|
2275
|
+
if (res.ok) {
|
|
2276
|
+
const svg = await res.text();
|
|
2277
|
+
if (svg && svg.includes('<svg')) {
|
|
2278
|
+
container.innerHTML = svg;
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
} catch (e) {}
|
|
2282
|
+
}, 400);
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
function copyVpaId() {
|
|
2286
|
+
const vpa = (state?.payeeVpa || document.getElementById('displayVpaVal')?.textContent || '').trim();
|
|
2287
|
+
if (!vpa) return;
|
|
2288
|
+
|
|
2289
|
+
const btn = document.getElementById('btnCopyVpa');
|
|
2290
|
+
const text = document.getElementById('copyVpaText');
|
|
2291
|
+
if (btn && text) {
|
|
2292
|
+
navigator.clipboard.writeText(vpa).then(() => {
|
|
2293
|
+
text.textContent = 'Copied!';
|
|
2294
|
+
btn.classList.add('copied');
|
|
2295
|
+
setTimeout(() => {
|
|
2296
|
+
text.textContent = 'Copy';
|
|
2297
|
+
btn.classList.remove('copied');
|
|
2298
|
+
}, 2000);
|
|
2299
|
+
}).catch(() => {
|
|
2300
|
+
text.textContent = 'Copied!';
|
|
2301
|
+
setTimeout(() => { text.textContent = 'Copy'; }, 2000);
|
|
2302
|
+
});
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
// --- RAZORPAY AUTHENTIC "PROCESSING YOUR PAYMENT" MODAL SHEET ---
|
|
2307
|
+
function openProcessingModal(title = 'Processing your payment', subtitle = 'This will only take a few seconds.') {
|
|
2308
|
+
const modal = document.getElementById('rzpProcessingModal');
|
|
2309
|
+
const tEl = document.getElementById('rzpProcessingTitle');
|
|
2310
|
+
const sEl = document.getElementById('rzpProcessingSubtitle');
|
|
2311
|
+
if (tEl) tEl.textContent = title;
|
|
2312
|
+
if (sEl) sEl.textContent = subtitle;
|
|
2313
|
+
if (modal) {
|
|
2314
|
+
modal.hidden = false;
|
|
2315
|
+
requestAnimationFrame(() => modal.classList.add('active'));
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
function closeProcessingModal() {
|
|
2320
|
+
const modal = document.getElementById('rzpProcessingModal');
|
|
2321
|
+
if (modal) {
|
|
2322
|
+
modal.classList.remove('active');
|
|
2323
|
+
setTimeout(() => { modal.hidden = true; }, 300);
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
// --- RAZORPAY AUTHENTIC "CANCEL PAYMENT" CONFIRMATION MODAL SHEET ---
|
|
2328
|
+
function openCancelPaymentModal() {
|
|
2329
|
+
emitTriggerHook('CANCEL_CLICKED');
|
|
2330
|
+
document.body.style.overflow = 'hidden';
|
|
2331
|
+
const yBody = document.querySelector('.ypg-body');
|
|
2332
|
+
if (yBody) yBody.style.overflow = 'hidden';
|
|
2333
|
+
|
|
2334
|
+
const modal = document.getElementById('rzpCancelModal');
|
|
2335
|
+
const promptView = document.getElementById('rzpCancelPromptView');
|
|
2336
|
+
const progressView = document.getElementById('rzpCancellingProgressView');
|
|
2337
|
+
if (promptView) promptView.style.display = 'flex';
|
|
2338
|
+
if (progressView) progressView.style.display = 'none';
|
|
2339
|
+
if (modal) {
|
|
2340
|
+
modal.hidden = false;
|
|
2341
|
+
requestAnimationFrame(() => modal.classList.add('active'));
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
function closeCancelPaymentModal() {
|
|
2346
|
+
document.body.style.overflow = '';
|
|
2347
|
+
const yBody = document.querySelector('.ypg-body');
|
|
2348
|
+
if (yBody) yBody.style.overflow = '';
|
|
2349
|
+
|
|
2350
|
+
const modal = document.getElementById('rzpCancelModal');
|
|
2351
|
+
if (modal) {
|
|
2352
|
+
modal.classList.remove('active');
|
|
2353
|
+
setTimeout(() => { modal.hidden = true; }, 300);
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
async function confirmCancelPayment() {
|
|
2358
|
+
emitTriggerHook('CANCEL_CONFIRMED');
|
|
2359
|
+
if (countdownTimer) { clearInterval(countdownTimer); countdownTimer = null; }
|
|
2360
|
+
if (queuePollTimer) { clearInterval(queuePollTimer); queuePollTimer = null; }
|
|
2361
|
+
|
|
2362
|
+
const promptView = document.getElementById('rzpCancelPromptView');
|
|
2363
|
+
const progressView = document.getElementById('rzpCancellingProgressView');
|
|
2364
|
+
if (promptView) promptView.style.display = 'none';
|
|
2365
|
+
if (progressView) progressView.style.display = 'flex';
|
|
2366
|
+
|
|
2367
|
+
try {
|
|
2368
|
+
await fetch(`${API_BASE}/api/v1/checkout/${PAYMENT_ID}/cancel`, {
|
|
2369
|
+
method: 'POST',
|
|
2370
|
+
headers: getApiHeaders()
|
|
2371
|
+
});
|
|
2372
|
+
} catch (e) { }
|
|
2373
|
+
|
|
2374
|
+
// Smooth realistic delay matching Razorpay transition
|
|
2375
|
+
setTimeout(() => {
|
|
2376
|
+
document.body.style.overflow = '';
|
|
2377
|
+
const yBody = document.querySelector('.ypg-body');
|
|
2378
|
+
if (yBody) yBody.style.overflow = '';
|
|
2379
|
+
|
|
2380
|
+
closeCancelPaymentModal();
|
|
2381
|
+
closeProcessingModal();
|
|
2382
|
+
|
|
2383
|
+
const storeUrl = getStoreReturnUrl();
|
|
2384
|
+
const isEmbedded = (window.parent && window.parent !== window) || location.search.includes('embedded=1');
|
|
2385
|
+
|
|
2386
|
+
if (isEmbedded) {
|
|
2387
|
+
try {
|
|
2388
|
+
postToMerchant({ event: 'elpay.modal_close', redirectUrl: storeUrl });
|
|
2389
|
+
postToMerchant({ event: 'elpay.close', redirectUrl: storeUrl });
|
|
2390
|
+
postToMerchant({ event: 'elpay.cancel', redirectUrl: storeUrl });
|
|
2391
|
+
postToMerchant({ event: 'elpay.payment_cancelled', redirectUrl: storeUrl });
|
|
2392
|
+
} catch (e) { }
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
loadState();
|
|
2396
|
+
}, 500);
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
// Direct invocation from header back button, close button, or inline links
|
|
2400
|
+
function cancelPayment() {
|
|
2401
|
+
openCancelPaymentModal();
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
// Listen for cancel requests from parent iframe cross button
|
|
2405
|
+
window.addEventListener('message', (e) => {
|
|
2406
|
+
if (!e.data || e.data.event !== 'elpay.request_cancel') return;
|
|
2407
|
+
// Only the storefront that embeds this iframe may drive it.
|
|
2408
|
+
const merchantOrigin = resolveMerchantOrigin();
|
|
2409
|
+
if (merchantOrigin && e.origin !== merchantOrigin) return;
|
|
2410
|
+
openCancelPaymentModal();
|
|
2411
|
+
});
|
|
2412
|
+
|
|
2413
|
+
// --- MANUAL PAYMENT DONE VERIFICATION ---
|
|
2414
|
+
async function submitPaymentDone() {
|
|
2415
|
+
openProcessingModal('Processing your payment', 'This will only take a few seconds.');
|
|
2416
|
+
emitTriggerHook('PAYMENT_DONE_CLICKED');
|
|
2417
|
+
|
|
2418
|
+
try {
|
|
2419
|
+
const res = await fetch(`${API_BASE}/api/v1/checkout/${PAYMENT_ID}/payment-done`, {
|
|
2420
|
+
method: 'POST',
|
|
2421
|
+
headers: getApiHeaders()
|
|
2422
|
+
});
|
|
2423
|
+
if (res.ok) {
|
|
2424
|
+
loadState();
|
|
2425
|
+
}
|
|
2426
|
+
} catch (e) { }
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
// --- SINGLE ACTIVE PAYMENT SESSION CONFLICT RESOLUTION ---
|
|
2430
|
+
let activeConflictSessionId = null;
|
|
2431
|
+
|
|
2432
|
+
async function executeRetryNewPayment() {
|
|
2433
|
+
try {
|
|
2434
|
+
const btn = document.getElementById('btnEndSessionAndStartNew');
|
|
2435
|
+
if (btn) {
|
|
2436
|
+
btn.disabled = true;
|
|
2437
|
+
btn.textContent = 'Activating new payment...';
|
|
2438
|
+
}
|
|
2439
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
2440
|
+
if (typeof DeviceIdentity !== 'undefined') {
|
|
2441
|
+
if (DeviceIdentity.currentDeviceId) headers['X-Device-Id'] = DeviceIdentity.currentDeviceId;
|
|
2442
|
+
if (DeviceIdentity.currentDeviceToken) headers['X-Device-Token'] = DeviceIdentity.currentDeviceToken;
|
|
2443
|
+
}
|
|
2444
|
+
const token = activeToken || localStorage.getItem('ypg_customer_token');
|
|
2445
|
+
if (token) {
|
|
2446
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
2447
|
+
headers['x-customer-token'] = token;
|
|
2448
|
+
}
|
|
2449
|
+
const res = await fetch(`${API_BASE}/api/v1/checkout/${PAYMENT_ID}/retry-new-payment`, {
|
|
2450
|
+
method: 'POST',
|
|
2451
|
+
headers
|
|
2452
|
+
});
|
|
2453
|
+
if (res.ok) {
|
|
2454
|
+
const conflictModal = document.getElementById('activeConflictModal');
|
|
2455
|
+
if (conflictModal) conflictModal.hidden = true;
|
|
2456
|
+
await loadState();
|
|
2457
|
+
} else {
|
|
2458
|
+
const err = await res.json().catch(() => ({}));
|
|
2459
|
+
alert(err.error || 'Failed to retry payment');
|
|
2460
|
+
if (btn) {
|
|
2461
|
+
btn.disabled = false;
|
|
2462
|
+
btn.textContent = 'END SESSION & START NEW PAYMENT';
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
} catch (e) {
|
|
2466
|
+
alert('Network error while retrying payment');
|
|
2467
|
+
const btn = document.getElementById('btnEndSessionAndStartNew');
|
|
2468
|
+
if (btn) {
|
|
2469
|
+
btn.disabled = false;
|
|
2470
|
+
btn.textContent = 'END SESSION & START NEW PAYMENT';
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
function continueExistingActivePayment() {
|
|
2476
|
+
if (activeConflictSessionId) {
|
|
2477
|
+
window.location.href = `/checkout/${activeConflictSessionId}`;
|
|
2478
|
+
} else {
|
|
2479
|
+
const conflictModal = document.getElementById('activeConflictModal');
|
|
2480
|
+
if (conflictModal) conflictModal.hidden = true;
|
|
2481
|
+
loadState();
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
|
|
2486
|
+
|
|
2487
|
+
function renderBlockedState(data) {
|
|
2488
|
+
closeProcessingModal();
|
|
2489
|
+
closeCancelPaymentModal();
|
|
2490
|
+
if (countdownTimer) { clearInterval(countdownTimer); countdownTimer = null; }
|
|
2491
|
+
if (queuePollTimer) { clearInterval(queuePollTimer); queuePollTimer = null; }
|
|
2492
|
+
|
|
2493
|
+
const bar = document.getElementById('paymentMethodSelectorBar');
|
|
2494
|
+
if (bar) bar.style.display = 'none';
|
|
2495
|
+
const footer = document.getElementById('ypgStickyFooter');
|
|
2496
|
+
if (footer) footer.style.display = 'none';
|
|
2497
|
+
|
|
2498
|
+
hideAllContent();
|
|
2499
|
+
const vBlocked = document.getElementById('viewBlockedContent');
|
|
2500
|
+
if (vBlocked) vBlocked.hidden = false;
|
|
2501
|
+
|
|
2502
|
+
// Expand to full width on PC desktop split view
|
|
2503
|
+
const pcMethodsList = document.getElementById('pcMethodsList');
|
|
2504
|
+
if (pcMethodsList) pcMethodsList.style.setProperty('display', 'none', 'important');
|
|
2505
|
+
const pcActiveView = document.querySelector('.ypg-pc-active-view');
|
|
2506
|
+
if (pcActiveView) pcActiveView.style.width = '100%';
|
|
2507
|
+
const pcTopbarTitle = document.querySelector('.ypg-pc-topbar-title');
|
|
2508
|
+
if (pcTopbarTitle) pcTopbarTitle.textContent = 'Security Incident Investigation';
|
|
2509
|
+
|
|
2510
|
+
const orderRef = data.orderId || state?.orderId || 'N/A';
|
|
2511
|
+
const orderEl = document.getElementById('blockedOrderRefVal');
|
|
2512
|
+
if (orderEl) orderEl.textContent = `#${orderRef}`;
|
|
2513
|
+
|
|
2514
|
+
const sessEl = document.getElementById('blockedSessionIdVal');
|
|
2515
|
+
if (sessEl) sessEl.textContent = data.id || PAYMENT_ID || 'N/A';
|
|
2516
|
+
|
|
2517
|
+
const timeEl = document.getElementById('blockedTimestampVal');
|
|
2518
|
+
if (timeEl) timeEl.textContent = new Date().toLocaleTimeString();
|
|
2519
|
+
|
|
2520
|
+
if (data.blockedNotice?.title) {
|
|
2521
|
+
const titleEl = document.getElementById('blockedTitleText');
|
|
2522
|
+
if (titleEl) titleEl.textContent = data.blockedNotice.title;
|
|
2523
|
+
}
|
|
2524
|
+
if (data.blockedNotice?.subtitle) {
|
|
2525
|
+
const subEl = document.getElementById('blockedSubtitleText');
|
|
2526
|
+
if (subEl) subEl.textContent = data.blockedNotice.subtitle;
|
|
2527
|
+
}
|
|
2528
|
+
if (data.blockedNotice?.message) {
|
|
2529
|
+
const msgEl = document.getElementById('blockedNoticeText');
|
|
2530
|
+
if (msgEl) msgEl.textContent = data.blockedNotice.message;
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
emitTriggerHook('PAYMENT_BLOCKED', {
|
|
2534
|
+
reason: data.reason || data.blockedNotice?.reason || 'FRAUDULENT_PAYMENT_INJECTION',
|
|
2535
|
+
orderId: orderRef
|
|
2536
|
+
});
|
|
2537
|
+
|
|
2538
|
+
try {
|
|
2539
|
+
postToMerchant({
|
|
2540
|
+
event: 'elpay.payment_blocked',
|
|
2541
|
+
data: {
|
|
2542
|
+
status: 'BLOCKED',
|
|
2543
|
+
isBanned: true,
|
|
2544
|
+
orderId: orderRef,
|
|
2545
|
+
paymentId: data.id || PAYMENT_ID
|
|
2546
|
+
}
|
|
2547
|
+
});
|
|
2548
|
+
} catch (e) {}
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
function handleBlockedExit() {
|
|
2552
|
+
const failRedirectUrl = getFailedRedirectUrl();
|
|
2553
|
+
if (window.parent && window.parent !== window) {
|
|
2554
|
+
try {
|
|
2555
|
+
postToMerchant({
|
|
2556
|
+
event: 'elpay.payment_blocked_exit',
|
|
2557
|
+
data: { status: 'BLOCKED', isBanned: true, orderId: state?.orderId }
|
|
2558
|
+
});
|
|
2559
|
+
} catch (e) {}
|
|
2560
|
+
}
|
|
2561
|
+
window.location.href = failRedirectUrl;
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
let lastDuplicateSessionPayload = null;
|
|
2565
|
+
function renderDuplicateSession(data) {
|
|
2566
|
+
lastDuplicateSessionPayload = data;
|
|
2567
|
+
if (countdownTimer) { clearInterval(countdownTimer); countdownTimer = null; }
|
|
2568
|
+
if (queuePollTimer) { clearInterval(queuePollTimer); queuePollTimer = null; }
|
|
2569
|
+
if (sse) { try { sse.close(); } catch(e){} sse = null; }
|
|
2570
|
+
|
|
2571
|
+
const bar = document.getElementById('paymentMethodSelectorBar');
|
|
2572
|
+
if (bar) bar.style.display = 'none';
|
|
2573
|
+
const footer = document.getElementById('ypgStickyFooter');
|
|
2574
|
+
if (footer) footer.style.display = 'none';
|
|
2575
|
+
|
|
2576
|
+
hideAllContent();
|
|
2577
|
+
const vDup = document.getElementById('viewDuplicateSessionContent');
|
|
2578
|
+
if (vDup) vDup.hidden = false;
|
|
2579
|
+
|
|
2580
|
+
// Expand to full width on PC desktop split view
|
|
2581
|
+
const pcMethodsList = document.getElementById('pcMethodsList');
|
|
2582
|
+
if (pcMethodsList) pcMethodsList.style.setProperty('display', 'none', 'important');
|
|
2583
|
+
const pcActiveView = document.querySelector('.ypg-pc-active-view');
|
|
2584
|
+
if (pcActiveView) pcActiveView.style.width = '100%';
|
|
2585
|
+
const pcTopbarTitle = document.querySelector('.ypg-pc-topbar-title');
|
|
2586
|
+
if (pcTopbarTitle) pcTopbarTitle.textContent = 'Device Security Verification';
|
|
2587
|
+
|
|
2588
|
+
emitTriggerHook('DUPLICATE_SESSION_DETECTED', {
|
|
2589
|
+
sessionId: data.session_id || PAYMENT_ID,
|
|
2590
|
+
action: 'END_SESSION_AND_CREATE_NEW'
|
|
2591
|
+
});
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
async function handleEndAndStartNewPayment() {
|
|
2595
|
+
const btn = document.getElementById('btnEndAndRestartPayment');
|
|
2596
|
+
if (btn) {
|
|
2597
|
+
btn.disabled = true;
|
|
2598
|
+
btn.innerHTML = '<span class="ypg-spinner" style="display:inline-block;width:15px;height:15px;border:2px solid #fff;border-top-color:transparent;border-radius:50%;animation:rzpRotate 0.8s linear infinite;margin-right:8px;vertical-align:middle;"></span> CREATING NEW PAYMENT...';
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
try {
|
|
2602
|
+
await DeviceIdentity.getOrRegister();
|
|
2603
|
+
const headers = getApiHeaders({ 'Content-Type': 'application/json' });
|
|
2604
|
+
|
|
2605
|
+
const conflictingSessionId = lastDuplicateSessionPayload?.conflicting_session_id || activeConflictSessionId || null;
|
|
2606
|
+
|
|
2607
|
+
const res = await fetch(`${API_BASE}/api/v1/checkout/${PAYMENT_ID}/end-and-restart-payment`, {
|
|
2608
|
+
method: 'POST',
|
|
2609
|
+
headers,
|
|
2610
|
+
body: JSON.stringify({
|
|
2611
|
+
deviceId: DeviceIdentity.currentDeviceId,
|
|
2612
|
+
deviceToken: DeviceIdentity.currentDeviceToken,
|
|
2613
|
+
conflictingSessionId
|
|
2614
|
+
})
|
|
2615
|
+
});
|
|
2616
|
+
|
|
2617
|
+
const data = await res.json();
|
|
2618
|
+
if (data.success && (data.checkoutUrl || data.newSessionId)) {
|
|
2619
|
+
const targetUrl = data.checkoutUrl || `/checkout/${data.newSessionId}`;
|
|
2620
|
+
window.location.href = targetUrl;
|
|
2621
|
+
} else {
|
|
2622
|
+
alert(data.error || 'Failed to start new payment session. Please try again.');
|
|
2623
|
+
if (btn) {
|
|
2624
|
+
btn.disabled = false;
|
|
2625
|
+
btn.innerHTML = '<span>END SESSION & START NEW PAYMENT</span>';
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
} catch (err) {
|
|
2629
|
+
alert('Network error while starting new payment session.');
|
|
2630
|
+
if (btn) {
|
|
2631
|
+
btn.disabled = false;
|
|
2632
|
+
btn.innerHTML = '<span>END SESSION & START NEW PAYMENT</span>';
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
// --- SSE REALTIME STREAM (AUTOMATIC OBSERVER & CASHIER INTEGRATION) ---
|
|
2638
|
+
function connectSse() {
|
|
2639
|
+
if (sse) {
|
|
2640
|
+
try { sse.close(); } catch (e) { }
|
|
2641
|
+
sse = null;
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2644
|
+
try {
|
|
2645
|
+
const params = new URLSearchParams();
|
|
2646
|
+
if (DeviceIdentity.currentDeviceId) params.set('deviceId', DeviceIdentity.currentDeviceId);
|
|
2647
|
+
if (DeviceIdentity.currentDeviceToken) params.set('deviceToken', DeviceIdentity.currentDeviceToken);
|
|
2648
|
+
const qs = params.toString();
|
|
2649
|
+
const streamUrl = `${API_BASE}/api/v1/checkout/${PAYMENT_ID}/stream${qs ? '?' + qs : ''}`;
|
|
2650
|
+
sse = new EventSource(streamUrl);
|
|
2651
|
+
|
|
2652
|
+
const handleRealtimePayload = (event) => {
|
|
2653
|
+
try {
|
|
2654
|
+
const data = JSON.parse(event.data);
|
|
2655
|
+
if (data && (data.error === 'DUPLICATE_SESSION_DETECTED' || data.type === 'DUPLICATE_SESSION_DETECTED')) {
|
|
2656
|
+
renderDuplicateSession(data);
|
|
2657
|
+
} else if (data && (data.status === 'BLOCKED' || data.isBanned || data.fraudDetected || data.type === 'FRAUD_BLOCKED' || data.event === 'account_blocked')) {
|
|
2658
|
+
renderBlockedState(data);
|
|
2659
|
+
} else if (data && data.type === 'STATE_CHANGE' && data.payload) {
|
|
2660
|
+
renderState(data.payload);
|
|
2661
|
+
} else if (data && data.status) {
|
|
2662
|
+
if (data.status === 'BLOCKED' || data.isBanned) {
|
|
2663
|
+
renderBlockedState(data);
|
|
2664
|
+
} else {
|
|
2665
|
+
loadState();
|
|
2666
|
+
}
|
|
2667
|
+
} else {
|
|
2668
|
+
loadState();
|
|
2669
|
+
}
|
|
2670
|
+
} catch (e) {
|
|
2671
|
+
loadState();
|
|
2672
|
+
}
|
|
2673
|
+
};
|
|
2674
|
+
|
|
2675
|
+
sse.onmessage = handleRealtimePayload;
|
|
2676
|
+
sse.addEventListener('status_change', handleRealtimePayload);
|
|
2677
|
+
sse.addEventListener('state_change', handleRealtimePayload);
|
|
2678
|
+
sse.addEventListener('account_blocked', handleRealtimePayload);
|
|
2679
|
+
sse.addEventListener('fraud_detected', handleRealtimePayload);
|
|
2680
|
+
sse.addEventListener('counter_payment_confirmed', handleRealtimePayload);
|
|
2681
|
+
sse.addEventListener('counter_payment_declined', handleRealtimePayload);
|
|
2682
|
+
sse.addEventListener('payment_success', handleRealtimePayload);
|
|
2683
|
+
sse.addEventListener('queue_cleared', handleRealtimePayload);
|
|
2684
|
+
sse.addEventListener('session_expired', handleRealtimePayload);
|
|
2685
|
+
|
|
2686
|
+
sse.onerror = () => {
|
|
2687
|
+
if (sse) {
|
|
2688
|
+
try { sse.close(); } catch (e) { }
|
|
2689
|
+
sse = null;
|
|
2690
|
+
}
|
|
2691
|
+
setTimeout(connectSse, 3000);
|
|
2692
|
+
};
|
|
2693
|
+
} catch (e) {
|
|
2694
|
+
setTimeout(connectSse, 3000);
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
// Continuous 2-second fallback polling: guarantees instant response when cashier confirms/declines or queues clear
|
|
2699
|
+
setInterval(() => {
|
|
2700
|
+
if (state && (state.status === 'SUCCESS' || state.status === 'FAILED' || state.status === 'EXPIRED' || state.status === 'BLOCKED' || state.isBanned)) {
|
|
2701
|
+
return;
|
|
2702
|
+
}
|
|
2703
|
+
loadState();
|
|
2704
|
+
}, 2000);
|
|
2705
|
+
|
|
2706
|
+
// --- SUPPORT / HELP MODAL ---
|
|
2707
|
+
function openHelpModal() {
|
|
2708
|
+
document.getElementById('helpModal').hidden = false;
|
|
2709
|
+
const popoverMobile = document.getElementById('accountMenuPopover');
|
|
2710
|
+
if (popoverMobile) { popoverMobile.setAttribute('hidden', ''); popoverMobile.hidden = true; popoverMobile.style.display = 'none'; }
|
|
2711
|
+
const popoverPc = document.getElementById('accountMenuPopoverPc');
|
|
2712
|
+
if (popoverPc) { popoverPc.setAttribute('hidden', ''); popoverPc.hidden = true; popoverPc.style.display = 'none'; }
|
|
2713
|
+
|
|
2714
|
+
const branding = state?.branding || {};
|
|
2715
|
+
renderSupportInfo({
|
|
2716
|
+
supportEmails: branding.supportEmails || (branding.supportEmail ? [branding.supportEmail] : []),
|
|
2717
|
+
supportMobiles: branding.supportMobiles || (branding.supportMobile ? [branding.supportMobile] : [])
|
|
2718
|
+
});
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
function renderSupportInfo(info) {
|
|
2722
|
+
const emailBox = document.getElementById('helpModalEmailsContainer');
|
|
2723
|
+
const phoneBox = document.getElementById('helpModalMobilesContainer');
|
|
2724
|
+
const caseBox = document.getElementById('helpModalCaseInfoText');
|
|
2725
|
+
|
|
2726
|
+
// Support contacts are merchant branding, i.e. attacker-influenced input,
|
|
2727
|
+
// so they are escaped before going into innerHTML.
|
|
2728
|
+
if (emailBox && info.supportEmails) {
|
|
2729
|
+
emailBox.innerHTML = `<strong>Email:</strong> ${escapeHtml(info.supportEmails.join(', '))}`;
|
|
2730
|
+
}
|
|
2731
|
+
if (phoneBox && info.supportMobiles) {
|
|
2732
|
+
phoneBox.innerHTML = `<strong>Phone:</strong> ${escapeHtml(info.supportMobiles.join(', '))}`;
|
|
2733
|
+
}
|
|
2734
|
+
if (caseBox) {
|
|
2735
|
+
caseBox.textContent = `PAYMENT_ID: ${PAYMENT_ID}\nORDER_ID: ${state?.orderId || 'N/A'}\nAMOUNT: ₹${state?.amount || 'N/A'}\nSTATUS: ${state?.status || 'N/A'}`;
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
function closeHelpModal() {
|
|
2740
|
+
document.getElementById('helpModal').hidden = true;
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
function copyCaseInfo() {
|
|
2744
|
+
const text = document.getElementById('helpModalCaseInfoText').textContent;
|
|
2745
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
2746
|
+
document.getElementById('copyCaseText').textContent = 'Copied to Clipboard!';
|
|
2747
|
+
setTimeout(() => {
|
|
2748
|
+
document.getElementById('copyCaseText').textContent = 'Copy Payment Case Details';
|
|
2749
|
+
}, 2000);
|
|
2750
|
+
});
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
// WhatsApp OTP Input Keyboard Listeners
|
|
2754
|
+
const mobIn = document.getElementById('authMobileInput');
|
|
2755
|
+
if (mobIn) {
|
|
2756
|
+
mobIn.addEventListener('input', (e) => {
|
|
2757
|
+
const clean = e.target.value.replace(/\D/g, '').slice(0, 10);
|
|
2758
|
+
if (e.target.value !== clean) {
|
|
2759
|
+
e.target.value = clean;
|
|
2760
|
+
}
|
|
2761
|
+
currentAuthMobile = clean;
|
|
2762
|
+
});
|
|
2763
|
+
mobIn.addEventListener('keydown', (e) => {
|
|
2764
|
+
if (e.key === 'Enter') {
|
|
2765
|
+
e.preventDefault();
|
|
2766
|
+
handleAuthMobileSubmit();
|
|
2767
|
+
}
|
|
2768
|
+
});
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
const nameIn = document.getElementById('authSignupName');
|
|
2772
|
+
const emailIn = document.getElementById('authSignupEmail');
|
|
2773
|
+
if (nameIn) {
|
|
2774
|
+
nameIn.addEventListener('input', (e) => {
|
|
2775
|
+
currentAuthName = e.target.value;
|
|
2776
|
+
});
|
|
2777
|
+
nameIn.addEventListener('keydown', (e) => {
|
|
2778
|
+
if (e.key === 'Enter') {
|
|
2779
|
+
e.preventDefault();
|
|
2780
|
+
emailIn?.focus();
|
|
2781
|
+
}
|
|
2782
|
+
});
|
|
2783
|
+
}
|
|
2784
|
+
if (emailIn) {
|
|
2785
|
+
emailIn.addEventListener('input', (e) => {
|
|
2786
|
+
currentAuthEmail = e.target.value;
|
|
2787
|
+
});
|
|
2788
|
+
emailIn.addEventListener('keydown', (e) => {
|
|
2789
|
+
if (e.key === 'Enter') {
|
|
2790
|
+
e.preventDefault();
|
|
2791
|
+
handleAuthSignupSubmit();
|
|
2792
|
+
}
|
|
2793
|
+
});
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
const otpIn = document.getElementById('authOtpInput');
|
|
2797
|
+
if (otpIn) {
|
|
2798
|
+
otpIn.addEventListener('input', (e) => {
|
|
2799
|
+
const val = e.target.value.replace(/\D/g, '');
|
|
2800
|
+
e.target.value = val;
|
|
2801
|
+
if (val.length === 6) {
|
|
2802
|
+
handleAuthVerifyOtpSubmit();
|
|
2803
|
+
}
|
|
2804
|
+
});
|
|
2805
|
+
otpIn.addEventListener('keydown', (e) => {
|
|
2806
|
+
if (e.key === 'Enter') {
|
|
2807
|
+
e.preventDefault();
|
|
2808
|
+
handleAuthVerifyOtpSubmit();
|
|
2809
|
+
}
|
|
2810
|
+
});
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
// Boot checkout - restore preserved 1-week identity if available
|
|
2814
|
+
const bootPreserved = getPreservedIdentity();
|
|
2815
|
+
if (bootPreserved && bootPreserved.customer) {
|
|
2816
|
+
activeCustomer = bootPreserved.customer;
|
|
2817
|
+
activeToken = bootPreserved.token || localStorage.getItem('ypg_customer_token') || null;
|
|
2818
|
+
updateCustomerDisplay(activeCustomer);
|
|
2819
|
+
} else {
|
|
2820
|
+
activeCustomer = null;
|
|
2821
|
+
activeToken = null;
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
try {
|
|
2825
|
+
const initEl = document.getElementById('ypgInitialState');
|
|
2826
|
+
if (initEl && initEl.textContent && initEl.textContent.trim()) {
|
|
2827
|
+
const parsed = JSON.parse(initEl.textContent);
|
|
2828
|
+
if (parsed && (parsed.amount || parsed.branding || parsed.merchantName)) {
|
|
2829
|
+
renderState(parsed);
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
} catch (e) { }
|
|
2833
|
+
|
|
2834
|
+
loadState();
|
|
2835
|
+
connectSse();
|
|
2836
|
+
</script>
|
|
2837
|
+
</body>
|
|
2838
|
+
|
|
2839
|
+
</html>
|