@mmpay/browser 0.0.0-stage → 1.3.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.
@@ -0,0 +1,476 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>MMPay SDK Playground</title>
7
+ <!-- Paths resolve from the SDK folder, so the build is loaded as ./dist/mmpay-sdk.js -->
8
+ <base href="../">
9
+ <script src="https://cdn.tailwindcss.com/3.4.17"></script>
10
+ <style>
11
+ .swatch[aria-pressed="true"] { box-shadow: 0 0 0 2px #fff, 0 0 0 4px #18181b; }
12
+ .seg button[aria-pressed="true"] { background: #fff; color: #18181b; box-shadow: 0 1px 2px rgba(0,0,0,.08); }
13
+ .tab[aria-selected="true"] { color: #fff; border-color: #fff; }
14
+ pre code .k { color: #c792ea; } pre code .s { color: #c3e88d; } pre code .c { color: #6b7280; }
15
+ </style>
16
+ </head>
17
+ <body class="bg-zinc-100 text-zinc-900 antialiased">
18
+ <header class="border-b border-zinc-200 bg-white">
19
+ <div class="mx-auto flex max-w-7xl items-center justify-between gap-4 px-4 py-4">
20
+ <div>
21
+ <h1 class="text-lg font-semibold">MMPay Browser SDK Playground</h1>
22
+ <p class="text-sm text-zinc-500">Pick a look, copy the code, run a real test against <code>./dist/mmpay-sdk.js</code>.</p>
23
+ </div>
24
+ <span id="sdkBadge" class="rounded-full bg-zinc-100 px-3 py-1 text-xs font-medium text-zinc-500">SDK: checking…</span>
25
+ </div>
26
+ </header>
27
+
28
+ <div id="fileWarning" hidden class="mx-auto mt-4 max-w-7xl px-4">
29
+ <div class="rounded-lg border border-amber-300 bg-amber-50 p-3 text-sm text-amber-900">
30
+ Opened as a file: the WebSocket will be refused and the SDK falls back to polling. Run <code>npm run serve</code> and open <code>http://localhost:5173/test/</code>.
31
+ </div>
32
+ </div>
33
+
34
+ <main class="mx-auto grid max-w-7xl gap-6 px-4 py-6 lg:grid-cols-[minmax(0,5fr)_minmax(0,7fr)]">
35
+ <!-- Left: configuration -->
36
+ <section class="space-y-5">
37
+ <div class="rounded-2xl border border-zinc-200 bg-white p-5">
38
+ <h2 class="mb-4 text-sm font-semibold uppercase tracking-wide text-zinc-500">Appearance</h2>
39
+
40
+ <label class="mb-1 block text-sm font-medium">Theme</label>
41
+ <div class="seg mb-4 grid grid-cols-3 gap-1 rounded-xl bg-zinc-100 p-1 text-sm font-medium text-zinc-500" data-group="theme">
42
+ <button type="button" data-value="light" class="rounded-lg py-1.5">Light</button>
43
+ <button type="button" data-value="dark" class="rounded-lg py-1.5">Dark</button>
44
+ <button type="button" data-value="auto" class="rounded-lg py-1.5">System</button>
45
+ </div>
46
+
47
+ <label class="mb-1 block text-sm font-medium">Card style</label>
48
+ <div class="seg mb-4 grid grid-cols-2 gap-1 rounded-xl bg-zinc-100 p-1 text-sm font-medium text-zinc-500" data-group="style">
49
+ <button type="button" data-value="solid" class="rounded-lg py-1.5">Solid</button>
50
+ <button type="button" data-value="translucent" class="rounded-lg py-1.5">Translucent</button>
51
+ </div>
52
+
53
+ <label class="mb-2 block text-sm font-medium">Button color</label>
54
+ <div class="mb-4 flex flex-wrap items-center gap-2" id="swatches">
55
+ <button type="button" class="swatch h-8 w-8 rounded-full border border-zinc-300 bg-white text-[10px] text-zinc-400" data-color="">auto</button>
56
+ <label class="flex h-8 items-center gap-2 rounded-full border border-zinc-300 pl-1 pr-3 text-sm">
57
+ <input type="color" id="colorPicker" class="h-6 w-6 cursor-pointer rounded-full border-0 bg-transparent p-0">
58
+ <input type="text" id="colorText" placeholder="#DE3163" class="w-20 bg-transparent text-xs outline-none">
59
+ </label>
60
+ </div>
61
+
62
+ <div class="grid grid-cols-2 gap-3">
63
+ <label class="text-sm font-medium">Merchant name
64
+ <input id="merchantName" value="Golden Lotus Tea House" class="mt-1 w-full rounded-lg border border-zinc-300 px-3 py-2 text-sm font-normal">
65
+ </label>
66
+ <label class="text-sm font-medium">Language
67
+ <select id="language" class="mt-1 w-full rounded-lg border border-zinc-300 px-3 py-2 text-sm font-normal">
68
+ <option value="">Browser default</option><option value="en">English</option><option value="mm">မြန်မာ</option>
69
+ </select>
70
+ </label>
71
+ </div>
72
+ </div>
73
+
74
+ <div class="rounded-2xl border border-zinc-200 bg-white p-5">
75
+ <h2 class="mb-4 text-sm font-semibold uppercase tracking-wide text-zinc-500">Payment</h2>
76
+ <div class="seg mb-4 grid grid-cols-2 gap-1 rounded-xl bg-zinc-100 p-1 text-sm font-medium text-zinc-500" data-group="flow">
77
+ <button type="button" data-value="modal" class="rounded-lg py-1.5">showPaymentModal()</button>
78
+ <button type="button" data-value="pay" class="rounded-lg py-1.5">pay(orderId)</button>
79
+ </div>
80
+ <div class="grid grid-cols-2 gap-3">
81
+ <label class="text-sm font-medium">Amount (MMK)
82
+ <input id="amount" type="number" min="1000" value="25000" class="mt-1 w-full rounded-lg border border-zinc-300 px-3 py-2 text-sm font-normal">
83
+ </label>
84
+ <label class="text-sm font-medium">Timer (minutes)
85
+ <input id="timeout" type="number" min="1" max="15" value="15" class="mt-1 w-full rounded-lg border border-zinc-300 px-3 py-2 text-sm font-normal">
86
+ </label>
87
+ <label class="col-span-2 text-sm font-medium">Order ID
88
+ <div class="mt-1 flex gap-2">
89
+ <input id="orderId" class="w-full rounded-lg border border-zinc-300 px-3 py-2 font-mono text-sm font-normal">
90
+ <button type="button" id="btnNewId" class="shrink-0 rounded-lg border border-zinc-300 px-3 text-sm">New</button>
91
+ </div>
92
+ </label>
93
+ </div>
94
+ </div>
95
+
96
+ <div class="rounded-2xl border border-zinc-200 bg-white p-5">
97
+ <h2 class="mb-4 text-sm font-semibold uppercase tracking-wide text-zinc-500">Connection</h2>
98
+ <label class="mb-3 flex items-center justify-between gap-3 text-sm font-medium">
99
+ <span>Mock API <span class="font-normal text-zinc-500">— no server, status set by buttons</span></span>
100
+ <input type="checkbox" id="mock" class="h-5 w-5 accent-zinc-900">
101
+ </label>
102
+ <div class="space-y-3" id="realFields">
103
+ <label class="block text-sm font-medium">Publishable key
104
+ <input id="key" placeholder="pk_test_…" autocomplete="off" spellcheck="false" class="mt-1 w-full rounded-lg border border-zinc-300 px-3 py-2 font-mono text-sm font-normal">
105
+ </label>
106
+ <div class="grid grid-cols-2 gap-3">
107
+ <label class="text-sm font-medium">Base URL
108
+ <input id="baseUrl" value="https://bbapi.myanmyanpay.com" class="mt-1 w-full rounded-lg border border-zinc-300 px-3 py-2 text-sm font-normal">
109
+ </label>
110
+ <label class="text-sm font-medium">Transport
111
+ <select id="transport" class="mt-1 w-full rounded-lg border border-zinc-300 px-3 py-2 text-sm font-normal">
112
+ <option value="auto">auto</option><option value="websocket">websocket</option><option value="polling">polling</option>
113
+ </select>
114
+ </label>
115
+ </div>
116
+ </div>
117
+ </div>
118
+ </section>
119
+
120
+ <!-- Right: code + run -->
121
+ <section class="space-y-5 lg:sticky lg:top-6 lg:self-start">
122
+ <div class="overflow-hidden rounded-2xl bg-zinc-900 shadow-xl">
123
+ <div class="flex items-center justify-between border-b border-zinc-800 px-4">
124
+ <div class="flex gap-4 text-sm font-medium text-zinc-400" role="tablist">
125
+ <button type="button" class="tab border-b-2 border-transparent py-3" data-tab="html" role="tab">HTML</button>
126
+ <button type="button" class="tab border-b-2 border-transparent py-3" data-tab="react" role="tab">React</button>
127
+ <button type="button" class="tab border-b-2 border-transparent py-3" data-tab="vue" role="tab">Vue</button>
128
+ </div>
129
+ <button type="button" id="btnCopy" class="rounded-md bg-zinc-800 px-3 py-1.5 text-xs font-medium text-zinc-200 hover:bg-zinc-700">Copy</button>
130
+ </div>
131
+ <pre class="max-h-[420px] overflow-auto p-4 text-[13px] leading-relaxed text-zinc-100"><code id="code"></code></pre>
132
+ </div>
133
+
134
+ <div class="rounded-2xl border border-zinc-200 bg-white p-5">
135
+ <div class="flex flex-wrap items-center gap-2">
136
+ <button type="button" id="btnRun" class="rounded-xl bg-zinc-900 px-5 py-2.5 text-sm font-semibold text-white hover:bg-zinc-700">▶ Run test</button>
137
+ <button type="button" id="btnSpam" class="rounded-xl border border-zinc-300 px-4 py-2.5 text-sm font-medium">Spam test (5×)</button>
138
+ <button type="button" id="btnReset" class="rounded-xl border border-zinc-300 px-4 py-2.5 text-sm font-medium">Reset state</button>
139
+ <button type="button" id="btnReload" class="rounded-xl border border-zinc-300 px-4 py-2.5 text-sm font-medium">Reload (auto-resume)</button>
140
+ </div>
141
+ <div id="mockButtons" class="mt-3 flex flex-wrap items-center gap-2 text-sm">
142
+ <span class="text-zinc-500">Mock status:</span>
143
+ <button type="button" data-status="SUCCESS" class="rounded-lg bg-emerald-50 px-3 py-1.5 font-medium text-emerald-700">Paid</button>
144
+ <button type="button" data-status="FAILED" class="rounded-lg bg-red-50 px-3 py-1.5 font-medium text-red-700">Failed</button>
145
+ <button type="button" data-status="EXPIRED" class="rounded-lg bg-zinc-100 px-3 py-1.5 font-medium text-zinc-700">Expired</button>
146
+ <button type="button" data-status="CANCELLED" class="rounded-lg bg-amber-50 px-3 py-1.5 font-medium text-amber-700">Cancelled</button>
147
+ </div>
148
+ <div id="state" class="mt-4 grid grid-cols-2 gap-2 text-xs sm:grid-cols-4"></div>
149
+ <div class="mt-4 flex items-center justify-between">
150
+ <h3 class="text-sm font-semibold">Log</h3>
151
+ <button type="button" id="btnClearLog" class="text-xs text-zinc-500 hover:text-zinc-900">Clear</button>
152
+ </div>
153
+ <pre id="log" class="mt-2 h-56 overflow-auto whitespace-pre-wrap break-all rounded-xl bg-zinc-950 p-3 font-mono text-[12px] leading-relaxed text-zinc-200"></pre>
154
+ </div>
155
+ </section>
156
+ </main>
157
+
158
+ <script>
159
+ // ---- Network log: shows what the built SDK really sends ---------------
160
+ window.MMPayNetLog = function () { };
161
+ (function () {
162
+ var nativeFetch = window.fetch.bind(window);
163
+ window.fetch = function (input, init) {
164
+ var url = typeof input === 'string' ? input : input.url;
165
+ if (url.indexOf('/xpayments/') === -1) return nativeFetch(input, init);
166
+ var name = url.split('/xpayments/')[1].split('?')[0], t = Date.now();
167
+ return nativeFetch(input, init).then(function (r) {
168
+ window.MMPayNetLog('http', r.status + ' ' + name + ' ' + (Date.now() - t) + 'ms'); return r;
169
+ }, function (e) { window.MMPayNetLog('err', 'HTTP failed ' + name + ': ' + e.message); throw e; });
170
+ };
171
+ var Native = window.WebSocket;
172
+ if (!Native) return;
173
+ var Logged = function (url, protocols) {
174
+ var s = protocols === undefined ? new Native(url) : new Native(url, protocols);
175
+ window.MMPayNetLog('ws', 'connecting ' + String(url).split('?')[0]);
176
+ s.addEventListener('open', function () { window.MMPayNetLog('ws', 'open'); });
177
+ s.addEventListener('message', function (e) { window.MMPayNetLog('ws', '← ' + e.data); });
178
+ s.addEventListener('close', function (e) { window.MMPayNetLog('ws', 'closed ' + e.code + (e.reason ? ' ' + e.reason : '')); });
179
+ return s;
180
+ };
181
+ Logged.prototype = Native.prototype;
182
+ ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'].forEach(function (k) { Logged[k] = Native[k]; });
183
+ window.WebSocket = Logged;
184
+ })();
185
+
186
+ // ---- Mock API (orders survive reload via sessionStorage) --------------
187
+ (function () {
188
+ var realFetch = window.fetch;
189
+ var KEY = 'mmpay_test_mock_orders';
190
+ var QR = '00020101021226580014A000000677010111011500000000000000002150000000000000005204581453031045405250005802MM5911MMPay Store6006Yangon6304ABCD';
191
+ function load() { try { return JSON.parse(sessionStorage.getItem(KEY)) || {orders: {}, created: 0}; } catch (e) { return {orders: {}, created: 0}; } }
192
+ function save(db) { sessionStorage.setItem(KEY, JSON.stringify(db)); }
193
+ function json(status, body) { return new Response(JSON.stringify(body), {status: status, headers: {'Content-Type': 'application/json'}}); }
194
+ function wait(ms) { return new Promise(function (r) { setTimeout(r, ms); }); }
195
+ window.MMPayMock = {
196
+ enabled: function () { return document.getElementById('mock').checked; },
197
+ db: load,
198
+ setStatus: function (status) {
199
+ var db = load(), changed = [];
200
+ Object.keys(db.orders).forEach(function (id) { if (db.orders[id].status === 'PENDING') { db.orders[id].status = status; changed.push(id); } });
201
+ save(db); return changed;
202
+ },
203
+ reset: function () { sessionStorage.removeItem(KEY); }
204
+ };
205
+ window.fetch = function (input, init) {
206
+ var url = typeof input === 'string' ? input : input.url;
207
+ if (!window.MMPayMock.enabled() || url.indexOf('/xpayments/') === -1) return realFetch(input, init);
208
+ var body = {}; try { body = JSON.parse((init && init.body) || '{}'); } catch (e) { }
209
+ var ep = url.split('/xpayments/')[1].replace(/^(sandbox|production)-/, '');
210
+ var db = load(), order = db.orders[body.orderId];
211
+ if (ep === 'token-request') return wait(300).then(function () { return json(200, {token: 'mock-token'}); });
212
+ if (ep === 'payment-stream-ticket') return Promise.resolve(json(404, {error: 'Not Found'}));
213
+ if (ep === 'payment-polling') return Promise.resolve(order ? json(200, order) : json(404, {success: false, message: 'Order Not Found'}));
214
+ if (ep === 'payment-cancel') {
215
+ if (!order || order.status !== 'PENDING') return Promise.resolve(json(404, {success: false, message: 'Order Not Found'}));
216
+ order.status = 'CANCELLED'; save(db); return Promise.resolve(json(200, order));
217
+ }
218
+ if (ep === 'payment-show') return wait(900).then(function () { return order ? json(200, Object.assign({createdAt: new Date().toISOString()}, order)) : json(404, {success: false, message: 'Order Not Found'}); });
219
+ if (ep === 'payment-create') return wait(1200).then(function () {
220
+ var fresh = load(), existing = fresh.orders[body.orderId];
221
+ if (existing && existing.status !== 'PENDING') return json(500, {success: false, message: 'Order already ' + existing.status.toLowerCase()});
222
+ if (existing) return json(200, existing);
223
+ var ref = 'MMQR' + String(Date.now()).slice(-10);
224
+ fresh.orders[body.orderId] = {orderId: body.orderId, amount: body.amount, currency: 'MMK', status: 'PENDING', vendorQrRefId: ref, transactionRefId: ref, qr: QR};
225
+ fresh.created++; save(fresh); return json(201, fresh.orders[body.orderId]);
226
+ });
227
+ return Promise.resolve(json(404, {error: 'Not Found'}));
228
+ };
229
+ })();
230
+ </script>
231
+ <script src="./dist/mmpay-sdk.js"></script>
232
+ <script>
233
+ var $ = function (id) { return document.getElementById(id); };
234
+ var FORM_KEY = 'mmpay_playground_v1';
235
+ var state = {theme: 'light', style: 'solid', flow: 'modal', tab: 'html', color: ''};
236
+ var PRESETS = ['#DE3163', '#2563EB', '#16A34A', '#7C3AED', '#EA580C', '#0F172A'];
237
+ var FIELDS = ['merchantName', 'language', 'amount', 'timeout', 'key', 'baseUrl', 'transport'];
238
+ var sdk = null, sdkSignature = '';
239
+
240
+ // ---- Log ---------------------------------------------------------------
241
+ var TAG = {http: 'text-sky-400', ws: 'text-violet-400', event: 'text-emerald-400', call: 'text-zinc-400', err: 'text-red-400', mock: 'text-amber-400'};
242
+ function esc(s) { return String(s).replace(/[&<>"']/g, function (c) { return {'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'}[c]; }); }
243
+ function log(kind, msg) {
244
+ var line = document.createElement('div');
245
+ line.innerHTML = '<span class="text-zinc-600">' + new Date().toTimeString().slice(0, 8) + '</span> <span class="' + (TAG[kind] || '') + '">' + kind.toUpperCase().padEnd(5) + '</span> ' + esc(msg);
246
+ $('log').appendChild(line); $('log').scrollTop = $('log').scrollHeight;
247
+ }
248
+ window.MMPayNetLog = log;
249
+
250
+ // ---- Derived options -----------------------------------------------------
251
+ function mode() {
252
+ if (state.theme === 'auto') return 'auto';
253
+ return state.theme + (state.style === 'translucent' ? '-translucent' : '');
254
+ }
255
+ function options() {
256
+ var o = {merchantName: $('merchantName').value.trim() || undefined};
257
+ if ($('language').value) o.language = $('language').value;
258
+ var minutes = Number($('timeout').value);
259
+ if (minutes && minutes < 15) o.timeoutSeconds = Math.max(1, Math.round(minutes)) * 60;
260
+ if (!$('mock').checked) {
261
+ if ($('baseUrl').value.trim() && $('baseUrl').value.trim() !== 'https://bbapi.myanmyanpay.com') o.baseUrl = $('baseUrl').value.trim();
262
+ if ($('transport').value !== 'auto') o.transport = $('transport').value;
263
+ }
264
+ var design = {};
265
+ if (mode() !== 'light') design.mode = mode();
266
+ if (state.color) design.color = state.color;
267
+ if (Object.keys(design).length) o.design = design;
268
+ return o;
269
+ }
270
+
271
+ // ---- Code generation ----------------------------------------------------
272
+ function objectLiteral(obj, indent) {
273
+ var pad = new Array(indent + 1).join(' ');
274
+ var lines = Object.keys(obj).filter(function (k) { return obj[k] !== undefined; }).map(function (k) {
275
+ var v = obj[k];
276
+ var val = typeof v === 'object' ? objectLiteral(v, indent + 2) : (typeof v === 'string' ? "'" + v.replace(/'/g, "\\'") + "'" : String(v));
277
+ return pad + ' ' + k + ': ' + val;
278
+ });
279
+ return lines.length ? '{\n' + lines.join(',\n') + '\n' + pad + '}' : '{}';
280
+ }
281
+ function snippet() {
282
+ var key = $('key').value.trim() || 'pk_test_YOUR_KEY';
283
+ var opts = objectLiteral(options(), 0);
284
+ var amount = Number($('amount').value) || 25000;
285
+ var call = state.flow === 'modal'
286
+ ? 'mmpay.showPaymentModal({ orderId, amount: ' + amount + ' }, onResult);'
287
+ : 'mmpay.pay(orderId, onResult); // order created on your backend first';
288
+ var handler = [
289
+ 'function onResult(result) {',
290
+ " if (result.success) console.log('Paid', result.orderId, result.vendorQrRefId);",
291
+ " if (result.cancelled || result.expired || result.failed) console.log('Not paid', result);",
292
+ '}'
293
+ ].join('\n');
294
+ if (state.tab === 'html') return [
295
+ '<script src="https://cdn.jsdelivr.net/npm/@mmpay/browser@1.3.0/dist/mmpay-sdk.js"><\/script>',
296
+ '<button id="pay">Pay with MMQR</button>',
297
+ '<script>',
298
+ " const mmpay = new MMPaySDK('" + key + "', " + opts.replace(/\n/g, '\n ') + ');',
299
+ '',
300
+ ' ' + handler.replace(/\n/g, '\n '),
301
+ '',
302
+ " document.getElementById('pay').onclick = () => {",
303
+ " const orderId = 'ORD-' + Date.now();",
304
+ ' ' + call,
305
+ ' };',
306
+ '<\/script>'
307
+ ].join('\n');
308
+ if (state.tab === 'react') return [
309
+ "import { useMemo } from 'react';",
310
+ "import { MMPaySDK } from '@mmpay/browser';",
311
+ '',
312
+ handler,
313
+ '',
314
+ 'export function PayButton() {',
315
+ " const mmpay = useMemo(() => new MMPaySDK('" + key + "', " + opts.replace(/\n/g, '\n ') + '), []);',
316
+ '',
317
+ ' const pay = () => {',
318
+ " const orderId = 'ORD-' + Date.now();",
319
+ ' ' + call,
320
+ ' };',
321
+ '',
322
+ ' return <button onClick={pay}>Pay with MMQR</button>;',
323
+ '}'
324
+ ].join('\n');
325
+ return [
326
+ '<script setup>',
327
+ "import { MMPaySDK } from '@mmpay/browser';",
328
+ '',
329
+ "const mmpay = new MMPaySDK('" + key + "', " + opts + ');',
330
+ '',
331
+ handler,
332
+ '',
333
+ 'function pay() {',
334
+ " const orderId = 'ORD-' + Date.now();",
335
+ ' ' + call,
336
+ '}',
337
+ '<\/script>',
338
+ '',
339
+ '<template>',
340
+ ' <button @click="pay">Pay with MMQR</button>',
341
+ '</template>'
342
+ ].join('\n');
343
+ }
344
+ function highlight(src) {
345
+ return esc(src)
346
+ .replace(/(^|[ \t;])(\/\/[^\n]*)/gm, '$1<span class="c">$2</span>')
347
+ .replace(/(&#39;[^&\n]*?&#39;)/g, '<span class="s">$1</span>')
348
+ .replace(/\b(const|new|function|return|import|from|export|if)\b/g, '<span class="k">$1</span>');
349
+ }
350
+ function render() {
351
+ document.querySelectorAll('.seg').forEach(function (g) {
352
+ g.querySelectorAll('button').forEach(function (b) { b.setAttribute('aria-pressed', String(state[g.dataset.group] === b.dataset.value)); });
353
+ });
354
+ document.querySelector('[data-group="style"]').classList.toggle('opacity-40', state.theme === 'auto');
355
+ document.querySelectorAll('.tab').forEach(function (t) { t.setAttribute('aria-selected', String(t.dataset.tab === state.tab)); });
356
+ document.querySelectorAll('.swatch').forEach(function (s) { s.setAttribute('aria-pressed', String((s.dataset.color || '') === state.color)); });
357
+ if (document.activeElement !== $('colorText')) $('colorText').value = state.color;
358
+ if (/^#[0-9a-f]{6}$/i.test(state.color)) $('colorPicker').value = state.color;
359
+ var mock = $('mock').checked;
360
+ $('realFields').classList.toggle('opacity-40', mock);
361
+ $('mockButtons').style.display = mock ? '' : 'none';
362
+ $('code').innerHTML = highlight(snippet());
363
+ save();
364
+ }
365
+
366
+ // ---- Persistence ----------------------------------------------------------
367
+ function save() {
368
+ var v = {state: state, mock: $('mock').checked};
369
+ FIELDS.forEach(function (id) { v[id] = $(id).value; });
370
+ try { localStorage.setItem(FORM_KEY, JSON.stringify(v)); } catch (e) { }
371
+ }
372
+ function restore() {
373
+ try {
374
+ var v = JSON.parse(localStorage.getItem(FORM_KEY)); if (!v) return;
375
+ Object.assign(state, v.state || {});
376
+ FIELDS.forEach(function (id) { if (v[id] !== undefined) $(id).value = v[id]; });
377
+ $('mock').checked = !!v.mock;
378
+ } catch (e) { }
379
+ }
380
+
381
+ // ---- Status panel ----------------------------------------------------------
382
+ function refreshState() {
383
+ var cached = null; try { cached = JSON.parse(localStorage.getItem('mmpay_pending_tx')); } catch (e) { }
384
+ var cells = [
385
+ ['Unfinished order', cached ? cached.payload.orderId : '—'],
386
+ ['Time left', cached ? Math.max(0, Math.round((cached.expireAt - Date.now()) / 1000)) + 's' : '—'],
387
+ ['Language saved', localStorage.getItem('mmpay_lang') || '—']
388
+ ];
389
+ if ($('mock').checked) cells.push(['Mock orders created', String(window.MMPayMock.db().created)]);
390
+ $('state').innerHTML = cells.map(function (c) {
391
+ return '<div class="rounded-lg bg-zinc-50 p-2"><div class="text-zinc-500">' + c[0] + '</div><div class="truncate font-mono font-medium">' + esc(c[1]) + '</div></div>';
392
+ }).join('');
393
+ }
394
+
395
+ // ---- Running the SDK -------------------------------------------------------
396
+ function getSdk() {
397
+ if (typeof MMPaySDK !== 'function') { log('err', './dist/mmpay-sdk.js missing — run npm run build'); return null; }
398
+ var key = $('key').value.trim();
399
+ if (!$('mock').checked && key.indexOf('pk_test_') !== 0) { log('err', 'Enter a pk_test_ key, or turn on the mock API'); return null; }
400
+ var o = options();
401
+ if ($('mock').checked) o.baseUrl = location.origin;
402
+ o.pollInterval = 2000;
403
+ var sig = JSON.stringify([key, o]);
404
+ if (!sdk || sig !== sdkSignature) { sdk = new MMPaySDK(key || 'pk_test_mock', o); sdkSignature = sig; }
405
+ return sdk;
406
+ }
407
+ function onResult(result) {
408
+ var flags = Object.keys(result).filter(function (k) { return result[k] === true; });
409
+ log('event', (flags.join(', ') || 'none') + ' ' + JSON.stringify({orderId: result.orderId, amount: result.amount, ref: result.vendorQrRefId}));
410
+ refreshState();
411
+ }
412
+ function run() {
413
+ var s = getSdk(); if (!s) return;
414
+ var orderId = $('orderId').value.trim();
415
+ if (state.flow === 'modal') {
416
+ var params = {orderId: orderId, amount: Number($('amount').value)};
417
+ log('call', 'showPaymentModal ' + JSON.stringify(params));
418
+ s.showPaymentModal(params, onResult);
419
+ } else {
420
+ log('call', 'pay ' + orderId);
421
+ s.pay(orderId, onResult);
422
+ }
423
+ }
424
+ function newOrderId() { $('orderId').value = 'TEST-' + Date.now(); }
425
+
426
+ // ---- Wiring ------------------------------------------------------------------
427
+ PRESETS.forEach(function (c) {
428
+ var b = document.createElement('button');
429
+ b.type = 'button'; b.className = 'swatch h-8 w-8 rounded-full'; b.style.background = c; b.dataset.color = c; b.title = c;
430
+ $('swatches').insertBefore(b, $('swatches').lastElementChild);
431
+ });
432
+ $('swatches').addEventListener('click', function (e) {
433
+ var b = e.target.closest('.swatch'); if (!b) return;
434
+ state.color = b.dataset.color || ''; render();
435
+ });
436
+ $('colorPicker').addEventListener('input', function () { state.color = this.value.toUpperCase(); render(); });
437
+ $('colorText').addEventListener('input', function () { state.color = this.value.trim(); render(); });
438
+ document.querySelectorAll('.seg').forEach(function (g) {
439
+ g.addEventListener('click', function (e) { var b = e.target.closest('button'); if (b) { state[g.dataset.group] = b.dataset.value; render(); } });
440
+ });
441
+ document.querySelectorAll('.tab').forEach(function (t) { t.onclick = function () { state.tab = t.dataset.tab; render(); }; });
442
+ FIELDS.concat(['mock']).forEach(function (id) { $(id).addEventListener('input', render); $(id).addEventListener('change', render); });
443
+ $('btnCopy').onclick = function () {
444
+ var done = function () { $('btnCopy').textContent = 'Copied ✓'; setTimeout(function () { $('btnCopy').textContent = 'Copy'; }, 1500); };
445
+ if (navigator.clipboard && window.isSecureContext) navigator.clipboard.writeText(snippet()).then(done);
446
+ else { var t = document.createElement('textarea'); t.value = snippet(); document.body.appendChild(t); t.select(); document.execCommand('copy'); t.remove(); done(); }
447
+ };
448
+ $('btnRun').onclick = run;
449
+ $('btnNewId').onclick = function () { newOrderId(); };
450
+ $('btnSpam').onclick = function () {
451
+ log('call', 'spam: 5 calls with new order IDs — only one order should be created');
452
+ var n = 0, timer = setInterval(function () { newOrderId(); run(); if (++n >= 5) clearInterval(timer); }, 700);
453
+ };
454
+ $('btnReset').onclick = function () {
455
+ localStorage.removeItem('mmpay_pending_tx'); localStorage.removeItem('mmpay_lang'); window.MMPayMock.reset();
456
+ log('mock', 'cleared cached order, saved language and mock orders'); refreshState();
457
+ };
458
+ $('btnReload').onclick = function () { save(); location.reload(); };
459
+ $('btnClearLog').onclick = function () { $('log').textContent = ''; };
460
+ document.querySelectorAll('[data-status]').forEach(function (b) {
461
+ b.onclick = function () { var c = window.MMPayMock.setStatus(b.dataset.status); log('mock', 'set ' + b.dataset.status + ' → ' + (c.join(', ') || 'no pending order')); refreshState(); };
462
+ });
463
+
464
+ restore();
465
+ newOrderId();
466
+ render();
467
+ refreshState();
468
+ setInterval(refreshState, 1000);
469
+ if (location.protocol === 'file:') $('fileWarning').hidden = false;
470
+ var ok = typeof MMPaySDK === 'function';
471
+ $('sdkBadge').textContent = ok ? 'SDK loaded ✓' : 'SDK missing — npm run build';
472
+ $('sdkBadge').className = 'rounded-full px-3 py-1 text-xs font-medium ' + (ok ? 'bg-emerald-50 text-emerald-700' : 'bg-red-50 text-red-700');
473
+ if (ok && localStorage.getItem('mmpay_pending_tx')) { log('call', 'unfinished order in cache — SDK auto-resumes it'); getSdk(); }
474
+ </script>
475
+ </body>
476
+ </html>
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "ESNext",
4
+ "declaration": true,
5
+ "outDir": "dist/esm",
6
+ "rootDir": "./src",
7
+ "strict": false,
8
+ "moduleResolution": "Node",
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true
11
+ },
12
+ "include": [
13
+ "src/**/*"
14
+ ]
15
+ }