mmpay-browser-sdk 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,465 @@
1
+ export class MMPaySDK {
2
+ constructor(publishableKey, options = {}) {
3
+ this.pollIntervalId = undefined;
4
+ this.onCompleteCallback = null;
5
+ this.overlayElement = null;
6
+ // Properties to store pending data for re-rendering after cancel attempt
7
+ this.pendingApiResponse = null;
8
+ this.pendingPaymentPayload = null;
9
+ this.QR_SIZE = 300;
10
+ if (!publishableKey) {
11
+ throw new Error("A Publishable Key is required to initialize [MMPaySDK].");
12
+ }
13
+ this.publishableKey = publishableKey;
14
+ this.environment = options.environment || 'production';
15
+ this.baseUrl = options.baseUrl || 'https://api.mm-pay.com';
16
+ this.merchantName = options.merchantName || 'Your Merchant';
17
+ this.POLL_INTERVAL_MS = options.pollInterval || 3000;
18
+ }
19
+ /**
20
+ * _callApi
21
+ * @param endpoint
22
+ * @param data
23
+ * @returns
24
+ */
25
+ async _callApi(endpoint, data = {}) {
26
+ const response = await fetch(`${this.baseUrl}${endpoint}`, {
27
+ method: 'POST',
28
+ headers: {
29
+ 'Content-Type': 'application/json',
30
+ 'Authorization': `Bearer ${this.publishableKey}`
31
+ },
32
+ body: JSON.stringify(data)
33
+ });
34
+ if (!response.ok) {
35
+ const errorText = await response.text();
36
+ throw new Error(`API error (${response.status}): ${response.statusText}. Details: ${errorText}`);
37
+ }
38
+ return response.json();
39
+ }
40
+ /**
41
+ * createPaymentRequest
42
+ * @param {PaymentData} payload
43
+ * @returns
44
+ */
45
+ async createPaymentRequest(payload) {
46
+ try {
47
+ const endpoint = this.environment === 'sandbox'
48
+ ? '/xpayments/sandbox-payment-create'
49
+ : '/xpayments/production-payment-create';
50
+ return await this._callApi(endpoint, payload);
51
+ }
52
+ catch (error) {
53
+ console.error("Payment request failed:", error);
54
+ throw error;
55
+ }
56
+ }
57
+ /**
58
+ * _createAndRenderModal
59
+ * @param {string} contentHtml
60
+ * @param isTerminal
61
+ * @returns
62
+ */
63
+ _createAndRenderModal(contentHtml, isTerminal = false) {
64
+ this._cleanupModal(false);
65
+ const overlay = document.createElement('div');
66
+ overlay.id = 'mmpay-full-modal';
67
+ document.body.appendChild(overlay);
68
+ this.overlayElement = overlay;
69
+ const style = document.createElement('style');
70
+ style.innerHTML = `
71
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&family=Padauk:wght@400;700&display=swap');
72
+
73
+ #mmpay-full-modal {
74
+ position: fixed;
75
+ top: 0;
76
+ left: 0;
77
+ width: 100vw;
78
+ height: 100vh;
79
+ background-color: rgba(0, 0, 0, 0.85);
80
+ z-index: 9999;
81
+ display: flex;
82
+ align-items: center;
83
+ justify-content: center;
84
+ transition: opacity 0.3s;
85
+ padding: 15px;
86
+ box-sizing: border-box;
87
+ overflow: auto;
88
+ }
89
+ .mmpay-overlay-content {
90
+ display: flex;
91
+ align-items: center;
92
+ justify-content: center;
93
+ min-height: 100%;
94
+ width: 100%;
95
+ padding: 20px 0;
96
+ }
97
+ /* Card Base Styles */
98
+ .mmpay-card {
99
+ background: #ffffff;
100
+ border-radius: 16px;
101
+ box-shadow: 0 20px 40px -10px rgba(0, 0, 0, 0.4);
102
+ text-align: center;
103
+ font-family: 'Inter', 'Padauk', sans-serif;
104
+ border: 1px solid #f3f4f6;
105
+ animation: fadeInScale 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
106
+ box-sizing: border-box;
107
+ position: relative;
108
+ width: min(90vw, 330px);
109
+ margin: auto;
110
+ }
111
+ @keyframes fadeInScale {
112
+ from { opacity: 0; transform: scale(0.9); }
113
+ to { opacity: 1; transform: scale(1); }
114
+ }
115
+ .mmpay-close-btn {
116
+ position: absolute;
117
+ top: 10px;
118
+ right: 10px;
119
+ background: none;
120
+ border: none;
121
+ cursor: pointer;
122
+ padding: 8px;
123
+ color: #9ca3af;
124
+ border-radius: 50%;
125
+ transition: color 0.2s, background-color 0.2s;
126
+ line-height: 1;
127
+ z-index: 10;
128
+ }
129
+ .mmpay-close-btn:hover {
130
+ color: #4b5563;
131
+ background-color: #f3f4f6;
132
+ }
133
+ .mmpay-button {
134
+ background-color: #4f46e5;
135
+ color: white;
136
+ border: none;
137
+ padding: 10px 20px;
138
+ border-radius: 8px;
139
+ font-size: 0.95rem;
140
+ font-weight: 700;
141
+ cursor: pointer;
142
+ margin-top: 15px;
143
+ transition: background-color 0.2s, box-shadow 0.2s, transform 0.1s;
144
+ box-shadow: 0 5px 15px rgba(79, 70, 229, 0.3);
145
+ width: 100%;
146
+ }
147
+ .mmpay-button:hover {
148
+ background-color: #4338ca;
149
+ box-shadow: 0 8px 18px rgba(67, 56, 202, 0.4);
150
+ transform: translateY(-1px);
151
+ }
152
+ .mmpay-button:active {
153
+ transform: translateY(0);
154
+ background-color: #3f35c7;
155
+ }
156
+ .mmpay-text-myanmar { font-family: 'Padauk', sans-serif; }
157
+ `;
158
+ overlay.appendChild(style);
159
+ window.MMPayCloseModal = (forceClose = false) => {
160
+ if (isTerminal || forceClose) {
161
+ this._cleanupModal(true);
162
+ }
163
+ else {
164
+ this._showCancelConfirmationModal();
165
+ }
166
+ };
167
+ window.MMPayReRenderModal = () => this._reRenderPendingModalInstance();
168
+ overlay.innerHTML += `<div class="mmpay-overlay-content">${contentHtml}</div>`;
169
+ document.body.style.overflow = 'hidden'; // FIX: Prevent body scroll when modal is open
170
+ return overlay;
171
+ }
172
+ /**
173
+ * showPaymentModal
174
+ * @param {PaymentData} payload
175
+ * @param {Function} onComplete
176
+ */
177
+ async showPaymentModal(payload, onComplete) {
178
+ const initialContent = `<div class="mmpay-overlay-content"><div style="text-align: center; color: #fff;">ငွေပေးချေမှု စတင်နေသည်...</div></div>`;
179
+ this._createAndRenderModal(initialContent, false);
180
+ this.onCompleteCallback = onComplete;
181
+ try {
182
+ const apiResponse = await this.createPaymentRequest(payload);
183
+ if (apiResponse && apiResponse.qr && apiResponse.transactionId) {
184
+ this.pendingApiResponse = apiResponse;
185
+ this.pendingPaymentPayload = payload;
186
+ this._renderQrModalContent(apiResponse, payload, this.merchantName);
187
+ this._startPolling(apiResponse._id, onComplete);
188
+ }
189
+ else {
190
+ this._showTerminalMessage(apiResponse.orderId || 'N/A', 'FAILED', 'ငွေပေးချေမှု စတင်ရန် မအောင်မြင်ပါ။ QR ဒေတာ မရရှိပါ။');
191
+ }
192
+ }
193
+ catch (error) {
194
+ // Myanmar translation for "Error during payment initiation. See console."
195
+ this._showTerminalMessage(payload.orderId || 'N/A', 'FAILED', 'ငွေပေးချေမှု စတင်စဉ် အမှားအယွင်း ဖြစ်ပွားသည်။ ကွန်ဆိုးလ်တွင် ကြည့်ပါ။');
196
+ }
197
+ }
198
+ /**
199
+ * _renderQrModalContent
200
+ * @param {CreatePaymentResponse} apiResponse
201
+ * @param {PaymentData} payload
202
+ * @param {string} merchantName
203
+ */
204
+ _renderQrModalContent(apiResponse, payload, merchantName) {
205
+ const qrData = apiResponse.qr;
206
+ const amountDisplay = `${apiResponse.amount.toFixed(2)} ${apiResponse.currency}`;
207
+ const qrCanvasId = 'mmpayQrCanvas';
208
+ const orderId = payload.orderId;
209
+ window.MMPayDownloadQR = function () {
210
+ const canvas = document.getElementById(qrCanvasId);
211
+ if (!canvas)
212
+ return;
213
+ try {
214
+ const dataURL = canvas.toDataURL('image/png');
215
+ const link = document.createElement('a');
216
+ link.href = dataURL;
217
+ link.download = `MMPay-QR-${orderId}.png`;
218
+ document.body.appendChild(link);
219
+ link.click();
220
+ document.body.removeChild(link);
221
+ }
222
+ catch (e) {
223
+ console.error("Failed to download QR image:", e);
224
+ }
225
+ };
226
+ const qrContentHtml = `
227
+ <style>
228
+ .mmpay-card { max-width: 350px; padding: 16px; }
229
+ .mmpay-header { color: #1f2937; font-size: 1rem; font-weight: bold; margin-bottom: 8px; }
230
+ .mmpay-qr-container { padding: 0; margin: 10px auto; display: inline-block; line-height: 0; width: 300px; height: 300px; }
231
+ #${qrCanvasId} { display: block; background: white; border-radius: 8px; width: 100%; height: 100%; }
232
+ .mmpay-amount { font-size: 1.2rem; font-weight: 800; color: #1f2937; margin: 0; }
233
+ .mmpay-separator { border-top: 1px solid #f3f4f6; margin: 12px 0; }
234
+ .mmpay-detail { font-size: 0.8rem; color: #6b7280; margin: 3px 0; display: flex; justify-content: space-between; align-items: center; padding: 0 5px; }
235
+ .mmpay-detail strong { color: #374151; font-weight: 600; text-align: right; }
236
+ .mmpay-detail span { text-align: left; }
237
+ .mmpay-secure-text { color: #757575; border-radius: 9999px; font-size: 0.8rem; font-weight: 600; display: inline-flex; align-items: center; justify-content: center; }
238
+ .mmpay-warning { font-size: 0.75rem; color: #9ca3af; font-weight: 500; margin-top: 12px; line-height: 1.5; }
239
+ </style>
240
+
241
+ <div class="mmpay-card">
242
+ <!-- Close Button - Triggers Confirmation Modal -->
243
+ <button class="mmpay-close-btn" onclick="MMPayCloseModal(false)">
244
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
245
+ <path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
246
+ </svg>
247
+ </button>
248
+
249
+ <div style="padding:0px auto 16px auto">
250
+ <img src="https://upload.wikimedia.org/wikipedia/commons/2/2f/MMQR_Logo.svg" style="width:40px">
251
+ </div>
252
+
253
+ <div class="mmpay-header mmpay-text-myanmar">
254
+ ${merchantName} သို့ပေးချေပါ
255
+ </div>
256
+
257
+ <div class="mmpay-amount">${amountDisplay}</div>
258
+
259
+ <div class="mmpay-qr-container">
260
+ <canvas id="${qrCanvasId}" width="${this.QR_SIZE}" height="${this.QR_SIZE}"></canvas>
261
+ </div>
262
+
263
+ <button class="mmpay-button mmpay-text-myanmar" onclick="MMPayDownloadQR()">
264
+ QR ကုဒ်ကို ဒေါင်းလုဒ်လုပ်ပါ
265
+ </button>
266
+
267
+ <div class="mmpay-separator"></div>
268
+
269
+ <div class="mmpay-detail">
270
+ <span class="mmpay-text-myanmar">မှာယူမှုနံပါတ်:</span> <strong>${apiResponse.orderId}</strong>
271
+ </div>
272
+ <div class="mmpay-detail">
273
+ <span class="mmpay-text-myanmar">ငွေပေးငွေယူနံပါတ်:</span> <strong>${apiResponse.transactionId}</strong>
274
+ </div>
275
+
276
+ <p class="mmpay-warning mmpay-text-myanmar">
277
+ ကျေးဇူးပြု၍ သင့်ဖုန်းဖြင့် ငွေပေးချေမှုကို အပြီးသတ်ပေးပါ။
278
+ </p>
279
+
280
+ <div class="mmpay-secure-text">
281
+ လုံခြုံသော ငွေပေးချေမှု
282
+ </div>
283
+ </div>
284
+ `;
285
+ this._cleanupModal(false);
286
+ this._createAndRenderModal(qrContentHtml, false);
287
+ this._injectQrScript(qrData, qrCanvasId);
288
+ }
289
+ /**
290
+ * _showTerminalMessage
291
+ * @param {string} orderId
292
+ * @param {string} status
293
+ * @param {string} message
294
+ */
295
+ _showTerminalMessage(orderId, status, message) {
296
+ this._cleanupModal(true);
297
+ const successColor = '#10b981'; // Tailwind Green 500
298
+ const failureColor = '#ef4444'; // Tailwind Red 500
299
+ const expiredColor = '#f59e0b'; // Tailwind Amber 500
300
+ let color;
301
+ let iconSvg;
302
+ let statusText;
303
+ if (status === 'SUCCESS') {
304
+ color = successColor;
305
+ statusText = 'အောင်မြင်';
306
+ iconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="${color}" viewBox="0 0 16 16">
307
+ <path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022l-3.473 4.425-2.094-2.094a.75.75 0 0 0-1.06 1.06L6.92 10.865l.764.764a.75.75 0 0 0 1.06 0l4.5-5.5a.75.75 0 0 0-.01-1.05z"/>
308
+ </svg>`;
309
+ }
310
+ else {
311
+ // Shared icon for FAILED and EXPIRED (X mark)
312
+ color = status === 'FAILED' ? failureColor : expiredColor;
313
+ statusText = status === 'FAILED' ? 'မအောင်မြင်' : 'သက်တမ်းကုန်';
314
+ iconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="${color}" viewBox="0 0 16 16">
315
+ <path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.146a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.146z"/>
316
+ </svg>`;
317
+ }
318
+ const content = `
319
+ <div class="mmpay-card mmpay-terminal-card" style="
320
+ background: white; padding: 25px; box-sizing: border-box;
321
+ ">
322
+ <div style="margin-bottom: 20px;">${iconSvg}</div>
323
+
324
+ <h2 style="font-size: 1.5rem; font-weight: 800; color: ${color}; margin-bottom: 10px;">
325
+ ငွေပေးချေမှု ${statusText}
326
+ </h2>
327
+ <p style="color: #4b5563; font-size: 0.95rem; font-weight: 600;">မှာယူမှုနံပါတ်: ${orderId}</p>
328
+ <p style="color: #6b7280; margin-top: 15px; margin-bottom: 25px; font-size: 0.9rem;">${message}</p>
329
+
330
+ <button class="mmpay-button mmpay-text-myanmar" style="background-color: ${color};" onclick="MMPayCloseModal(true)">
331
+ ပိတ်မည်
332
+ </button>
333
+ </div>
334
+ `;
335
+ this._createAndRenderModal(content, true); // Set isTerminal=true so the close button always forces cleanup
336
+ }
337
+ /**
338
+ * _showCancelConfirmationModal
339
+ */
340
+ _showCancelConfirmationModal() {
341
+ if (this.pollIntervalId !== undefined) {
342
+ window.clearInterval(this.pollIntervalId);
343
+ this.pollIntervalId = undefined;
344
+ }
345
+ this._cleanupModal(false);
346
+ const content = `
347
+ <div class="mmpay-card mmpay-terminal-card" style="
348
+ background: white; padding: 25px; box-sizing: border-box;
349
+ ">
350
+ <h2 style="font-size: 1.25rem; font-weight: 800; color: #f59e0b; margin-bottom: 10px;">
351
+ ငွေပေးချေမှုကို ပယ်ဖျက်မည်လား။
352
+ </h2>
353
+ <p style="color: #6b7280; margin-top: 15px; margin-bottom: 25px; font-size: 0.9rem;">
354
+ သင်သည် QR ဖြင့် ငွေပေးချေခြင်း မပြုရသေးကြောင်း သေချာပါသလား။ ပယ်ဖျက်ပြီးပါက ပြန်လည် စတင်ရပါမည်။
355
+ </p>
356
+
357
+ <div style="display: flex; gap: 10px;">
358
+ <button class="mmpay-button mmpay-text-myanmar"
359
+ style="flex-grow: 1; background-color: #f3f4f6; color: #1f2937; box-shadow: none; margin-top: 0;"
360
+ onclick="MMPayCloseModal(true)">
361
+ ပယ်ဖျက်မည်
362
+ </button>
363
+ </div>
364
+ </div>
365
+ `;
366
+ this._createAndRenderModal(content, false); // Set isTerminal=false so the close button calls MMPayCloseModal(true)
367
+ }
368
+ /**
369
+ * _reRenderPendingModalInstance
370
+ */
371
+ _reRenderPendingModalInstance() {
372
+ if (this.pendingApiResponse && this.pendingPaymentPayload && this.onCompleteCallback) {
373
+ this._cleanupModal(true);
374
+ this.showPaymentModal(this.pendingPaymentPayload, this.onCompleteCallback);
375
+ }
376
+ else {
377
+ this._cleanupModal(true);
378
+ }
379
+ }
380
+ /**
381
+ * Cleans up the modal and stops polling.
382
+ * @param restoreBodyScroll
383
+ */
384
+ _cleanupModal(restoreBodyScroll) {
385
+ if (this.pollIntervalId !== undefined) {
386
+ window.clearInterval(this.pollIntervalId);
387
+ this.pollIntervalId = undefined;
388
+ }
389
+ if (this.overlayElement && this.overlayElement.parentNode) {
390
+ this.overlayElement.parentNode.removeChild(this.overlayElement);
391
+ this.overlayElement = null;
392
+ }
393
+ if (restoreBodyScroll) {
394
+ document.body.style.overflow = '';
395
+ }
396
+ delete window.MMPayCloseModal;
397
+ delete window.MMPayReRenderModal;
398
+ }
399
+ /**
400
+ * _injectQrScript
401
+ * @param {string} qrData
402
+ * @param {string} qrCanvasId
403
+ */
404
+ _injectQrScript(qrData, qrCanvasId) {
405
+ const script = document.createElement('script');
406
+ script.src = "https://cdn.jsdelivr.net/npm/qrious@4.0.2/dist/qrious.min.js";
407
+ script.onload = () => {
408
+ setTimeout(() => {
409
+ const canvas = document.getElementById(qrCanvasId);
410
+ if (typeof QRious !== 'undefined' && canvas) {
411
+ new QRious({
412
+ element: canvas,
413
+ value: qrData,
414
+ size: this.QR_SIZE,
415
+ padding: 15,
416
+ level: 'H'
417
+ });
418
+ }
419
+ else {
420
+ console.error('Failed to load QRious or find canvas.');
421
+ }
422
+ }, 10);
423
+ };
424
+ document.head.appendChild(script);
425
+ }
426
+ /**
427
+ * _startPolling
428
+ * @param {string} _id
429
+ * @param {Function} onComplete
430
+ */
431
+ async _startPolling(_id, onComplete) {
432
+ if (this.pollIntervalId !== undefined) {
433
+ window.clearInterval(this.pollIntervalId);
434
+ }
435
+ const checkStatus = async () => {
436
+ try {
437
+ const endpoint = this.environment === 'sandbox'
438
+ ? '/xpayments/sandbox-payment-polling'
439
+ : '/xpayments/production-payment-polling';
440
+ const response = await this._callApi(endpoint, { _id: _id });
441
+ const status = (response.status || '').toUpperCase();
442
+ if (status === 'SUCCESS' || status === 'FAILED' || status === 'EXPIRED') {
443
+ window.clearInterval(this.pollIntervalId);
444
+ this.pollIntervalId = undefined;
445
+ const success = status === 'SUCCESS';
446
+ const message = success ?
447
+ `ငွေပေးချေမှု အောင်မြင်ပါပြီ။ ငွေပေးငွေယူ ရည်ညွှန်းနံပါတ်: ${response.transactionRefId || 'N/A'}` :
448
+ `ငွေပေးချေမှု ${status === 'FAILED' ? 'မအောင်မြင်ပါ' : 'သက်တမ်းကုန်သွားပါပြီ'}.`;
449
+ this._showTerminalMessage(response.orderId || 'N/A', status, message);
450
+ if (onComplete) {
451
+ onComplete({ success: success, transaction: response });
452
+ }
453
+ return;
454
+ }
455
+ }
456
+ catch (error) {
457
+ console.error("Polling error:", error);
458
+ }
459
+ };
460
+ checkStatus();
461
+ this.pollIntervalId = window.setInterval(checkStatus, this.POLL_INTERVAL_MS);
462
+ }
463
+ }
464
+ // Make the SDK class and its instance methods accessible globally
465
+ window.MMPaySDK = MMPaySDK;