@tineon/t9n 0.2.1 → 0.2.2
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/dist/index.js +49 -8
- package/dist/types.d.ts +4 -2
- package/dist/types.js +9 -4
- package/dist/ui/modal.d.ts +8 -3
- package/dist/ui/modal.js +27 -16
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
import { checkoutConfigSchema, T9N_DEFAULT_API_BASE_URL, } from "./types.js";
|
|
2
2
|
import { CheckoutModal } from "./ui/modal.js";
|
|
3
|
+
const T9N_NETWORK_LABELS = {
|
|
4
|
+
bitcoin: "Bitcoin",
|
|
5
|
+
ethereum: "Ethereum",
|
|
6
|
+
tron: "Tron",
|
|
7
|
+
bsc: "BSC",
|
|
8
|
+
polygon: "Polygon",
|
|
9
|
+
arbitrum: "Arbitrum",
|
|
10
|
+
avax: "Avalanche",
|
|
11
|
+
solana: "Solana",
|
|
12
|
+
lisk: "Lisk",
|
|
13
|
+
flow: "Flow",
|
|
14
|
+
ronin: "Ronin",
|
|
15
|
+
sei: "Sei",
|
|
16
|
+
fantom: "Fantom",
|
|
17
|
+
hedera: "Hedera",
|
|
18
|
+
};
|
|
19
|
+
// Networks on which each stablecoin is supported — determines per-chain chip expansion
|
|
20
|
+
const T9N_STABLECOIN_NETWORKS = {
|
|
21
|
+
USDT: ["tron", "ethereum", "bsc", "polygon", "solana", "arbitrum", "avax"],
|
|
22
|
+
USDC: ["ethereum", "polygon", "bsc", "arbitrum", "avax", "solana"],
|
|
23
|
+
};
|
|
3
24
|
const DEFAULT_BUTTON_TEXT = "Pay with T9N";
|
|
4
25
|
const DEFAULT_THEME = "solid";
|
|
5
26
|
export class T9nCheckout {
|
|
@@ -59,8 +80,22 @@ export class T9nCheckout {
|
|
|
59
80
|
this.expiresAt = new Date(session.expiresAt);
|
|
60
81
|
this.cfg.hooks?.onSessionCreated?.(session);
|
|
61
82
|
this.emitStatus("created");
|
|
62
|
-
|
|
63
|
-
|
|
83
|
+
// Build per-chain options — stablecoins expand into one row per supported network
|
|
84
|
+
const modalOptions = [];
|
|
85
|
+
for (const sym of this.currencies) {
|
|
86
|
+
const stablecoinNets = T9N_STABLECOIN_NETWORKS[sym];
|
|
87
|
+
if (stablecoinNets) {
|
|
88
|
+
for (const net of stablecoinNets) {
|
|
89
|
+
modalOptions.push({ symbol: sym, network: net, networkLabel: T9N_NETWORK_LABELS[net] ?? net });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
const net = this.defaultNetworkFor(sym);
|
|
94
|
+
modalOptions.push({ symbol: sym, network: net, networkLabel: "" });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
this.modal = new CheckoutModal(modalOptions, {
|
|
98
|
+
onSelectCurrency: async (symbol, network) => this.selectCurrency(symbol, network),
|
|
64
99
|
onConfirmPayment: async () => this.confirmPayment(),
|
|
65
100
|
onClose: () => this.close(),
|
|
66
101
|
});
|
|
@@ -142,8 +177,7 @@ export class T9nCheckout {
|
|
|
142
177
|
}
|
|
143
178
|
return (await res.json());
|
|
144
179
|
}
|
|
145
|
-
async selectCurrency(currency) {
|
|
146
|
-
const network = this.defaultNetworkFor(currency);
|
|
180
|
+
async selectCurrency(currency, network) {
|
|
147
181
|
const res = await this.fetchWithTimeout(`${this.getApiBaseUrl()}/api/merchant/checkout/sessions/${this.sessionId}/select-currency`, {
|
|
148
182
|
method: "POST",
|
|
149
183
|
headers: {
|
|
@@ -172,12 +206,14 @@ export class T9nCheckout {
|
|
|
172
206
|
throw new Error(`T9N_SELECT_ERROR_${res.status}: ${message}`);
|
|
173
207
|
}
|
|
174
208
|
const payload = (await res.json());
|
|
209
|
+
const resolvedNetwork = payload.network || network;
|
|
210
|
+
const networkLabel = T9N_NETWORK_LABELS[resolvedNetwork] ?? resolvedNetwork;
|
|
175
211
|
this.modal?.setAddress(payload.depositAddress);
|
|
176
212
|
this.modal?.setQrPayload(payload.depositAddress);
|
|
177
|
-
this.modal?.setDisclaimer(`Send exactly ${payload.expectedAmountCrypto} ${payload.currency} to the address below. Sending the wrong amount or
|
|
213
|
+
this.modal?.setDisclaimer(`Send exactly ${payload.expectedAmountCrypto} ${payload.currency} on the ${networkLabel} network to the address below. Sending the wrong amount, asset, or network can result in permanent loss of funds.`);
|
|
178
214
|
this.cfg.hooks?.onCurrencySelected?.({
|
|
179
215
|
currency: payload.currency,
|
|
180
|
-
network,
|
|
216
|
+
network: resolvedNetwork,
|
|
181
217
|
depositAddress: payload.depositAddress,
|
|
182
218
|
expectedAmountCrypto: payload.expectedAmountCrypto,
|
|
183
219
|
});
|
|
@@ -322,7 +358,9 @@ export class T9nCheckout {
|
|
|
322
358
|
this.emitFailOnce({ sessionId: this.sessionId, error: "checkout failed" });
|
|
323
359
|
}
|
|
324
360
|
}
|
|
325
|
-
|
|
361
|
+
// pending_confirmation means the deposit was detected and is processing —
|
|
362
|
+
// do NOT fire onFail; just let the timer and next poll resolve it.
|
|
363
|
+
if (normalized === "awaiting_payment") {
|
|
326
364
|
if (this.hasAttemptedConfirm) {
|
|
327
365
|
this.modal?.showResult("failed", "No payment detected yet. Please try again.");
|
|
328
366
|
this.lastResultFailed = true;
|
|
@@ -393,6 +431,9 @@ export class T9nCheckout {
|
|
|
393
431
|
AVAX: "avax",
|
|
394
432
|
SOL: "solana",
|
|
395
433
|
TRX: "tron",
|
|
434
|
+
ARB: "arbitrum",
|
|
435
|
+
USDT: "tron", // TRC-20 default (lowest fees, widest adoption)
|
|
436
|
+
USDC: "ethereum", // ERC-20 default
|
|
396
437
|
LSK: "lisk",
|
|
397
438
|
FLOW: "flow",
|
|
398
439
|
RON: "ronin",
|
|
@@ -450,7 +491,7 @@ export class T9nCheckout {
|
|
|
450
491
|
break;
|
|
451
492
|
}
|
|
452
493
|
}
|
|
453
|
-
scheduleAutoClose(delayMs =
|
|
494
|
+
scheduleAutoClose(delayMs = 3500) {
|
|
454
495
|
this.skipCloseMark = true;
|
|
455
496
|
if (this.closeTimeoutId) {
|
|
456
497
|
window.clearTimeout(this.closeTimeoutId);
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
export declare const
|
|
3
|
-
|
|
2
|
+
export declare const SUPPORTED_CURRENCIES: readonly ["BTC", "ETH", "BNB", "MATIC", "AVAX", "SOL", "TRX", "ARB", "USDT", "USDC", "LSK", "FLOW", "RON", "SEI", "FTM", "HBAR"];
|
|
3
|
+
/** @deprecated Use SUPPORTED_CURRENCIES */
|
|
4
|
+
export declare const NATIVE_CURRENCIES: readonly ["BTC", "ETH", "BNB", "MATIC", "AVAX", "SOL", "TRX", "ARB", "USDT", "USDC", "LSK", "FLOW", "RON", "SEI", "FTM", "HBAR"];
|
|
5
|
+
export type NativeCurrency = (typeof SUPPORTED_CURRENCIES)[number];
|
|
4
6
|
export type SupportedCurrency = NativeCurrency;
|
|
5
7
|
export type T9nButtonTheme = "solid" | "light" | "outline";
|
|
6
8
|
export type T9nButtonOptions = {
|
package/dist/types.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
export const
|
|
2
|
+
export const SUPPORTED_CURRENCIES = [
|
|
3
3
|
"BTC",
|
|
4
4
|
"ETH",
|
|
5
5
|
"BNB",
|
|
@@ -7,6 +7,9 @@ export const NATIVE_CURRENCIES = [
|
|
|
7
7
|
"AVAX",
|
|
8
8
|
"SOL",
|
|
9
9
|
"TRX",
|
|
10
|
+
"ARB",
|
|
11
|
+
"USDT",
|
|
12
|
+
"USDC",
|
|
10
13
|
"LSK",
|
|
11
14
|
"FLOW",
|
|
12
15
|
"RON",
|
|
@@ -14,6 +17,8 @@ export const NATIVE_CURRENCIES = [
|
|
|
14
17
|
"FTM",
|
|
15
18
|
"HBAR",
|
|
16
19
|
];
|
|
20
|
+
/** @deprecated Use SUPPORTED_CURRENCIES */
|
|
21
|
+
export const NATIVE_CURRENCIES = SUPPORTED_CURRENCIES;
|
|
17
22
|
export const T9N_DEFAULT_API_BASE_URL = "https://slimepay-server.up.railway.app";
|
|
18
23
|
const trimmedString = () => z
|
|
19
24
|
.string()
|
|
@@ -26,10 +31,10 @@ export const checkoutConfigSchema = z.object({
|
|
|
26
31
|
amountNgn: z.number().finite().positive(),
|
|
27
32
|
currencies: z
|
|
28
33
|
.array(z.string().transform((v) => v.trim().toUpperCase()))
|
|
29
|
-
.min(1, { message: "At least one supported
|
|
30
|
-
.max(
|
|
34
|
+
.min(1, { message: "At least one supported currency is required" })
|
|
35
|
+
.max(16, { message: "Too many currencies provided" })
|
|
31
36
|
.superRefine((list, ctx) => {
|
|
32
|
-
const supported = new Set(
|
|
37
|
+
const supported = new Set(SUPPORTED_CURRENCIES);
|
|
33
38
|
const seen = new Set();
|
|
34
39
|
for (const [index, currency] of list.entries()) {
|
|
35
40
|
if (!supported.has(currency)) {
|
package/dist/ui/modal.d.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
export type CurrencyOption = {
|
|
2
|
+
symbol: string;
|
|
3
|
+
network: string;
|
|
4
|
+
networkLabel: string;
|
|
5
|
+
};
|
|
1
6
|
export type ModalHandlers = {
|
|
2
|
-
onSelectCurrency: (
|
|
7
|
+
onSelectCurrency: (symbol: string, network: string) => Promise<void>;
|
|
3
8
|
onConfirmPayment: () => Promise<void>;
|
|
4
9
|
onClose: () => void;
|
|
5
10
|
};
|
|
6
11
|
export declare class CheckoutModal {
|
|
7
|
-
private
|
|
12
|
+
private options;
|
|
8
13
|
private handlers;
|
|
9
14
|
private host;
|
|
10
15
|
private root;
|
|
@@ -28,7 +33,7 @@ export declare class CheckoutModal {
|
|
|
28
33
|
private chips;
|
|
29
34
|
private confirmLabel;
|
|
30
35
|
private selectedCurrency;
|
|
31
|
-
constructor(
|
|
36
|
+
constructor(options: CurrencyOption[], handlers: ModalHandlers);
|
|
32
37
|
private mount;
|
|
33
38
|
private handleCurrencySelect;
|
|
34
39
|
setConfirmLabel(label: string): void;
|
package/dist/ui/modal.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export class CheckoutModal {
|
|
2
|
-
constructor(
|
|
3
|
-
this.
|
|
2
|
+
constructor(options, handlers) {
|
|
3
|
+
this.options = options;
|
|
4
4
|
this.handlers = handlers;
|
|
5
5
|
this.chips = [];
|
|
6
6
|
this.confirmLabel = "I have made the payment";
|
|
@@ -94,19 +94,24 @@ export class CheckoutModal {
|
|
|
94
94
|
.subtitle { font-size: 14px; color: var(--sp-text-muted); margin: 4px 0 0; }
|
|
95
95
|
|
|
96
96
|
.grid-chips {
|
|
97
|
-
display:
|
|
97
|
+
display: flex; flex-direction: column; gap: 8px;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
.chip {
|
|
101
101
|
background: #fff; border: 1px solid var(--sp-border);
|
|
102
|
-
padding: 12px; border-radius: 16px;
|
|
103
|
-
display: flex; align-items: center; gap:
|
|
102
|
+
padding: 12px 14px; border-radius: 16px;
|
|
103
|
+
display: flex; align-items: center; gap: 12px;
|
|
104
104
|
cursor: pointer; transition: all 0.2s;
|
|
105
|
-
|
|
105
|
+
text-align: left; width: 100%;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
.chip:hover { border-color: var(--sp-primary); background: #f8faff; }
|
|
109
|
-
.chip img { width:
|
|
109
|
+
.chip img { width: 32px; height: 32px; border-radius: 50%; object-fit: cover; flex-shrink: 0; }
|
|
110
|
+
.chip-img-placeholder { width: 32px; height: 32px; border-radius: 50%; background: #e2e8f0; display: grid; place-items: center; font-size: 11px; font-weight: 800; flex-shrink: 0; }
|
|
111
|
+
.chip-text { flex: 1; min-width: 0; }
|
|
112
|
+
.chip-symbol { font-weight: 700; font-size: 14px; color: var(--sp-text); }
|
|
113
|
+
.chip-chain { font-size: 11px; color: var(--sp-text-muted); margin-top: 1px; }
|
|
114
|
+
.chip-arrow { color: #cbd5e1; flex-shrink: 0; }
|
|
110
115
|
|
|
111
116
|
.payment-area { display: none; flex-direction: column; align-items: center; text-align: center; }
|
|
112
117
|
.payment-area.active { display: flex; }
|
|
@@ -181,7 +186,6 @@ export class CheckoutModal {
|
|
|
181
186
|
|
|
182
187
|
@media (max-width: 480px) {
|
|
183
188
|
.modal { max-width: 100%; border-radius: 24px 24px 0 0; position: absolute; bottom: 0; max-height: 92vh; }
|
|
184
|
-
.grid-chips { grid-template-columns: 1fr; }
|
|
185
189
|
.content { max-height: calc(92vh - 90px); overflow-y: auto; }
|
|
186
190
|
}
|
|
187
191
|
`;
|
|
@@ -227,15 +231,22 @@ export class CheckoutModal {
|
|
|
227
231
|
`;
|
|
228
232
|
const chipsContainer = document.createElement("div");
|
|
229
233
|
chipsContainer.className = "grid-chips";
|
|
230
|
-
this.
|
|
234
|
+
this.options.forEach((opt) => {
|
|
231
235
|
const chip = document.createElement("button");
|
|
232
236
|
chip.className = "chip";
|
|
233
|
-
const iconUrl = COIN_IMAGES[
|
|
237
|
+
const iconUrl = COIN_IMAGES[opt.symbol] || "";
|
|
238
|
+
const imgHtml = iconUrl
|
|
239
|
+
? `<img src="${iconUrl}" alt="${opt.symbol}" />`
|
|
240
|
+
: `<div class="chip-img-placeholder">${opt.symbol[0]}</div>`;
|
|
234
241
|
chip.innerHTML = `
|
|
235
|
-
${
|
|
236
|
-
<
|
|
242
|
+
${imgHtml}
|
|
243
|
+
<div class="chip-text">
|
|
244
|
+
<div class="chip-symbol">${opt.symbol}</div>
|
|
245
|
+
${opt.networkLabel ? `<div class="chip-chain">${opt.networkLabel}</div>` : ""}
|
|
246
|
+
</div>
|
|
247
|
+
<svg class="chip-arrow" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="m9 18 6-6-6-6"/></svg>
|
|
237
248
|
`;
|
|
238
|
-
chip.onclick = () => this.handleCurrencySelect(
|
|
249
|
+
chip.onclick = () => this.handleCurrencySelect(opt.symbol, opt.network);
|
|
239
250
|
this.chips.push(chip);
|
|
240
251
|
chipsContainer.appendChild(chip);
|
|
241
252
|
});
|
|
@@ -302,14 +313,14 @@ export class CheckoutModal {
|
|
|
302
313
|
this.root.append(style, overlay);
|
|
303
314
|
document.body.appendChild(this.host);
|
|
304
315
|
}
|
|
305
|
-
async handleCurrencySelect(
|
|
306
|
-
this.selectedCurrency =
|
|
316
|
+
async handleCurrencySelect(symbol, network) {
|
|
317
|
+
this.selectedCurrency = symbol;
|
|
307
318
|
this.selectionSection.style.display = "none";
|
|
308
319
|
this.paymentSection.classList.add("active");
|
|
309
320
|
this.resultSection.classList.remove("visible");
|
|
310
321
|
this.qrEl.innerHTML = `<div class="spin" style="margin: 60px 0;">${loaderIconSvg()}</div><p style="font-size:12px;color:#64748b">Generating Address...</p>`;
|
|
311
322
|
try {
|
|
312
|
-
await this.handlers.onSelectCurrency(
|
|
323
|
+
await this.handlers.onSelectCurrency(symbol, network);
|
|
313
324
|
}
|
|
314
325
|
catch (e) {
|
|
315
326
|
this.resetSelection();
|