@mmpay/browser 0.0.0-stage → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.vscode/settings.json +33 -0
- package/README.md +489 -2
- package/dist/cjs/index.js +1770 -0
- package/dist/esm/api.d.ts +26 -0
- package/dist/esm/deprecated/api.d.ts +12 -0
- package/dist/esm/deprecated/showPaymentModal.d.ts +6 -0
- package/dist/esm/functions/api.d.ts +12 -0
- package/dist/esm/functions/showPaymentModal.d.ts +2 -0
- package/dist/esm/i18n.d.ts +13 -0
- package/dist/esm/index.d.ts +65 -0
- package/dist/esm/index.js +1768 -0
- package/dist/esm/logo.d.ts +5 -0
- package/dist/esm/stream.d.ts +46 -0
- package/dist/esm/styles.d.ts +5 -0
- package/dist/esm/templates.d.ts +6 -0
- package/dist/esm/types.d.ts +101 -0
- package/dist/esm/ui.d.ts +85 -0
- package/dist/mmpay-sdk.js +1776 -0
- package/package.json +45 -4
- package/rollup.config.ts +33 -0
- package/src/api.ts +121 -0
- package/src/i18n.ts +112 -0
- package/src/index.ts +490 -0
- package/src/logo.ts +5 -0
- package/src/stream.ts +214 -0
- package/src/styles.ts +206 -0
- package/src/types.ts +117 -0
- package/src/ui.ts +715 -0
- package/test/index.html +476 -0
- package/tsconfig.json +15 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
import {MMPayAPI} from './api';
|
|
2
|
+
import {PaymentStatusWatcher} from './stream';
|
|
3
|
+
import {
|
|
4
|
+
ICreatePaymentRequestParams,
|
|
5
|
+
ICreatePaymentResponse,
|
|
6
|
+
IModalEventResult,
|
|
7
|
+
IPaymentShowRequestParams,
|
|
8
|
+
IPollingRequest,
|
|
9
|
+
IPollingResponse,
|
|
10
|
+
MMPayLanguage,
|
|
11
|
+
MMPayTerminalStatus,
|
|
12
|
+
MMPayTransport,
|
|
13
|
+
SDKOptions
|
|
14
|
+
} from './types';
|
|
15
|
+
import {MMPayUI, MMPayUIHandlers} from './ui';
|
|
16
|
+
|
|
17
|
+
const MIN_LOADING_MS = 400;
|
|
18
|
+
|
|
19
|
+
export class MMPaySDK {
|
|
20
|
+
private POLL_INTERVAL_MS: number;
|
|
21
|
+
private TIMEOUT_SECONDS: number = 900;
|
|
22
|
+
private readonly CACHE_KEY: string = 'mmpay_pending_tx';
|
|
23
|
+
private environment: 'sandbox' | 'production';
|
|
24
|
+
private transport: MMPayTransport;
|
|
25
|
+
protected merchantName: string;
|
|
26
|
+
private onCompleteCallback: ((result: IModalEventResult) => void) | null = null;
|
|
27
|
+
private statusWatcher: PaymentStatusWatcher | null = null;
|
|
28
|
+
private countdownIntervalId: number | undefined = undefined;
|
|
29
|
+
private pendingApiResponse: any | null = null;
|
|
30
|
+
private pendingPaymentPayload: any | null = null;
|
|
31
|
+
private flowId: number = 0;
|
|
32
|
+
private settled: boolean = true;
|
|
33
|
+
private cancelling: boolean = false;
|
|
34
|
+
private creating: boolean = false;
|
|
35
|
+
protected api: MMPayAPI;
|
|
36
|
+
protected ui: MMPayUI;
|
|
37
|
+
|
|
38
|
+
constructor(publishableKey: string, options: SDKOptions = {}) {
|
|
39
|
+
if (!publishableKey) {
|
|
40
|
+
throw new Error("A Publishable Key is required to initialize [MMPaySDK].");
|
|
41
|
+
}
|
|
42
|
+
if (publishableKey.includes('pk_test')) {
|
|
43
|
+
this.environment = 'sandbox';
|
|
44
|
+
} else if (publishableKey.includes('pk_live')) {
|
|
45
|
+
this.environment = 'production';
|
|
46
|
+
} else {
|
|
47
|
+
this.environment = options.environment || 'production';
|
|
48
|
+
}
|
|
49
|
+
const baseUrl = options.baseUrl || 'https://bbapi.myanmyanpay.com';
|
|
50
|
+
this.merchantName = options.merchantName || 'MyanMyanPay';
|
|
51
|
+
this.POLL_INTERVAL_MS = options.pollInterval || 5000;
|
|
52
|
+
this.transport = options.transport || 'auto';
|
|
53
|
+
if (typeof options.timeoutSeconds === 'number' && isFinite(options.timeoutSeconds)) {
|
|
54
|
+
// The server expires an unpaid order after 15 minutes, so the countdown
|
|
55
|
+
// can be shortened but not extended.
|
|
56
|
+
this.TIMEOUT_SECONDS = Math.min(900, Math.max(60, Math.floor(options.timeoutSeconds)));
|
|
57
|
+
}
|
|
58
|
+
this.api = new MMPayAPI(baseUrl, this.environment, publishableKey);
|
|
59
|
+
this.ui = new MMPayUI({
|
|
60
|
+
mode: options.design?.mode || 'light',
|
|
61
|
+
color: options.design?.color
|
|
62
|
+
}, options.language);
|
|
63
|
+
if (typeof window !== 'undefined') {
|
|
64
|
+
this._checkAndAutoResume();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Switches the modal language. Works at any time, including while a payment
|
|
70
|
+
* is on screen, and is remembered for the shopper's next visit.
|
|
71
|
+
*/
|
|
72
|
+
public setLanguage(language: MMPayLanguage | 'my'): void {
|
|
73
|
+
this.ui.setLanguage(language);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public getLanguage(): MMPayLanguage {
|
|
77
|
+
return this.ui.getLanguage();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public async showPaymentModal(
|
|
81
|
+
params: ICreatePaymentRequestParams,
|
|
82
|
+
onComplete: (result: IModalEventResult) => void
|
|
83
|
+
): Promise<void> {
|
|
84
|
+
// A second call while an order is still being created (double click) must
|
|
85
|
+
// not create a second order.
|
|
86
|
+
if (this.creating) {
|
|
87
|
+
this.onCompleteCallback = onComplete;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const flowId = this._beginFlow(onComplete);
|
|
91
|
+
this.creating = true;
|
|
92
|
+
this.ui.showLoading({
|
|
93
|
+
merchantName: this.merchantName,
|
|
94
|
+
orderId: params.orderId,
|
|
95
|
+
amount: params.amount
|
|
96
|
+
}, this._getUiHandlers());
|
|
97
|
+
try {
|
|
98
|
+
const startTime = Date.now();
|
|
99
|
+
if (await this._showUnfinishedOrder(flowId, startTime)) return;
|
|
100
|
+
if (flowId !== this.flowId) return;
|
|
101
|
+
const tokenNonce = new Date().getTime().toString() + '_create';
|
|
102
|
+
const tokenResponse = await this.api.createToken({
|
|
103
|
+
amount: params.amount,
|
|
104
|
+
orderId: params.orderId,
|
|
105
|
+
nonce: tokenNonce
|
|
106
|
+
});
|
|
107
|
+
if (flowId !== this.flowId) return;
|
|
108
|
+
this.api.setToken(tokenResponse.token);
|
|
109
|
+
const apiResponse: any = await this.api.createPayment({...params, nonce: tokenNonce});
|
|
110
|
+
await this._holdLoading(startTime);
|
|
111
|
+
if (flowId !== this.flowId) return;
|
|
112
|
+
const actualRefId = apiResponse?.vendorQrRefId || apiResponse?.transactionRefId;
|
|
113
|
+
if (apiResponse && apiResponse.qr && actualRefId) {
|
|
114
|
+
apiResponse.vendorQrRefId = actualRefId;
|
|
115
|
+
this.pendingPaymentPayload = {...params, tokenNonce};
|
|
116
|
+
this.pendingApiResponse = apiResponse;
|
|
117
|
+
const expireAt = Date.now() + (this.TIMEOUT_SECONDS * 1000);
|
|
118
|
+
this._writeCache({
|
|
119
|
+
payload: {amount: apiResponse.amount, orderId: params.orderId, nonce: tokenNonce + '_poll'},
|
|
120
|
+
apiResponse: apiResponse,
|
|
121
|
+
expireAt: expireAt,
|
|
122
|
+
token: this.api.getToken(),
|
|
123
|
+
environment: this.environment
|
|
124
|
+
});
|
|
125
|
+
this._showQr(apiResponse, params.orderId);
|
|
126
|
+
this._startStatusWatch({orderId: params.orderId, nonce: tokenNonce + '_poll'});
|
|
127
|
+
this._startCountdown(params.orderId, expireAt);
|
|
128
|
+
this._triggerEvent({
|
|
129
|
+
created: true,
|
|
130
|
+
orderId: params.orderId,
|
|
131
|
+
vendorQrRefId: actualRefId,
|
|
132
|
+
amount: params.amount
|
|
133
|
+
});
|
|
134
|
+
} else {
|
|
135
|
+
throw new Error("Invalid API Response: Missing QR Data.");
|
|
136
|
+
}
|
|
137
|
+
} catch (error: any) {
|
|
138
|
+
if (flowId !== this.flowId) return;
|
|
139
|
+
this._failFlow(params.orderId, error, params.amount);
|
|
140
|
+
} finally {
|
|
141
|
+
if (flowId === this.flowId) this.creating = false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Spam guard: while the shopper has an order that is not paid, cancelled or
|
|
147
|
+
* expired, a new one is not created. The unfinished order is shown again
|
|
148
|
+
* instead. The server is asked first, so an order that finished in the
|
|
149
|
+
* meantime does not block a new one.
|
|
150
|
+
*/
|
|
151
|
+
private async _showUnfinishedOrder(flowId: number, startTime: number): Promise<boolean> {
|
|
152
|
+
const cached = this._readCache();
|
|
153
|
+
if (!cached) return false;
|
|
154
|
+
if (cached.environment !== this.environment || Date.now() >= cached.expireAt) {
|
|
155
|
+
this._clearCache();
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
this.api.setToken(cached.token);
|
|
159
|
+
try {
|
|
160
|
+
const response = await this.api.pollPayment({orderId: cached.payload.orderId, nonce: cached.payload.nonce});
|
|
161
|
+
await this._holdLoading(startTime);
|
|
162
|
+
if (flowId !== this.flowId) return true;
|
|
163
|
+
if ((response.status || '').toUpperCase() === 'PENDING') {
|
|
164
|
+
this._resumePaymentState(cached.apiResponse, cached.payload, cached.expireAt);
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (flowId !== this.flowId) return true;
|
|
169
|
+
}
|
|
170
|
+
this.api.setToken(null);
|
|
171
|
+
this._clearCache();
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
public async pay(orderId: string, onComplete: (result: IModalEventResult) => void): Promise<void> {
|
|
176
|
+
const flowId = this._beginFlow(onComplete);
|
|
177
|
+
const cached = this._readCache();
|
|
178
|
+
if (cached) {
|
|
179
|
+
if (cached.environment === this.environment && Date.now() < cached.expireAt && cached.payload?.orderId === orderId) {
|
|
180
|
+
this.api.setToken(cached.token);
|
|
181
|
+
this._resumePaymentState(cached.apiResponse, cached.payload, cached.expireAt);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
this._clearCache();
|
|
185
|
+
}
|
|
186
|
+
this.ui.showLoading({merchantName: this.merchantName, orderId}, this._getUiHandlers());
|
|
187
|
+
const showPayload: IPaymentShowRequestParams = {orderId, nonce: new Date().getTime().toString() + '_show'};
|
|
188
|
+
const expireAt = Date.now() + (this.TIMEOUT_SECONDS * 1000);
|
|
189
|
+
try {
|
|
190
|
+
const startTime = Date.now();
|
|
191
|
+
const tokenNonce = new Date().getTime().toString() + '_read';
|
|
192
|
+
const tokenResponse = await this.api.createToken({orderId, nonce: tokenNonce});
|
|
193
|
+
if (flowId !== this.flowId) return;
|
|
194
|
+
this.api.setToken(tokenResponse.token);
|
|
195
|
+
const apiResponse: any = await this.api.showPayment(showPayload);
|
|
196
|
+
await this._holdLoading(startTime);
|
|
197
|
+
if (flowId !== this.flowId) return;
|
|
198
|
+
if (apiResponse) {
|
|
199
|
+
const status = (apiResponse.status || '').toUpperCase();
|
|
200
|
+
const actualRefId = apiResponse.vendorQrRefId || apiResponse.transactionRefId;
|
|
201
|
+
if (status !== 'PENDING') {
|
|
202
|
+
const terminalStatus: MMPayTerminalStatus =
|
|
203
|
+
status === 'SUCCESS' || status === 'CANCELLED' || status === 'EXPIRED' ? status : 'FAILED';
|
|
204
|
+
this._finish(terminalStatus, {
|
|
205
|
+
orderId: apiResponse.orderId || orderId,
|
|
206
|
+
amount: apiResponse.amount,
|
|
207
|
+
vendorQrRefId: actualRefId
|
|
208
|
+
}, {
|
|
209
|
+
// Kept as before: a non-pending status the modal has no screen for
|
|
210
|
+
// (REFUNDED) shows the failed screen without reporting `failed`.
|
|
211
|
+
failed: status === 'FAILED'
|
|
212
|
+
});
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (apiResponse.qr && actualRefId) {
|
|
216
|
+
const mappedPaymentResponse: ICreatePaymentResponse = {...apiResponse, vendorQrRefId: actualRefId};
|
|
217
|
+
const mappedPaymentPayload = {amount: apiResponse.amount, orderId: apiResponse.orderId, nonce: showPayload.nonce};
|
|
218
|
+
this._writeCache({
|
|
219
|
+
payload: mappedPaymentPayload,
|
|
220
|
+
apiResponse: mappedPaymentResponse,
|
|
221
|
+
expireAt: expireAt,
|
|
222
|
+
token: this.api.getToken(),
|
|
223
|
+
environment: this.environment
|
|
224
|
+
});
|
|
225
|
+
this._resumePaymentState(mappedPaymentResponse, mappedPaymentPayload, expireAt);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
throw new Error("Invalid API Response: Missing QR Data.");
|
|
230
|
+
} catch (error: any) {
|
|
231
|
+
if (flowId !== this.flowId) return;
|
|
232
|
+
this._failFlow(orderId || 'N/A', error);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Starts a fresh flow. Anything still running from an earlier call is
|
|
238
|
+
* stopped, and its late responses are ignored through `flowId`.
|
|
239
|
+
*/
|
|
240
|
+
private _beginFlow(onComplete: (result: IModalEventResult) => void): number {
|
|
241
|
+
this._stopTimers();
|
|
242
|
+
this.onCompleteCallback = onComplete;
|
|
243
|
+
this.pendingApiResponse = null;
|
|
244
|
+
this.pendingPaymentPayload = null;
|
|
245
|
+
this.settled = false;
|
|
246
|
+
this.cancelling = false;
|
|
247
|
+
this.creating = false;
|
|
248
|
+
return ++this.flowId;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Keeps the loading state up briefly so a fast response does not flicker. */
|
|
252
|
+
private async _holdLoading(startTime: number): Promise<void> {
|
|
253
|
+
const elapsed = Date.now() - startTime;
|
|
254
|
+
if (elapsed < MIN_LOADING_MS) {
|
|
255
|
+
await new Promise(resolve => setTimeout(resolve, MIN_LOADING_MS - elapsed));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private _showQr(apiResponse: any, orderId: string): void {
|
|
260
|
+
this.ui.showQr({
|
|
261
|
+
merchantName: this.merchantName,
|
|
262
|
+
orderId,
|
|
263
|
+
amount: apiResponse.amount,
|
|
264
|
+
vendorQrRefId: apiResponse.vendorQrRefId,
|
|
265
|
+
qr: apiResponse.qr
|
|
266
|
+
}, this._getUiHandlers());
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private _failFlow(orderId: string, error: any, amount?: number): void {
|
|
270
|
+
this._stopTimers();
|
|
271
|
+
this.settled = true;
|
|
272
|
+
this.api.setToken(null);
|
|
273
|
+
this.ui.showResult('FAILED', {
|
|
274
|
+
orderId,
|
|
275
|
+
messageKey: 'message.ERROR',
|
|
276
|
+
detail: error?.message || 'Error occurred.'
|
|
277
|
+
}, this._getUiHandlers());
|
|
278
|
+
this._triggerEvent(amount === undefined ? {failed: true, orderId} : {failed: true, orderId, amount});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Single exit for every finished payment, whichever source reported it
|
|
283
|
+
* first (socket, polling or the countdown), so the result screen and the
|
|
284
|
+
* merchant callback fire exactly once.
|
|
285
|
+
*/
|
|
286
|
+
private _finish(
|
|
287
|
+
status: MMPayTerminalStatus,
|
|
288
|
+
info: {orderId: string; amount?: number; vendorQrRefId?: string},
|
|
289
|
+
overrides: {failed?: boolean; messageKey?: string} = {}
|
|
290
|
+
): void {
|
|
291
|
+
if (this.settled) return;
|
|
292
|
+
this.settled = true;
|
|
293
|
+
this._stopTimers();
|
|
294
|
+
this._clearCache();
|
|
295
|
+
this.api.setToken(null);
|
|
296
|
+
this.ui.showResult(status, {
|
|
297
|
+
orderId: info.orderId,
|
|
298
|
+
messageKey: overrides.messageKey || 'message.' + status,
|
|
299
|
+
ref: status === 'SUCCESS' ? (info.vendorQrRefId || 'N/A') : undefined
|
|
300
|
+
}, this._getUiHandlers());
|
|
301
|
+
this._triggerEvent({
|
|
302
|
+
success: status === 'SUCCESS',
|
|
303
|
+
failed: overrides.failed !== undefined ? overrides.failed : status === 'FAILED',
|
|
304
|
+
expired: status === 'EXPIRED',
|
|
305
|
+
cancelled: status === 'CANCELLED',
|
|
306
|
+
amount: info.amount,
|
|
307
|
+
orderId: info.orderId,
|
|
308
|
+
vendorQrRefId: info.vendorQrRefId
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
protected _getUiHandlers(): MMPayUIHandlers {
|
|
313
|
+
return {
|
|
314
|
+
onCloseRequest: () => this.ui.showCancelConfirmation(),
|
|
315
|
+
onCancelConfirm: () => {
|
|
316
|
+
this._cancelPendingPayment();
|
|
317
|
+
},
|
|
318
|
+
onDismiss: () => {
|
|
319
|
+
this._clearCache();
|
|
320
|
+
this._cleanup();
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private async _cancelPendingPayment(): Promise<void> {
|
|
326
|
+
if (this.cancelling) return;
|
|
327
|
+
this.cancelling = true;
|
|
328
|
+
const flowId = this.flowId;
|
|
329
|
+
const pending = this.pendingPaymentPayload;
|
|
330
|
+
if (pending && !this.settled) {
|
|
331
|
+
this.ui.setCancelBusy(true);
|
|
332
|
+
try {
|
|
333
|
+
await this.api.cancelPayment({
|
|
334
|
+
orderId: pending.orderId,
|
|
335
|
+
nonce: new Date().getTime().toString() + '_cancel'
|
|
336
|
+
});
|
|
337
|
+
if (flowId === this.flowId && !this.settled) {
|
|
338
|
+
this._triggerEvent({
|
|
339
|
+
cancelled: true,
|
|
340
|
+
orderId: pending.orderId,
|
|
341
|
+
amount: pending.amount
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
} catch (e) { }
|
|
345
|
+
}
|
|
346
|
+
// A result that arrived while the cancel request was in flight is already
|
|
347
|
+
// on screen. Leave it there instead of closing over it.
|
|
348
|
+
if (flowId !== this.flowId || this.settled) return;
|
|
349
|
+
this.settled = true;
|
|
350
|
+
this._clearCache();
|
|
351
|
+
this._cleanup();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
protected _triggerEvent(eventData: IModalEventResult): void {
|
|
355
|
+
if (this.onCompleteCallback) {
|
|
356
|
+
try {
|
|
357
|
+
this.onCompleteCallback(eventData);
|
|
358
|
+
} catch (e) { }
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private _stopTimers(): void {
|
|
363
|
+
if (this.statusWatcher) {
|
|
364
|
+
this.statusWatcher.stop();
|
|
365
|
+
this.statusWatcher = null;
|
|
366
|
+
}
|
|
367
|
+
if (this.countdownIntervalId !== undefined) {
|
|
368
|
+
window.clearInterval(this.countdownIntervalId);
|
|
369
|
+
this.countdownIntervalId = undefined;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
protected _cleanup(): void {
|
|
374
|
+
this._stopTimers();
|
|
375
|
+
this.ui.close();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
protected _clearCache(): void {
|
|
379
|
+
try {
|
|
380
|
+
localStorage.removeItem(this.CACHE_KEY);
|
|
381
|
+
} catch (e) { }
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
private _readCache(): any | null {
|
|
385
|
+
try {
|
|
386
|
+
const cachedData = localStorage.getItem(this.CACHE_KEY);
|
|
387
|
+
if (!cachedData) return null;
|
|
388
|
+
const parsed = JSON.parse(cachedData);
|
|
389
|
+
if (parsed && parsed.payload && parsed.apiResponse && typeof parsed.expireAt === 'number') {
|
|
390
|
+
return parsed;
|
|
391
|
+
}
|
|
392
|
+
} catch (e) { }
|
|
393
|
+
this._clearCache();
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
private _writeCache(data: object): void {
|
|
398
|
+
try {
|
|
399
|
+
localStorage.setItem(this.CACHE_KEY, JSON.stringify(data));
|
|
400
|
+
} catch (e) { }
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
private _checkAndAutoResume(): void {
|
|
404
|
+
const cached = this._readCache();
|
|
405
|
+
if (!cached) return;
|
|
406
|
+
if (cached.environment !== this.environment || Date.now() >= cached.expireAt) {
|
|
407
|
+
this._clearCache();
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const start = () => {
|
|
411
|
+
// The merchant may already have started a payment by the time the page
|
|
412
|
+
// finished loading.
|
|
413
|
+
if (this.flowId !== 0) return;
|
|
414
|
+
this.flowId++;
|
|
415
|
+
this.settled = false;
|
|
416
|
+
this.cancelling = false;
|
|
417
|
+
this.api.setToken(cached.token);
|
|
418
|
+
this._resumePaymentState(cached.apiResponse, cached.payload, cached.expireAt);
|
|
419
|
+
};
|
|
420
|
+
if (document.body) {
|
|
421
|
+
start();
|
|
422
|
+
} else {
|
|
423
|
+
document.addEventListener('DOMContentLoaded', start);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
private _resumePaymentState(apiResponse: any, payload: any, expireAt: number): void {
|
|
428
|
+
this.pendingPaymentPayload = payload;
|
|
429
|
+
this.pendingApiResponse = apiResponse;
|
|
430
|
+
const actualRefId = apiResponse.vendorQrRefId || apiResponse.transactionRefId;
|
|
431
|
+
apiResponse.vendorQrRefId = actualRefId;
|
|
432
|
+
this._showQr(apiResponse, payload.orderId);
|
|
433
|
+
this._startStatusWatch({orderId: payload.orderId, nonce: payload.nonce});
|
|
434
|
+
this._startCountdown(payload.orderId, expireAt);
|
|
435
|
+
this._triggerEvent({
|
|
436
|
+
created: true,
|
|
437
|
+
orderId: payload.orderId,
|
|
438
|
+
amount: payload.amount,
|
|
439
|
+
vendorQrRefId: actualRefId
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
protected _startStatusWatch(payload: IPollingRequest): void {
|
|
444
|
+
if (this.statusWatcher) this.statusWatcher.stop();
|
|
445
|
+
const flowId = this.flowId;
|
|
446
|
+
this.statusWatcher = new PaymentStatusWatcher(this.api, payload, {
|
|
447
|
+
transport: this.transport,
|
|
448
|
+
pollInterval: this.POLL_INTERVAL_MS,
|
|
449
|
+
onStatus: (response: IPollingResponse) => {
|
|
450
|
+
if (flowId !== this.flowId) return;
|
|
451
|
+
const status = (response.status || '').toUpperCase();
|
|
452
|
+
if (status === 'SUCCESS' || status === 'FAILED' || status === 'EXPIRED' || status === 'CANCELLED') {
|
|
453
|
+
this._finish(status, {
|
|
454
|
+
orderId: response.orderId || payload.orderId,
|
|
455
|
+
amount: response.amount || this.pendingPaymentPayload?.amount,
|
|
456
|
+
vendorQrRefId: response.vendorQrRefId || response.transactionRefId
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
},
|
|
460
|
+
onConnectionChange: state => {
|
|
461
|
+
if (flowId === this.flowId) this.ui.setConnectionState(state);
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
this.statusWatcher.start();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
protected _startCountdown(orderId: string, expireAt: number): void {
|
|
468
|
+
if (this.countdownIntervalId !== undefined) window.clearInterval(this.countdownIntervalId);
|
|
469
|
+
const flowId = this.flowId;
|
|
470
|
+
const update = () => {
|
|
471
|
+
if (flowId !== this.flowId) return;
|
|
472
|
+
const remaining = Math.max(0, Math.floor((expireAt - Date.now()) / 1000));
|
|
473
|
+
this.ui.setCountdown(remaining);
|
|
474
|
+
if (remaining <= 0) {
|
|
475
|
+
this._finish('EXPIRED', {
|
|
476
|
+
orderId,
|
|
477
|
+
amount: this.pendingPaymentPayload?.amount
|
|
478
|
+
}, {messageKey: 'message.TIMEOUT'});
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
update();
|
|
482
|
+
if (!this.settled) {
|
|
483
|
+
this.countdownIntervalId = window.setInterval(update, 1000);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (typeof window !== 'undefined') {
|
|
489
|
+
(window as any).MMPaySDK = MMPaySDK;
|
|
490
|
+
}
|
package/src/logo.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MyanMyanPay logo (256px PNG from myanmyanpay.com/mmp-logo.png), embedded so
|
|
3
|
+
* the loading screen shows it instantly instead of waiting on another request.
|
|
4
|
+
*/
|
|
5
|
+
export const MMPAY_LOGO_DATA_URI = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAMAAABrrFhUAAAAHlBMVEX///8AAAADAwNFRUVjY2NTU1MuLi7n5+e9vb2BgYFUuDMdAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAE90lEQVR4nO2d2XbCMAxE5yokhf//4R5atrCEYIckMLoPfeiJVTPIrm3JijBHmCPMEeYIc4Q5whxhjjBHmCPMEeYIc4Q5whxhjjBHmCPMEeYIc4Q5whxhjjBHmCPMEeYIc4Q5whxhjjBHmCPMEeYIc4Q5whxhjjBHmCPMEeYIc4Q5whxhjjBHmCPMEeYIc4Q5whxhjjBHle2jlLcZmlWAqPjzvaY1dmI5AYLomjK6uPzQkxmaV4Cg2aqUbXdpqqsyFMsIEDQ/xd2Wfk4dD7ppDM0sAFH+te3Zni1NZmhOAYKuqtunb67SASTVuIAqBGjquq3mKMBEhlKAEkQp6QHkEFDOAQd249dx3W5oEqwytOAc0IzuQdAOCfCKoZJWbxRgLMMCtC8YWpkAuHsAh98+/MJeFOC5oTUKEIPPvyDAGEMrFCCIpr1Pc9i2jxNglKEVCsDm8a7mp2W0ADFoaHN4ZnUCBBsN0f4/9FyAZ4b+FFifAEEM72p/YrQHjDK0PgHawW7fPvVAgOebjOun1iLA5km/N2MFGGcoBVB6wIEcAsTCh6Kd4xzA+Vj871z6st9td6atEWDI0OICxKkLN8uX8zn1pZ8UCDBkaHEBOISGtqf162W/T5u3WgEeGlpeAPYxzfYYnTT0AHp7V0sB2HvoqYeWApxJAUgPUA6BAzkH4LEO6JFzAPeX8JsaD9iseS9wheFusE8KQHqA/IZAxClX19IDovez8YoL7InNdrttjwKYRYY4HQpubw8EBkJ6XxMbJDimKe2OHRgT1B2VHzBs6FGreQWIc4rvn0/+n440m/u8lh8AIwwtL0Bzs/tzzxDBPkfIOUtsLJkn2H6jB+wepHTdy/IazhV+lBx2hzXlCpeRN0aISZVMDyhBlJJDAPs5oPO+N8iEFz4/8uYolWPg4u5w1N0drvknwEpuj9cZik+sH0C/25MZmlUAav50//Mv1AmyhogwR5gjzBHmqLL96KOwm6Oxdxl6EVW1dv83GOYLoZhqKRyfuRSOyTZDlRWZltsMba23wzHhgUidoSyk1OSpcCwyBJpKz824ABkY4UsiQ7tm/uDoraEFBWjcw+NNJkgE7h7A4bdPtu1ZSIk3pMktX1O0OX6U+RIlLzKKj3maKxBgMyLDdYJCSo8ydRcWIGZNlr7J1V5cgJg3Xf6vZXvO1rcrpLTn+YT5xYWUrm/seAowASpumQKQHqAcAiPqH23eU0hp3XNA9/4iKqsX4N0FFFYvAO4ewP1+pwCkByiHwHRzwJIpMrH8HLDe3WA3iwC9an6G64D4TyypSxDhcz3gqqLnugRo319U9bqm67oEmKWISr+qr6EAzbWhFKAEUUp6ADkE1B+6ZoWUorD+0dcUUop8yUrz4mt24stfs0O+aIm/r+YeJRkizwyt0AOwzxFyzhIbSxZSar/RA3bN/C9dXVWucBmZLs/XpMuXkQKQHsBHD4GurttZSInPvjlK7ZXfi7vDdYWUqm5Pq7zpvuMTFVKKOkPLhMb4j04WXfpvun5Ms6YQwWLB0T0V0fl+04rPUJkhoKrW9jVEvgBhjjBHmCPMEeYIc4Q5whxhjjBHmCPMEeYIc4Q5whxhjjBHmCPMEeYIc4Q5whxhjjBHmCPMEeYIc4Q5whxhjjBHmCPMEeYIc4Q5whxhjjBHmCPMEeYIc4Q5whxhjjBHmCPMEeYIc1IAzPkF8CT7lTTnMu0AAAAASUVORK5CYII=';
|