mmpay-browser-sdk 1.0.6 → 1.0.8
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/README.md +9 -2
- package/dist/cjs/index.d.ts +18 -112
- package/dist/cjs/index.js +90 -114
- package/dist/esm/index.d.ts +18 -112
- package/dist/esm/index.js +90 -114
- package/dist/mmpay-sdk.js +90 -114
- package/dist/mmpay-sdk.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +125 -142
- package/test/home.html +3 -2
package/dist/mmpay-sdk.js
CHANGED
|
@@ -5,18 +5,15 @@
|
|
|
5
5
|
})(this, (function (exports) { 'use strict';
|
|
6
6
|
|
|
7
7
|
class MMPaySDK {
|
|
8
|
-
/**
|
|
9
|
-
* constructor
|
|
10
|
-
* @param publishableKey
|
|
11
|
-
* @param options
|
|
12
|
-
*/
|
|
13
8
|
constructor(publishableKey, options = {}) {
|
|
14
9
|
this.pollIntervalId = undefined;
|
|
10
|
+
this.countdownIntervalId = undefined;
|
|
15
11
|
this.onCompleteCallback = null;
|
|
16
12
|
this.overlayElement = null;
|
|
17
13
|
this.pendingApiResponse = null;
|
|
18
14
|
this.pendingPaymentPayload = null;
|
|
19
15
|
this.QR_SIZE = 290;
|
|
16
|
+
this.TIMEOUT_SECONDS = 300;
|
|
20
17
|
if (!publishableKey) {
|
|
21
18
|
throw new Error("A Publishable Key is required to initialize [MMPaySDK].");
|
|
22
19
|
}
|
|
@@ -26,12 +23,6 @@
|
|
|
26
23
|
this.merchantName = options.merchantName || 'Your Merchant';
|
|
27
24
|
this.POLL_INTERVAL_MS = options.pollInterval || 5000;
|
|
28
25
|
}
|
|
29
|
-
/**
|
|
30
|
-
* _callApi
|
|
31
|
-
* @param endpoint
|
|
32
|
-
* @param data
|
|
33
|
-
* @returns
|
|
34
|
-
*/
|
|
35
26
|
async _callApi(endpoint, data = {}) {
|
|
36
27
|
let config = {
|
|
37
28
|
'Content-Type': 'application/json',
|
|
@@ -55,16 +46,6 @@
|
|
|
55
46
|
}
|
|
56
47
|
return response.json();
|
|
57
48
|
}
|
|
58
|
-
/**
|
|
59
|
-
* _callApiTokenRequest
|
|
60
|
-
* @param {ICreateTokenRequestParams} payload
|
|
61
|
-
* @param {number} payload.amount
|
|
62
|
-
* @param {string} payload.currency
|
|
63
|
-
* @param {string} payload.orderId
|
|
64
|
-
* @param {string} payload.nonce
|
|
65
|
-
* @param {string} payload.callbackUrl
|
|
66
|
-
* @returns {Promise<ICreateTokenResponse>}
|
|
67
|
-
*/
|
|
68
49
|
async _callApiTokenRequest(payload) {
|
|
69
50
|
try {
|
|
70
51
|
const endpoint = this.environment === 'sandbox'
|
|
@@ -77,16 +58,6 @@
|
|
|
77
58
|
throw error;
|
|
78
59
|
}
|
|
79
60
|
}
|
|
80
|
-
/**
|
|
81
|
-
* _callApiPaymentRequest
|
|
82
|
-
* @param {ICreatePaymentRequestParams} payload
|
|
83
|
-
* @param {number} payload.amount
|
|
84
|
-
* @param {string} payload.currency
|
|
85
|
-
* @param {string} payload.orderId
|
|
86
|
-
* @param {string} payload.nonce
|
|
87
|
-
* @param {string} payload.callbackUrl
|
|
88
|
-
* @returns {Promise<ICreatePaymentResponse>}
|
|
89
|
-
*/
|
|
90
61
|
async _callApiPaymentRequest(payload) {
|
|
91
62
|
try {
|
|
92
63
|
const endpoint = this.environment === 'sandbox'
|
|
@@ -99,61 +70,55 @@
|
|
|
99
70
|
throw error;
|
|
100
71
|
}
|
|
101
72
|
}
|
|
102
|
-
/**
|
|
103
|
-
* createPayment
|
|
104
|
-
* @param {ICorePayParams} params
|
|
105
|
-
* @param {number} params.amount
|
|
106
|
-
* @param {string} params.orderId
|
|
107
|
-
* @param {string} params.callbackUrl
|
|
108
|
-
* @returns {Promise<ICreatePaymentResponse>}
|
|
109
|
-
*/
|
|
110
73
|
async createPayment(params) {
|
|
111
|
-
const
|
|
74
|
+
const tokenPayload = {
|
|
75
|
+
amount: params.amount,
|
|
76
|
+
orderId: params.orderId,
|
|
77
|
+
nonce: new Date().getTime().toString() + '_mmp'
|
|
78
|
+
};
|
|
79
|
+
const paymentPayload = {
|
|
112
80
|
amount: params.amount,
|
|
113
81
|
orderId: params.orderId,
|
|
114
82
|
callbackUrl: params.callbackUrl,
|
|
115
|
-
|
|
83
|
+
customMessage: params.customMessage,
|
|
116
84
|
nonce: new Date().getTime().toString() + '_mmp'
|
|
117
85
|
};
|
|
118
86
|
try {
|
|
119
|
-
const tokenResponse = await this._callApiTokenRequest(
|
|
87
|
+
const tokenResponse = await this._callApiTokenRequest(tokenPayload);
|
|
120
88
|
this.tokenKey = tokenResponse.token;
|
|
121
|
-
|
|
122
|
-
return apiResponse;
|
|
89
|
+
return await this._callApiPaymentRequest(paymentPayload);
|
|
123
90
|
}
|
|
124
91
|
catch (error) {
|
|
125
92
|
console.error("Payment request failed:", error);
|
|
126
93
|
throw error;
|
|
127
94
|
}
|
|
128
95
|
}
|
|
129
|
-
/**
|
|
130
|
-
* showPaymentModal
|
|
131
|
-
* @param {ICorePayParams} params
|
|
132
|
-
* @param {number} params.amount
|
|
133
|
-
* @param {string} params.orderId
|
|
134
|
-
* @param {string} params.callbackUrl
|
|
135
|
-
* @param {Function} onComplete
|
|
136
|
-
*/
|
|
137
96
|
async showPaymentModal(params, onComplete) {
|
|
138
97
|
const initialContent = `<div class="mmpay-overlay-content"><div style="text-align: center; color: #fff;">ငွေပေးချေမှု စတင်နေသည်...</div></div>`;
|
|
139
98
|
this._createAndRenderModal(initialContent, false);
|
|
140
99
|
this.onCompleteCallback = onComplete;
|
|
141
|
-
const
|
|
100
|
+
const tokenPayload = {
|
|
101
|
+
amount: params.amount,
|
|
102
|
+
orderId: params.orderId,
|
|
103
|
+
nonce: new Date().getTime().toString() + '_mmp'
|
|
104
|
+
};
|
|
105
|
+
const paymentPayload = {
|
|
142
106
|
amount: params.amount,
|
|
143
107
|
orderId: params.orderId,
|
|
144
108
|
callbackUrl: params.callbackUrl,
|
|
145
|
-
|
|
109
|
+
customMessage: params.customMessage,
|
|
146
110
|
nonce: new Date().getTime().toString() + '_mmp'
|
|
147
111
|
};
|
|
148
112
|
try {
|
|
149
|
-
const tokenResponse = await this._callApiTokenRequest(
|
|
113
|
+
const tokenResponse = await this._callApiTokenRequest(tokenPayload);
|
|
150
114
|
this.tokenKey = tokenResponse.token;
|
|
151
|
-
const apiResponse = await this._callApiPaymentRequest(
|
|
115
|
+
const apiResponse = await this._callApiPaymentRequest(paymentPayload);
|
|
152
116
|
if (apiResponse && apiResponse.qr && apiResponse.transactionRefId) {
|
|
153
117
|
this.pendingApiResponse = apiResponse;
|
|
154
|
-
this.pendingPaymentPayload =
|
|
155
|
-
this._renderQrModalContent(apiResponse,
|
|
156
|
-
this._startPolling(
|
|
118
|
+
this.pendingPaymentPayload = paymentPayload;
|
|
119
|
+
this._renderQrModalContent(apiResponse, paymentPayload, this.merchantName);
|
|
120
|
+
this._startPolling(paymentPayload, onComplete);
|
|
121
|
+
this._startCountdown(paymentPayload.orderId);
|
|
157
122
|
}
|
|
158
123
|
else {
|
|
159
124
|
this._showTerminalMessage(apiResponse.orderId || 'N/A', 'FAILED', 'ငွေပေးချေမှု စတင်ရန် မအောင်မြင်ပါ။ QR ဒေတာ မရရှိပါ။');
|
|
@@ -161,15 +126,9 @@
|
|
|
161
126
|
}
|
|
162
127
|
catch (error) {
|
|
163
128
|
this.tokenKey = null;
|
|
164
|
-
this._showTerminalMessage(
|
|
129
|
+
this._showTerminalMessage(paymentPayload.orderId || 'N/A', 'FAILED', 'ငွေပေးချေမှု စတင်စဉ် အမှားအယွင်း ဖြစ်ပွားသည်။ ကွန်ဆိုးလ်တွင် ကြည့်ပါ။');
|
|
165
130
|
}
|
|
166
131
|
}
|
|
167
|
-
/**
|
|
168
|
-
* _createAndRenderModal
|
|
169
|
-
* @param {string} contentHtml
|
|
170
|
-
* @param {boolean} isTerminal
|
|
171
|
-
* @returns
|
|
172
|
-
*/
|
|
173
132
|
_createAndRenderModal(contentHtml, isTerminal = false) {
|
|
174
133
|
this._cleanupModal(false);
|
|
175
134
|
const overlay = document.createElement('div');
|
|
@@ -203,7 +162,6 @@
|
|
|
203
162
|
width: 100%;
|
|
204
163
|
padding: 20px 0;
|
|
205
164
|
}
|
|
206
|
-
/* Card Base Styles */
|
|
207
165
|
.mmpay-card {
|
|
208
166
|
background: #ffffff;
|
|
209
167
|
border-radius: 16px;
|
|
@@ -275,15 +233,9 @@
|
|
|
275
233
|
};
|
|
276
234
|
window.MMPayReRenderModal = () => this._reRenderPendingModalInstance();
|
|
277
235
|
overlay.innerHTML += `<div class="mmpay-overlay-content">${contentHtml}</div>`;
|
|
278
|
-
document.body.style.overflow = 'hidden';
|
|
236
|
+
document.body.style.overflow = 'hidden';
|
|
279
237
|
return overlay;
|
|
280
238
|
}
|
|
281
|
-
/**
|
|
282
|
-
* _renderQrModalContent
|
|
283
|
-
* @param {ICreatePaymentResponse} apiResponse
|
|
284
|
-
* @param {CreatePaymentRequest} payload
|
|
285
|
-
* @param {string} merchantName
|
|
286
|
-
*/
|
|
287
239
|
_renderQrModalContent(apiResponse, payload, merchantName) {
|
|
288
240
|
const qrData = apiResponse.qr;
|
|
289
241
|
const amountDisplay = `${apiResponse.amount.toFixed(2)} MMK`;
|
|
@@ -310,7 +262,7 @@
|
|
|
310
262
|
<style>
|
|
311
263
|
.mmpay-card { max-width: 350px; padding: 16px; }
|
|
312
264
|
.mmpay-header { color: #1f2937; font-size: 1rem; font-weight: bold; margin-bottom: 8px; }
|
|
313
|
-
.mmpay-qr-container { padding: 0; margin: 10px auto; display: inline-block; line-height: 0; width: 300px; height: 300px; }
|
|
265
|
+
.mmpay-qr-container { padding: 0; margin: 5px auto 10px auto; display: inline-block; line-height: 0; width: 300px; height: 300px; }
|
|
314
266
|
#${qrCanvasId} { display: block; background: white; border-radius: 8px; width: 100%; height: 100%; }
|
|
315
267
|
.mmpay-amount { font-size: 1.2rem; font-weight: 800; color: #1f2937; margin: 0; }
|
|
316
268
|
.mmpay-separator { border-top: 1px solid #f3f4f6; margin: 12px 0; }
|
|
@@ -319,10 +271,28 @@
|
|
|
319
271
|
.mmpay-detail span { text-align: left; }
|
|
320
272
|
.mmpay-secure-text { color: #757575; border-radius: 9999px; font-size: 0.8rem; font-weight: 600; display: inline-flex; align-items: center; justify-content: center; }
|
|
321
273
|
.mmpay-warning { font-size: 0.75rem; color: #9ca3af; font-weight: 500; margin-top: 12px; line-height: 1.5; }
|
|
274
|
+
|
|
275
|
+
.mmpay-timer-badge {
|
|
276
|
+
background-color: #fef2f2;
|
|
277
|
+
color: #b91c1c;
|
|
278
|
+
padding: 4px 10px;
|
|
279
|
+
border-radius: 12px;
|
|
280
|
+
font-weight: 700;
|
|
281
|
+
font-size: 0.85rem;
|
|
282
|
+
display: inline-flex;
|
|
283
|
+
align-items: center;
|
|
284
|
+
gap: 5px;
|
|
285
|
+
margin: 8px 0;
|
|
286
|
+
border: 1px solid #fee2e2;
|
|
287
|
+
}
|
|
288
|
+
.mmpay-timer-icon {
|
|
289
|
+
width: 14px;
|
|
290
|
+
height: 14px;
|
|
291
|
+
fill: currentColor;
|
|
292
|
+
}
|
|
322
293
|
</style>
|
|
323
294
|
|
|
324
295
|
<div class="mmpay-card">
|
|
325
|
-
<!-- Close Button - Triggers Confirmation Modal -->
|
|
326
296
|
<button class="mmpay-close-btn" onclick="MMPayCloseModal(false)">
|
|
327
297
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
|
|
328
298
|
<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"/>
|
|
@@ -337,6 +307,14 @@
|
|
|
337
307
|
${merchantName} သို့ပေးချေပါ
|
|
338
308
|
</div>
|
|
339
309
|
|
|
310
|
+
<div class="mmpay-timer-badge" id="mmpay-timer-badge">
|
|
311
|
+
<svg class="mmpay-timer-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
|
312
|
+
<path d="M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 8.71V3.5z"/>
|
|
313
|
+
<path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm7-8A7 7 0 1 1 1 8a7 7 0 0 1 14 0z"/>
|
|
314
|
+
</svg>
|
|
315
|
+
<span id="mmpay-countdown-text">05:00</span>
|
|
316
|
+
</div>
|
|
317
|
+
|
|
340
318
|
<div class="mmpay-amount">${amountDisplay}</div>
|
|
341
319
|
|
|
342
320
|
<div class="mmpay-qr-container">
|
|
@@ -369,17 +347,11 @@
|
|
|
369
347
|
this._createAndRenderModal(qrContentHtml, false);
|
|
370
348
|
this._injectQrScript(qrData, qrCanvasId);
|
|
371
349
|
}
|
|
372
|
-
/**
|
|
373
|
-
* _showTerminalMessage
|
|
374
|
-
* @param {string} orderId
|
|
375
|
-
* @param {string} status
|
|
376
|
-
* @param {string} message
|
|
377
|
-
*/
|
|
378
350
|
_showTerminalMessage(orderId, status, message) {
|
|
379
351
|
this._cleanupModal(true);
|
|
380
|
-
const successColor = '#10b981';
|
|
381
|
-
const failureColor = '#ef4444';
|
|
382
|
-
const expiredColor = '#f59e0b';
|
|
352
|
+
const successColor = '#10b981';
|
|
353
|
+
const failureColor = '#ef4444';
|
|
354
|
+
const expiredColor = '#f59e0b';
|
|
383
355
|
let color;
|
|
384
356
|
let iconSvg;
|
|
385
357
|
let statusText;
|
|
@@ -391,7 +363,6 @@
|
|
|
391
363
|
</svg>`;
|
|
392
364
|
}
|
|
393
365
|
else {
|
|
394
|
-
// Shared icon for FAILED and EXPIRED (X mark)
|
|
395
366
|
color = status === 'FAILED' ? failureColor : expiredColor;
|
|
396
367
|
statusText = status === 'FAILED' ? 'မအောင်မြင်' : 'သက်တမ်းကုန်';
|
|
397
368
|
iconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="${color}" viewBox="0 0 16 16">
|
|
@@ -415,16 +386,17 @@
|
|
|
415
386
|
</button>
|
|
416
387
|
</div>
|
|
417
388
|
`;
|
|
418
|
-
this._createAndRenderModal(content, true);
|
|
389
|
+
this._createAndRenderModal(content, true);
|
|
419
390
|
}
|
|
420
|
-
/**
|
|
421
|
-
* _showCancelConfirmationModal
|
|
422
|
-
*/
|
|
423
391
|
_showCancelConfirmationModal() {
|
|
424
392
|
if (this.pollIntervalId !== undefined) {
|
|
425
393
|
window.clearInterval(this.pollIntervalId);
|
|
426
394
|
this.pollIntervalId = undefined;
|
|
427
395
|
}
|
|
396
|
+
if (this.countdownIntervalId !== undefined) {
|
|
397
|
+
window.clearInterval(this.countdownIntervalId);
|
|
398
|
+
this.countdownIntervalId = undefined;
|
|
399
|
+
}
|
|
428
400
|
this._cleanupModal(false);
|
|
429
401
|
const content = `
|
|
430
402
|
<div class="mmpay-card mmpay-terminal-card" style="
|
|
@@ -446,11 +418,8 @@
|
|
|
446
418
|
</div>
|
|
447
419
|
</div>
|
|
448
420
|
`;
|
|
449
|
-
this._createAndRenderModal(content, false);
|
|
421
|
+
this._createAndRenderModal(content, false);
|
|
450
422
|
}
|
|
451
|
-
/**
|
|
452
|
-
* _reRenderPendingModalInstance
|
|
453
|
-
*/
|
|
454
423
|
_reRenderPendingModalInstance() {
|
|
455
424
|
if (this.pendingApiResponse && this.pendingPaymentPayload && this.onCompleteCallback) {
|
|
456
425
|
this._cleanupModal(true);
|
|
@@ -460,15 +429,15 @@
|
|
|
460
429
|
this._cleanupModal(true);
|
|
461
430
|
}
|
|
462
431
|
}
|
|
463
|
-
/**
|
|
464
|
-
* Cleans up the modal and stops polling.
|
|
465
|
-
* @param {boolean} restoreBodyScroll
|
|
466
|
-
*/
|
|
467
432
|
_cleanupModal(restoreBodyScroll) {
|
|
468
433
|
if (this.pollIntervalId !== undefined) {
|
|
469
434
|
window.clearInterval(this.pollIntervalId);
|
|
470
435
|
this.pollIntervalId = undefined;
|
|
471
436
|
}
|
|
437
|
+
if (this.countdownIntervalId !== undefined) {
|
|
438
|
+
window.clearInterval(this.countdownIntervalId);
|
|
439
|
+
this.countdownIntervalId = undefined;
|
|
440
|
+
}
|
|
472
441
|
if (this.overlayElement && this.overlayElement.parentNode) {
|
|
473
442
|
this.overlayElement.parentNode.removeChild(this.overlayElement);
|
|
474
443
|
this.overlayElement = null;
|
|
@@ -479,11 +448,6 @@
|
|
|
479
448
|
delete window.MMPayCloseModal;
|
|
480
449
|
delete window.MMPayReRenderModal;
|
|
481
450
|
}
|
|
482
|
-
/**
|
|
483
|
-
* _injectQrScript
|
|
484
|
-
* @param {string} qrData
|
|
485
|
-
* @param {string} qrCanvasId
|
|
486
|
-
*/
|
|
487
451
|
_injectQrScript(qrData, qrCanvasId) {
|
|
488
452
|
const script = document.createElement('script');
|
|
489
453
|
script.src = "https://cdn.jsdelivr.net/npm/qrious@4.0.2/dist/qrious.min.js";
|
|
@@ -506,16 +470,6 @@
|
|
|
506
470
|
};
|
|
507
471
|
document.head.appendChild(script);
|
|
508
472
|
}
|
|
509
|
-
/**
|
|
510
|
-
* _startPolling
|
|
511
|
-
* @param {IPollingRequest} payload
|
|
512
|
-
* @param {number} payload.amount
|
|
513
|
-
* @param {string} payload.currency
|
|
514
|
-
* @param {string} payload.orderId
|
|
515
|
-
* @param {string} payload.nonce
|
|
516
|
-
* @param {string} payload.callbackUrl
|
|
517
|
-
* @param {Function} onComplete
|
|
518
|
-
*/
|
|
519
473
|
async _startPolling(payload, onComplete) {
|
|
520
474
|
if (this.pollIntervalId !== undefined) {
|
|
521
475
|
window.clearInterval(this.pollIntervalId);
|
|
@@ -549,8 +503,30 @@
|
|
|
549
503
|
checkStatus();
|
|
550
504
|
this.pollIntervalId = window.setInterval(checkStatus, this.POLL_INTERVAL_MS);
|
|
551
505
|
}
|
|
506
|
+
_startCountdown(orderId) {
|
|
507
|
+
if (this.countdownIntervalId !== undefined) {
|
|
508
|
+
window.clearInterval(this.countdownIntervalId);
|
|
509
|
+
}
|
|
510
|
+
let remaining = this.TIMEOUT_SECONDS;
|
|
511
|
+
const timerElement = document.getElementById('mmpay-countdown-text');
|
|
512
|
+
const updateDisplay = () => {
|
|
513
|
+
if (!timerElement)
|
|
514
|
+
return;
|
|
515
|
+
const minutes = Math.floor(remaining / 60);
|
|
516
|
+
const seconds = remaining % 60;
|
|
517
|
+
timerElement.innerText = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
|
518
|
+
};
|
|
519
|
+
this.countdownIntervalId = window.setInterval(() => {
|
|
520
|
+
remaining--;
|
|
521
|
+
updateDisplay();
|
|
522
|
+
if (remaining <= 0) {
|
|
523
|
+
window.clearInterval(this.countdownIntervalId);
|
|
524
|
+
this.countdownIntervalId = undefined;
|
|
525
|
+
this._showTerminalMessage(orderId, 'EXPIRED', 'သတ်မှတ်ချိန်ကုန်သွားပါပြီ။');
|
|
526
|
+
}
|
|
527
|
+
}, 1000);
|
|
528
|
+
}
|
|
552
529
|
}
|
|
553
|
-
// Make the SDK class and its instance methods accessible globally
|
|
554
530
|
window.MMPaySDK = MMPaySDK;
|
|
555
531
|
|
|
556
532
|
exports.MMPaySDK = MMPaySDK;
|
package/dist/mmpay-sdk.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mmpay-sdk.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"mmpay-sdk.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|