@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 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
- this.modal = new CheckoutModal(this.currencies, {
63
- onSelectCurrency: async (currency) => this.selectCurrency(currency),
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 asset can result in loss of funds.`);
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
- if (normalized === "pending_confirmation" || normalized === "awaiting_payment") {
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 = 1200) {
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 NATIVE_CURRENCIES: readonly ["BTC", "ETH", "BNB", "MATIC", "AVAX", "SOL", "TRX", "LSK", "FLOW", "RON", "SEI", "FTM", "HBAR"];
3
- export type NativeCurrency = (typeof NATIVE_CURRENCIES)[number];
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 NATIVE_CURRENCIES = [
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 native currency is required" })
30
- .max(13, { message: "Too many currencies provided" })
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(NATIVE_CURRENCIES);
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)) {
@@ -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: (currency: string) => Promise<void>;
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 currencies;
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(currencies: string[], handlers: ModalHandlers);
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(currencies, handlers) {
3
- this.currencies = currencies;
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: grid; grid-template-columns: repeat(2, 1fr); gap: 10px;
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: 10px;
102
+ padding: 12px 14px; border-radius: 16px;
103
+ display: flex; align-items: center; gap: 12px;
104
104
  cursor: pointer; transition: all 0.2s;
105
- font-weight: 600; font-size: 14px;
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: 24px; height: 24px; border-radius: 6px; }
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.currencies.forEach((currency) => {
234
+ this.options.forEach((opt) => {
231
235
  const chip = document.createElement("button");
232
236
  chip.className = "chip";
233
- const iconUrl = COIN_IMAGES[currency] || "";
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
- ${iconUrl ? `<img src="${iconUrl}" />` : `<div style="width:24px;height:24px;background:#eee;border-radius:4px"></div>`}
236
- <span>${currency}</span>
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(currency);
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(currency) {
306
- this.selectedCurrency = currency;
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(currency);
323
+ await this.handlers.onSelectCurrency(symbol, network);
313
324
  }
314
325
  catch (e) {
315
326
  this.resetSelection();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tineon/t9n",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",