@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/stream.ts
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import {MMPayAPI, MMPayApiError} from './api';
|
|
2
|
+
import {IPollingRequest, IPollingResponse, MMPayTransport} from './types';
|
|
3
|
+
|
|
4
|
+
export type ConnectionState = 'live' | 'reconnecting';
|
|
5
|
+
|
|
6
|
+
interface WatcherOptions {
|
|
7
|
+
transport: MMPayTransport;
|
|
8
|
+
pollInterval: number;
|
|
9
|
+
onStatus: (response: IPollingResponse) => void;
|
|
10
|
+
onConnectionChange?: (state: ConnectionState) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const MAX_SOCKET_FAILURES = 3;
|
|
14
|
+
const SILENCE_TIMEOUT_MS = 45000;
|
|
15
|
+
const MAX_RECONNECT_DELAY_MS = 10000;
|
|
16
|
+
const MIN_POLL_INTERVAL_MS = 2000;
|
|
17
|
+
|
|
18
|
+
const CLOSE_SESSION_EXPIRED = 4408;
|
|
19
|
+
const CLOSE_TOO_MANY_CONNECTIONS = 4429;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Watches one payment until the owner calls `stop()`.
|
|
23
|
+
*
|
|
24
|
+
* Status is pushed over a WebSocket. If a socket cannot be established or keeps
|
|
25
|
+
* dropping (old server, blocking proxy), the watcher quietly degrades to the
|
|
26
|
+
* HTTP polling endpoint, so a payment is never left unobserved.
|
|
27
|
+
*/
|
|
28
|
+
export class PaymentStatusWatcher {
|
|
29
|
+
private socket: WebSocket | null = null;
|
|
30
|
+
private stopped = false;
|
|
31
|
+
private connecting = false;
|
|
32
|
+
private usingPolling = false;
|
|
33
|
+
private socketFailures = 0;
|
|
34
|
+
private pollIntervalId: number | undefined = undefined;
|
|
35
|
+
private reconnectTimeoutId: number | undefined = undefined;
|
|
36
|
+
private silenceTimeoutId: number | undefined = undefined;
|
|
37
|
+
private readonly onWake = () => this.wake();
|
|
38
|
+
private readonly onVisibility = () => {
|
|
39
|
+
if (document.visibilityState === 'visible') this.wake();
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
constructor(
|
|
43
|
+
private api: MMPayAPI,
|
|
44
|
+
private payload: IPollingRequest,
|
|
45
|
+
private options: WatcherOptions
|
|
46
|
+
) { }
|
|
47
|
+
|
|
48
|
+
public start(): void {
|
|
49
|
+
// Shoppers leave to pay in their banking app; a backgrounded tab may have
|
|
50
|
+
// its socket frozen, so re-check the moment they come back.
|
|
51
|
+
document.addEventListener('visibilitychange', this.onVisibility);
|
|
52
|
+
window.addEventListener('online', this.onWake);
|
|
53
|
+
window.addEventListener('pageshow', this.onWake);
|
|
54
|
+
this.checkNow();
|
|
55
|
+
if (this.options.transport !== 'polling' && typeof WebSocket !== 'undefined') {
|
|
56
|
+
this.connect();
|
|
57
|
+
} else {
|
|
58
|
+
this.startPolling();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public stop(): void {
|
|
63
|
+
this.stopped = true;
|
|
64
|
+
document.removeEventListener('visibilitychange', this.onVisibility);
|
|
65
|
+
window.removeEventListener('online', this.onWake);
|
|
66
|
+
window.removeEventListener('pageshow', this.onWake);
|
|
67
|
+
window.clearInterval(this.pollIntervalId);
|
|
68
|
+
window.clearTimeout(this.reconnectTimeoutId);
|
|
69
|
+
window.clearTimeout(this.silenceTimeoutId);
|
|
70
|
+
this.closeSocket();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private async checkNow(): Promise<void> {
|
|
74
|
+
try {
|
|
75
|
+
const response = await this.api.pollPayment(this.payload);
|
|
76
|
+
if (!this.stopped) this.options.onStatus(response);
|
|
77
|
+
} catch (error) { }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private wake(): void {
|
|
81
|
+
if (this.stopped) return;
|
|
82
|
+
this.checkNow();
|
|
83
|
+
if (this.usingPolling || this.connecting) return;
|
|
84
|
+
if (!this.socket || this.socket.readyState > 1) {
|
|
85
|
+
window.clearTimeout(this.reconnectTimeoutId);
|
|
86
|
+
this.connect();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private async connect(): Promise<void> {
|
|
91
|
+
if (this.stopped || this.usingPolling || this.connecting) return;
|
|
92
|
+
this.connecting = true;
|
|
93
|
+
let ticket: string;
|
|
94
|
+
try {
|
|
95
|
+
ticket = (await this.api.createStreamTicket(this.payload)).ticket;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
this.connecting = false;
|
|
98
|
+
const status = error instanceof MMPayApiError ? error.status : 0;
|
|
99
|
+
// The server does not offer the stream, or refuses this session.
|
|
100
|
+
if (status >= 400 && status < 500 && status !== 429) {
|
|
101
|
+
this.startPolling();
|
|
102
|
+
} else {
|
|
103
|
+
this.handleSocketFailure();
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
this.connecting = false;
|
|
108
|
+
if (this.stopped) return;
|
|
109
|
+
|
|
110
|
+
let socket: WebSocket;
|
|
111
|
+
try {
|
|
112
|
+
socket = new WebSocket(this.api.getStreamUrl(ticket));
|
|
113
|
+
} catch (error) {
|
|
114
|
+
this.handleSocketFailure();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
this.socket = socket;
|
|
118
|
+
this.armSilenceTimer();
|
|
119
|
+
|
|
120
|
+
socket.onmessage = (event: MessageEvent) => {
|
|
121
|
+
if (socket !== this.socket || this.stopped) return;
|
|
122
|
+
// Only a message proves the stream works end to end.
|
|
123
|
+
this.socketFailures = 0;
|
|
124
|
+
this.armSilenceTimer();
|
|
125
|
+
this.notifyConnection('live');
|
|
126
|
+
const message = this.parseMessage(event.data);
|
|
127
|
+
if (message && message.type === 'status' && this.isOwnStatus(message.data)) {
|
|
128
|
+
this.options.onStatus(message.data);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
socket.onclose = (event: CloseEvent) => {
|
|
132
|
+
if (socket !== this.socket || this.stopped) return;
|
|
133
|
+
this.socket = null;
|
|
134
|
+
window.clearTimeout(this.silenceTimeoutId);
|
|
135
|
+
if (event.code === CLOSE_SESSION_EXPIRED) return;
|
|
136
|
+
if (event.code === CLOSE_TOO_MANY_CONNECTIONS) {
|
|
137
|
+
this.startPolling();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
this.handleSocketFailure();
|
|
141
|
+
};
|
|
142
|
+
socket.onerror = () => { };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private parseMessage(data: unknown): any {
|
|
146
|
+
if (typeof data !== 'string') return null;
|
|
147
|
+
try {
|
|
148
|
+
return JSON.parse(data);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private isOwnStatus(data: any): data is IPollingResponse {
|
|
155
|
+
return !!data && typeof data.status === 'string' && data.orderId === this.payload.orderId;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private handleSocketFailure(): void {
|
|
159
|
+
if (this.stopped || this.usingPolling) return;
|
|
160
|
+
this.socketFailures++;
|
|
161
|
+
this.checkNow();
|
|
162
|
+
if (this.socketFailures >= MAX_SOCKET_FAILURES && this.options.transport !== 'websocket') {
|
|
163
|
+
this.startPolling();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
this.notifyConnection('reconnecting');
|
|
167
|
+
const backoff = Math.min(MAX_RECONNECT_DELAY_MS, 1000 * Math.pow(2, this.socketFailures - 1));
|
|
168
|
+
const delay = backoff / 2 + Math.random() * (backoff / 2);
|
|
169
|
+
window.clearTimeout(this.reconnectTimeoutId);
|
|
170
|
+
this.reconnectTimeoutId = window.setTimeout(() => this.connect(), delay);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private armSilenceTimer(): void {
|
|
174
|
+
window.clearTimeout(this.silenceTimeoutId);
|
|
175
|
+
this.silenceTimeoutId = window.setTimeout(() => {
|
|
176
|
+
// The server sends a heartbeat every 20 seconds. Silence means the
|
|
177
|
+
// connection is dead even if the browser has not noticed yet.
|
|
178
|
+
const socket = this.socket;
|
|
179
|
+
this.socket = null;
|
|
180
|
+
if (socket) this.discard(socket);
|
|
181
|
+
this.handleSocketFailure();
|
|
182
|
+
}, SILENCE_TIMEOUT_MS);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private startPolling(): void {
|
|
186
|
+
if (this.stopped || this.usingPolling) return;
|
|
187
|
+
this.usingPolling = true;
|
|
188
|
+
window.clearTimeout(this.reconnectTimeoutId);
|
|
189
|
+
window.clearTimeout(this.silenceTimeoutId);
|
|
190
|
+
this.closeSocket();
|
|
191
|
+
this.notifyConnection('live');
|
|
192
|
+
const interval = Math.max(MIN_POLL_INTERVAL_MS, this.options.pollInterval);
|
|
193
|
+
this.pollIntervalId = window.setInterval(() => this.checkNow(), interval);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private closeSocket(): void {
|
|
197
|
+
const socket = this.socket;
|
|
198
|
+
this.socket = null;
|
|
199
|
+
if (socket) this.discard(socket);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private discard(socket: WebSocket): void {
|
|
203
|
+
socket.onmessage = null;
|
|
204
|
+
socket.onclose = null;
|
|
205
|
+
socket.onerror = null;
|
|
206
|
+
try {
|
|
207
|
+
socket.close();
|
|
208
|
+
} catch (error) { }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private notifyConnection(state: ConnectionState): void {
|
|
212
|
+
if (this.options.onConnectionChange) this.options.onConnectionChange(state);
|
|
213
|
+
}
|
|
214
|
+
}
|
package/src/styles.ts
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Styles live inside the modal's shadow root, so the merchant page's CSS
|
|
3
|
+
* cannot restyle the payment modal and these rules cannot leak out.
|
|
4
|
+
*/
|
|
5
|
+
export const MMPAY_STYLES = `
|
|
6
|
+
.mmp-root, .mmp-root *, .mmp-root *::before, .mmp-root *::after { box-sizing: border-box; }
|
|
7
|
+
.mmp-root {
|
|
8
|
+
--mmp-card: #ffffff;
|
|
9
|
+
--mmp-text: #1d1d1f;
|
|
10
|
+
--mmp-sub: #6e6e73;
|
|
11
|
+
--mmp-border: rgba(0, 0, 0, 0.08);
|
|
12
|
+
--mmp-chip: #f2f2f5;
|
|
13
|
+
--mmp-chip-active: #ffffff;
|
|
14
|
+
--mmp-chip-hover: #e5e5ea;
|
|
15
|
+
--mmp-panel: #f7f7f9;
|
|
16
|
+
--mmp-skeleton: #e9e9ee;
|
|
17
|
+
--mmp-skeleton-shine: #f6f6f9;
|
|
18
|
+
--mmp-accent: #2c2c2e;
|
|
19
|
+
--mmp-accent-text: #ffffff;
|
|
20
|
+
--mmp-success: #1f9d55;
|
|
21
|
+
--mmp-success-bg: #e6f6ec;
|
|
22
|
+
--mmp-danger: #d93025;
|
|
23
|
+
--mmp-danger-bg: #fce8e6;
|
|
24
|
+
--mmp-warn: #c26a00;
|
|
25
|
+
--mmp-warn-bg: #fff3e0;
|
|
26
|
+
--mmp-card-blur: none;
|
|
27
|
+
position: absolute; top: 0; right: 0; bottom: 0; left: 0;
|
|
28
|
+
display: flex; align-items: center; justify-content: center;
|
|
29
|
+
padding: 16px; overflow-y: auto;
|
|
30
|
+
background: rgba(0, 0, 0, 0.45);
|
|
31
|
+
-webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
|
|
32
|
+
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, Helvetica, Arial, "Padauk", "Myanmar Text", "Noto Sans Myanmar", sans-serif;
|
|
33
|
+
font-size: 16px; line-height: 1.4; font-weight: 400; font-style: normal;
|
|
34
|
+
letter-spacing: normal; text-align: center; text-transform: none; white-space: normal;
|
|
35
|
+
color: var(--mmp-text); direction: ltr;
|
|
36
|
+
-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: 100%;
|
|
37
|
+
animation: mmp-fade 0.2s ease;
|
|
38
|
+
}
|
|
39
|
+
.mmp-root.mmp-dark {
|
|
40
|
+
--mmp-card: #1c1c1e;
|
|
41
|
+
--mmp-text: #f5f5f7;
|
|
42
|
+
--mmp-sub: rgba(235, 235, 245, 0.62);
|
|
43
|
+
--mmp-border: rgba(255, 255, 255, 0.12);
|
|
44
|
+
--mmp-chip: #2c2c2e;
|
|
45
|
+
--mmp-chip-active: #636366;
|
|
46
|
+
--mmp-chip-hover: #3a3a3c;
|
|
47
|
+
--mmp-panel: #2c2c2e;
|
|
48
|
+
--mmp-skeleton: #2c2c2e;
|
|
49
|
+
--mmp-skeleton-shine: #3a3a3c;
|
|
50
|
+
--mmp-accent: #ffffff;
|
|
51
|
+
--mmp-accent-text: #000000;
|
|
52
|
+
--mmp-success: #34c759;
|
|
53
|
+
--mmp-success-bg: rgba(52, 199, 89, 0.18);
|
|
54
|
+
--mmp-danger: #ff6259;
|
|
55
|
+
--mmp-danger-bg: rgba(255, 69, 58, 0.18);
|
|
56
|
+
--mmp-warn: #ffa733;
|
|
57
|
+
--mmp-warn-bg: rgba(255, 149, 0, 0.18);
|
|
58
|
+
}
|
|
59
|
+
.mmp-root.mmp-translucent { --mmp-card: rgba(255, 255, 255, 0.78); --mmp-card-blur: blur(20px); }
|
|
60
|
+
.mmp-root.mmp-dark.mmp-translucent { --mmp-card: rgba(28, 28, 30, 0.78); }
|
|
61
|
+
.mmp-root[data-lang="mm"] { line-height: 1.75; }
|
|
62
|
+
|
|
63
|
+
.mmp-card {
|
|
64
|
+
position: relative; display: flex; flex-direction: column;
|
|
65
|
+
width: 100%; max-width: 372px; max-height: 100%; margin: auto;
|
|
66
|
+
overflow-x: hidden; overflow-y: auto;
|
|
67
|
+
background: var(--mmp-card);
|
|
68
|
+
-webkit-backdrop-filter: var(--mmp-card-blur); backdrop-filter: var(--mmp-card-blur);
|
|
69
|
+
border-radius: 24px;
|
|
70
|
+
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.22), 0 0 0 1px var(--mmp-border);
|
|
71
|
+
outline: none;
|
|
72
|
+
animation: mmp-rise 0.35s cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
73
|
+
}
|
|
74
|
+
.mmp-topbar { display: flex; align-items: center; justify-content: space-between; padding: 14px 14px 0 14px; flex-shrink: 0; }
|
|
75
|
+
.mmp-lang { display: inline-flex; gap: 2px; padding: 3px; background: var(--mmp-chip); border: 1px solid var(--mmp-border); border-radius: 11px; }
|
|
76
|
+
.mmp-lang button {
|
|
77
|
+
margin: 0; padding: 5px 11px; min-width: 40px; border: none; border-radius: 8px; background: transparent;
|
|
78
|
+
font: inherit; font-size: 12px; font-weight: 600; line-height: 1.5; color: var(--mmp-sub); cursor: pointer;
|
|
79
|
+
transition: background 0.2s ease, color 0.2s ease;
|
|
80
|
+
}
|
|
81
|
+
.mmp-lang button[aria-pressed="true"] { background: var(--mmp-chip-active); color: var(--mmp-text); box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12); }
|
|
82
|
+
.mmp-close {
|
|
83
|
+
display: flex; align-items: center; justify-content: center; width: 34px; height: 34px; margin: 0; padding: 0;
|
|
84
|
+
border: none; border-radius: 50%; background: var(--mmp-chip); color: var(--mmp-sub); cursor: pointer;
|
|
85
|
+
transition: background 0.2s ease, color 0.2s ease;
|
|
86
|
+
}
|
|
87
|
+
.mmp-close:hover { background: var(--mmp-chip-hover); color: var(--mmp-text); }
|
|
88
|
+
.mmp-close[hidden] { display: flex; visibility: hidden; }
|
|
89
|
+
.mmp-root button:focus-visible { outline: 2px solid var(--mmp-text); outline-offset: 2px; }
|
|
90
|
+
|
|
91
|
+
.mmp-screen { display: flex; flex-direction: column; align-items: center; padding: 6px 22px 22px 22px; animation: mmp-screen-in 0.28s ease; }
|
|
92
|
+
.mmp-brand { display: flex; align-items: center; justify-content: center; height: 36px; margin-bottom: 10px; }
|
|
93
|
+
.mmp-brand img { display: block; height: 36px; width: auto; max-width: 160px; object-fit: contain; }
|
|
94
|
+
.mmp-merchant { max-width: 100%; font-size: 15px; font-weight: 500; color: var(--mmp-sub); overflow-wrap: anywhere; }
|
|
95
|
+
.mmp-amount { display: flex; align-items: baseline; justify-content: center; gap: 6px; min-height: 36px; margin: 2px 0 14px 0; }
|
|
96
|
+
.mmp-amount-value { font-size: 30px; font-weight: 700; line-height: 1.2; letter-spacing: -0.5px; font-variant-numeric: tabular-nums; }
|
|
97
|
+
.mmp-amount-currency { font-size: 14px; font-weight: 600; letter-spacing: 0.5px; color: var(--mmp-sub); }
|
|
98
|
+
|
|
99
|
+
.mmp-qr {
|
|
100
|
+
position: relative; display: flex; align-items: center; justify-content: center; flex-shrink: 0;
|
|
101
|
+
width: 228px; height: 228px; padding: 12px; background: #ffffff; border-radius: 18px;
|
|
102
|
+
border: 1px solid var(--mmp-border); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.06); overflow: hidden;
|
|
103
|
+
}
|
|
104
|
+
.mmp-qr canvas { display: block; width: 100% !important; height: 100% !important; animation: mmp-qr-in 0.4s cubic-bezier(0.2, 0.8, 0.2, 1); }
|
|
105
|
+
.mmp-qr-placeholder { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 22px; width: 100%; height: 100%; }
|
|
106
|
+
.mmp-loader-logo { display: block; width: 88px; height: 88px; border-radius: 22px; box-shadow: 0 10px 24px rgba(0, 0, 0, 0.18); animation: mmp-pulse 2.2s ease-in-out infinite; }
|
|
107
|
+
.mmp-loader-track { position: relative; width: 120px; height: 3px; border-radius: 3px; background: #ececf1; overflow: hidden; }
|
|
108
|
+
.mmp-loader-bar { position: absolute; top: 0; bottom: 0; left: 0; width: 40%; border-radius: 3px; background: #1d1d1f; animation: mmp-sweep 1.2s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
|
|
109
|
+
.mmp-qr-error { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; padding: 8px; font-size: 13px; color: #6e6e73; }
|
|
110
|
+
.mmp-qr-error button {
|
|
111
|
+
margin: 0; padding: 7px 14px; border: 1px solid #d2d2d7; border-radius: 10px; background: #ffffff;
|
|
112
|
+
font: inherit; font-size: 13px; font-weight: 600; color: #1d1d1f; cursor: pointer;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.mmp-caption { min-height: 44px; margin: 14px 0 0 0; display: flex; flex-direction: column; align-items: center; justify-content: center; }
|
|
116
|
+
.mmp-caption-title { font-size: 15px; font-weight: 600; }
|
|
117
|
+
.mmp-caption-title::after { content: ""; display: inline-block; width: 1.2em; text-align: left; animation: mmp-ellipsis 1.4s steps(4, end) infinite; }
|
|
118
|
+
.mmp-caption-sub { font-size: 13px; color: var(--mmp-sub); }
|
|
119
|
+
.mmp-hint { font-size: 13px; color: var(--mmp-sub); }
|
|
120
|
+
|
|
121
|
+
.mmp-status-row { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 8px; margin: 10px 0 14px 0; }
|
|
122
|
+
.mmp-waiting, .mmp-timer {
|
|
123
|
+
display: inline-flex; align-items: center; gap: 7px; padding: 5px 11px; border-radius: 999px;
|
|
124
|
+
font-size: 13px; font-weight: 600; background: var(--mmp-panel); border: 1px solid var(--mmp-border); color: var(--mmp-sub);
|
|
125
|
+
}
|
|
126
|
+
.mmp-dot { position: relative; width: 8px; height: 8px; border-radius: 50%; background: var(--mmp-success); flex-shrink: 0; }
|
|
127
|
+
.mmp-dot::after { content: ""; position: absolute; top: 0; right: 0; bottom: 0; left: 0; border-radius: 50%; background: inherit; animation: mmp-ping 1.6s ease-out infinite; }
|
|
128
|
+
.mmp-waiting[data-state="reconnecting"] .mmp-dot { background: var(--mmp-warn); }
|
|
129
|
+
.mmp-timer { font-variant-numeric: tabular-nums; color: var(--mmp-text); }
|
|
130
|
+
.mmp-timer svg { width: 13px; height: 13px; fill: currentColor; }
|
|
131
|
+
.mmp-timer[data-urgent="true"] { color: var(--mmp-danger); background: var(--mmp-danger-bg); border-color: transparent; }
|
|
132
|
+
|
|
133
|
+
.mmp-details { width: 100%; padding: 12px 14px; margin-bottom: 14px; background: var(--mmp-panel); border: 1px solid var(--mmp-border); border-radius: 14px; }
|
|
134
|
+
.mmp-detail { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 24px; font-size: 13px; color: var(--mmp-sub); }
|
|
135
|
+
.mmp-detail + .mmp-detail { margin-top: 6px; }
|
|
136
|
+
.mmp-detail > span:first-child { flex-shrink: 0; text-align: left; }
|
|
137
|
+
.mmp-detail strong { font-weight: 600; color: var(--mmp-text); text-align: right; overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }
|
|
138
|
+
.mmp-skeleton {
|
|
139
|
+
display: inline-block; height: 14px; border-radius: 7px;
|
|
140
|
+
background: linear-gradient(90deg, var(--mmp-skeleton) 25%, var(--mmp-skeleton-shine) 50%, var(--mmp-skeleton) 75%);
|
|
141
|
+
background-size: 200% 100%; animation: mmp-shimmer 1.4s linear infinite;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.mmp-btn {
|
|
145
|
+
display: flex; align-items: center; justify-content: center; gap: 8px; width: 100%; min-height: 50px; margin: 0; padding: 12px 20px;
|
|
146
|
+
border: 1px solid transparent; border-radius: 14px; background: var(--mmp-accent); color: var(--mmp-accent-text);
|
|
147
|
+
font: inherit; font-size: 16px; font-weight: 600; cursor: pointer; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.14);
|
|
148
|
+
transition: transform 0.15s ease, filter 0.2s ease, opacity 0.2s ease;
|
|
149
|
+
}
|
|
150
|
+
.mmp-btn:hover { filter: brightness(1.08); }
|
|
151
|
+
.mmp-btn:active { transform: scale(0.98); }
|
|
152
|
+
.mmp-btn:disabled { opacity: 0.45; cursor: default; filter: none; transform: none; box-shadow: none; }
|
|
153
|
+
.mmp-btn svg { width: 18px; height: 18px; flex-shrink: 0; }
|
|
154
|
+
.mmp-btn-secondary { background: transparent; color: var(--mmp-text); border-color: var(--mmp-border); box-shadow: none; }
|
|
155
|
+
.mmp-btn-secondary:hover { background: var(--mmp-chip); filter: none; }
|
|
156
|
+
.mmp-btn-danger { background: var(--mmp-danger-bg); color: var(--mmp-danger); box-shadow: none; }
|
|
157
|
+
.mmp-btn[data-busy="true"]::before {
|
|
158
|
+
content: ""; width: 16px; height: 16px; border-radius: 50%; border: 2px solid currentColor; border-right-color: transparent;
|
|
159
|
+
animation: mmp-spin 0.7s linear infinite;
|
|
160
|
+
}
|
|
161
|
+
.mmp-powered { margin-top: 12px; font-size: 12px; font-weight: 500; color: var(--mmp-sub); }
|
|
162
|
+
|
|
163
|
+
.mmp-sheet {
|
|
164
|
+
position: absolute; top: 52px; right: 0; bottom: 0; left: 0; z-index: 2;
|
|
165
|
+
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
166
|
+
padding: 12px 22px 24px 22px; background: var(--mmp-card); border-radius: 0 0 24px 24px; overflow-y: auto;
|
|
167
|
+
animation: mmp-sheet-in 0.26s cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
168
|
+
}
|
|
169
|
+
.mmp-root.mmp-translucent .mmp-sheet { -webkit-backdrop-filter: blur(24px); backdrop-filter: blur(24px); }
|
|
170
|
+
.mmp-icon { display: flex; align-items: center; justify-content: center; width: 72px; height: 72px; margin: 10px auto 18px auto; border-radius: 50%; flex-shrink: 0; animation: mmp-pop 0.45s cubic-bezier(0.2, 1.4, 0.4, 1); }
|
|
171
|
+
.mmp-icon svg { width: 36px; height: 36px; fill: none; stroke: currentColor; stroke-width: 2.6; stroke-linecap: round; stroke-linejoin: round; }
|
|
172
|
+
.mmp-icon svg path { stroke-dasharray: 60; stroke-dashoffset: 60; animation: mmp-draw 0.5s 0.2s ease forwards; }
|
|
173
|
+
.mmp-icon[data-tone="success"] { background: var(--mmp-success-bg); color: var(--mmp-success); }
|
|
174
|
+
.mmp-icon[data-tone="danger"] { background: var(--mmp-danger-bg); color: var(--mmp-danger); }
|
|
175
|
+
.mmp-icon[data-tone="warn"] { background: var(--mmp-warn-bg); color: var(--mmp-warn); }
|
|
176
|
+
.mmp-title { margin: 0 0 10px 0; font-size: 22px; font-weight: 700; line-height: inherit; color: var(--mmp-text); }
|
|
177
|
+
.mmp-pill { display: inline-block; max-width: 100%; padding: 6px 12px; margin-bottom: 14px; background: var(--mmp-panel); border: 1px solid var(--mmp-border); border-radius: 10px; font-size: 13px; font-weight: 600; color: var(--mmp-sub); overflow-wrap: anywhere; }
|
|
178
|
+
.mmp-text { margin: 0 0 6px 0; font-size: 15px; color: var(--mmp-sub); overflow-wrap: anywhere; }
|
|
179
|
+
.mmp-detail-text { max-width: 100%; max-height: 84px; margin: 4px 0 0 0; overflow-y: auto; font-size: 12px; line-height: 1.4; color: var(--mmp-sub); opacity: 0.85; overflow-wrap: anywhere; }
|
|
180
|
+
.mmp-actions { display: flex; flex-direction: column; gap: 10px; width: 100%; margin-top: 18px; }
|
|
181
|
+
|
|
182
|
+
@keyframes mmp-fade { from { opacity: 0; } to { opacity: 1; } }
|
|
183
|
+
@keyframes mmp-rise { from { opacity: 0; transform: translateY(18px) scale(0.97); } to { opacity: 1; transform: none; } }
|
|
184
|
+
@keyframes mmp-screen-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
|
|
185
|
+
@keyframes mmp-sheet-in { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: none; } }
|
|
186
|
+
@keyframes mmp-qr-in { from { opacity: 0; transform: scale(0.94); } to { opacity: 1; transform: none; } }
|
|
187
|
+
@keyframes mmp-pulse { 0%, 100% { opacity: 0.88; transform: scale(1); } 50% { opacity: 1; transform: scale(1.05); } }
|
|
188
|
+
@keyframes mmp-sweep { 0% { transform: translateX(-110%); } 100% { transform: translateX(260%); } }
|
|
189
|
+
@keyframes mmp-shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
|
|
190
|
+
@keyframes mmp-ping { 0% { transform: scale(1); opacity: 0.7; } 100% { transform: scale(2.6); opacity: 0; } }
|
|
191
|
+
@keyframes mmp-spin { to { transform: rotate(360deg); } }
|
|
192
|
+
@keyframes mmp-pop { from { opacity: 0; transform: scale(0.6); } to { opacity: 1; transform: none; } }
|
|
193
|
+
@keyframes mmp-draw { to { stroke-dashoffset: 0; } }
|
|
194
|
+
@keyframes mmp-ellipsis { 0% { content: ""; } 25% { content: "."; } 50% { content: ".."; } 75% { content: "..."; } }
|
|
195
|
+
|
|
196
|
+
@media (max-width: 380px) {
|
|
197
|
+
.mmp-root { padding: 10px; }
|
|
198
|
+
.mmp-screen { padding: 6px 16px 18px 16px; }
|
|
199
|
+
.mmp-qr { width: 204px; height: 204px; }
|
|
200
|
+
}
|
|
201
|
+
@media (prefers-reduced-motion: reduce) {
|
|
202
|
+
.mmp-root, .mmp-root *, .mmp-root *::before, .mmp-root *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; }
|
|
203
|
+
.mmp-icon svg path { stroke-dashoffset: 0; }
|
|
204
|
+
.mmp-loader-bar { width: 100%; opacity: 0.25; }
|
|
205
|
+
}
|
|
206
|
+
`;
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
export type MMPayLanguage = 'en' | 'mm';
|
|
2
|
+
|
|
3
|
+
export type MMPayTransport = 'auto' | 'websocket' | 'polling';
|
|
4
|
+
|
|
5
|
+
export type MMPayTerminalStatus = 'SUCCESS' | 'FAILED' | 'EXPIRED' | 'CANCELLED';
|
|
6
|
+
|
|
7
|
+
export interface IDesignOptions {
|
|
8
|
+
mode?: 'light' | 'dark' | 'light-translucent' | 'dark-translucent' | 'auto';
|
|
9
|
+
color?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface SDKOptions {
|
|
13
|
+
baseUrl?: string;
|
|
14
|
+
environment?: 'sandbox' | 'production';
|
|
15
|
+
merchantName?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Interval of the HTTP polling fallback, in milliseconds.
|
|
18
|
+
*/
|
|
19
|
+
pollInterval?: number;
|
|
20
|
+
/**
|
|
21
|
+
* How payment status reaches the browser. `auto` (default) uses a WebSocket
|
|
22
|
+
* and falls back to HTTP polling when a socket cannot be established.
|
|
23
|
+
*/
|
|
24
|
+
transport?: MMPayTransport;
|
|
25
|
+
/**
|
|
26
|
+
* Seconds the shopper has to pay before the modal shows "Expired".
|
|
27
|
+
* Default and maximum: 900 (15 minutes). Minimum: 60.
|
|
28
|
+
*/
|
|
29
|
+
timeoutSeconds?: number;
|
|
30
|
+
/**
|
|
31
|
+
* Language the modal opens in. The shopper can still switch, and their
|
|
32
|
+
* choice is remembered.
|
|
33
|
+
*/
|
|
34
|
+
language?: MMPayLanguage | 'my';
|
|
35
|
+
design?: IDesignOptions;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ICreateTokenRequestParams {
|
|
39
|
+
amount?: number;
|
|
40
|
+
orderId: string;
|
|
41
|
+
nonce: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ICreateTokenResponse {
|
|
45
|
+
token: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ICreatePaymentRequestParams {
|
|
49
|
+
amount: number;
|
|
50
|
+
orderId: string;
|
|
51
|
+
callbackUrl?: string;
|
|
52
|
+
customMessage?: string;
|
|
53
|
+
nonce: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ICreatePaymentResponse {
|
|
57
|
+
orderId: string;
|
|
58
|
+
amount: number;
|
|
59
|
+
status: 'PENDING' | 'SUCCESS' | 'FAILED' | 'REFUNDED' | 'CANCELLED' | 'EXPIRED';
|
|
60
|
+
currency: string;
|
|
61
|
+
vendorQrRefId: string;
|
|
62
|
+
qr: string;
|
|
63
|
+
createdAt: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface IPaymentShowRequestParams {
|
|
67
|
+
orderId: string;
|
|
68
|
+
nonce: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface IPaymentShowResponse {
|
|
72
|
+
orderId: string;
|
|
73
|
+
amount: number;
|
|
74
|
+
status: 'PENDING' | 'SUCCESS' | 'FAILED' | 'REFUNDED' | 'CANCELLED' | 'EXPIRED';
|
|
75
|
+
currency: string;
|
|
76
|
+
vendorQrRefId: string;
|
|
77
|
+
qr: string;
|
|
78
|
+
createdAt: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface ICancelPaymentRequestParams {
|
|
82
|
+
orderId: string;
|
|
83
|
+
nonce: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface ICancelPaymentResponse {
|
|
87
|
+
success: boolean;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface IPollingRequest {
|
|
91
|
+
orderId: string;
|
|
92
|
+
nonce: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface IPollingResponse {
|
|
96
|
+
status: 'PENDING' | 'SUCCESS' | 'FAILED' | 'REFUNDED' | 'CANCELLED' | 'EXPIRED';
|
|
97
|
+
vendorQrRefId?: string;
|
|
98
|
+
transactionRefId?: string;
|
|
99
|
+
orderId?: string;
|
|
100
|
+
amount?: number;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface IStreamTicketResponse {
|
|
104
|
+
ticket: string;
|
|
105
|
+
expiresIn: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface IModalEventResult {
|
|
109
|
+
success?: boolean;
|
|
110
|
+
created?: boolean;
|
|
111
|
+
cancelled?: boolean;
|
|
112
|
+
expired?: boolean;
|
|
113
|
+
failed?: boolean;
|
|
114
|
+
orderId?: string;
|
|
115
|
+
amount?: number;
|
|
116
|
+
vendorQrRefId?: string;
|
|
117
|
+
}
|