@tineon/t9n 0.1.9 → 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
  });
@@ -185,7 +221,6 @@ export class T9nCheckout {
185
221
  }
186
222
  async confirmPayment() {
187
223
  this.hasAttemptedConfirm = true;
188
- console.log("[T9N] confirmPayment: start", { sessionId: this.sessionId });
189
224
  this.modal?.setConfirmPending(true);
190
225
  try {
191
226
  const res = await this.fetchWithTimeout(`${this.getApiBaseUrl()}/api/merchant/checkout/sessions/${this.sessionId}/confirm-payment`, {
@@ -197,7 +232,6 @@ export class T9nCheckout {
197
232
  body: "{}",
198
233
  });
199
234
  if (!res.ok) {
200
- console.log("[T9N] confirmPayment: non-ok response", { status: res.status, sessionId: this.sessionId });
201
235
  this.modal?.setConfirmPending(false);
202
236
  this.modal?.showResult("failed", "Verification failed. Please try again.");
203
237
  this.lastResultFailed = true;
@@ -209,10 +243,8 @@ export class T9nCheckout {
209
243
  let payload = {};
210
244
  try {
211
245
  payload = (await res.json());
212
- console.log("[T9N] confirmPayment: response payload", { payload, sessionId: this.sessionId });
213
246
  }
214
247
  catch (_) {
215
- console.log("[T9N] confirmPayment: invalid json response", { sessionId: this.sessionId });
216
248
  this.modal?.setConfirmPending(false);
217
249
  this.modal?.showResult("failed", "Verification failed. Please try again.");
218
250
  this.lastResultFailed = true;
@@ -223,10 +255,8 @@ export class T9nCheckout {
223
255
  }
224
256
  this.modal?.setConfirmPending(false);
225
257
  const normalized = this.normalizeStatus(payload.status || "");
226
- console.log("[T9N] confirmPayment: normalized status", { status: payload.status, normalized, sessionId: this.sessionId });
227
258
  this.emitStatus(normalized);
228
259
  if (normalized === "settled") {
229
- console.log("[T9N] confirmPayment: showing success", { sessionId: this.sessionId });
230
260
  this.modal?.setConfirmLabel("I have made the payment");
231
261
  this.modal?.showResult("success", "Payment received successfully.");
232
262
  this.lastResultFailed = false;
@@ -234,7 +264,6 @@ export class T9nCheckout {
234
264
  return;
235
265
  }
236
266
  if (normalized === "expired") {
237
- console.log("[T9N] confirmPayment: showing expired", { sessionId: this.sessionId });
238
267
  this.modal?.showResult("failed", "This session has expired.");
239
268
  this.lastResultFailed = false;
240
269
  this.emitFailOnce({ sessionId: this.sessionId, error: "checkout expired" });
@@ -242,7 +271,6 @@ export class T9nCheckout {
242
271
  return;
243
272
  }
244
273
  if (normalized === "failed") {
245
- console.log("[T9N] confirmPayment: showing failed", { sessionId: this.sessionId });
246
274
  this.modal?.showResult("failed", "Payment failed. Please try again.");
247
275
  this.lastResultFailed = true;
248
276
  this.modal?.setConfirmLabel("Retry check");
@@ -250,21 +278,18 @@ export class T9nCheckout {
250
278
  return;
251
279
  }
252
280
  if (normalized === "closed") {
253
- console.log("[T9N] confirmPayment: showing closed", { sessionId: this.sessionId });
254
281
  this.modal?.showResult("failed", "Checkout was closed. Please try again.");
255
282
  this.lastResultFailed = true;
256
283
  this.modal?.setConfirmLabel("Retry check");
257
284
  this.emitFailOnce({ sessionId: this.sessionId || undefined, error: "checkout closed" });
258
285
  return;
259
286
  }
260
- console.log("[T9N] confirmPayment: showing no-funds retry", { sessionId: this.sessionId });
261
287
  this.modal?.showResult("failed", "No payment detected yet. Please try again.");
262
288
  this.lastResultFailed = true;
263
289
  this.modal?.setConfirmLabel("Retry check");
264
290
  this.emitFailOnce({ sessionId: this.sessionId || undefined, error: "payment not detected" });
265
291
  }
266
292
  catch (err) {
267
- console.log("[T9N] confirmPayment: error", { error: err?.message || err, sessionId: this.sessionId });
268
293
  this.modal?.setConfirmPending(false);
269
294
  this.modal?.showResult("failed", "Verification failed. Please try again.");
270
295
  this.lastResultFailed = true;
@@ -316,10 +341,8 @@ export class T9nCheckout {
316
341
  const payload = (await res.json());
317
342
  if (payload.status) {
318
343
  const normalized = this.normalizeStatus(payload.status);
319
- console.log("[T9N] poll: status", { status: payload.status, normalized, sessionId: this.sessionId });
320
344
  this.emitStatus(normalized);
321
345
  if (normalized === "settled") {
322
- console.log("[T9N] poll: showing success", { sessionId: this.sessionId });
323
346
  this.modal?.showResult("success", "Payment received successfully.");
324
347
  this.lastResultFailed = false;
325
348
  if (this.intervalId)
@@ -330,15 +353,15 @@ export class T9nCheckout {
330
353
  }
331
354
  if (normalized === "failed") {
332
355
  if (this.hasAttemptedConfirm) {
333
- console.log("[T9N] poll: showing failed", { sessionId: this.sessionId });
334
356
  this.modal?.showResult("failed", "No payment detected yet. Please try again.");
335
357
  this.lastResultFailed = true;
336
358
  this.emitFailOnce({ sessionId: this.sessionId, error: "checkout failed" });
337
359
  }
338
360
  }
339
- 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") {
340
364
  if (this.hasAttemptedConfirm) {
341
- console.log("[T9N] poll: showing pending no-funds", { sessionId: this.sessionId });
342
365
  this.modal?.showResult("failed", "No payment detected yet. Please try again.");
343
366
  this.lastResultFailed = true;
344
367
  this.emitFailOnce({ sessionId: this.sessionId || undefined, error: "payment not detected" });
@@ -346,7 +369,6 @@ export class T9nCheckout {
346
369
  }
347
370
  }
348
371
  if (payload.status === "expired") {
349
- console.log("[T9N] poll: showing expired", { sessionId: this.sessionId });
350
372
  this.modal?.showResult("failed", "This session has expired.");
351
373
  this.lastResultFailed = false;
352
374
  if (this.intervalId)
@@ -409,6 +431,9 @@ export class T9nCheckout {
409
431
  AVAX: "avax",
410
432
  SOL: "solana",
411
433
  TRX: "tron",
434
+ ARB: "arbitrum",
435
+ USDT: "tron", // TRC-20 default (lowest fees, widest adoption)
436
+ USDC: "ethereum", // ERC-20 default
412
437
  LSK: "lisk",
413
438
  FLOW: "flow",
414
439
  RON: "ronin",
@@ -466,7 +491,7 @@ export class T9nCheckout {
466
491
  break;
467
492
  }
468
493
  }
469
- scheduleAutoClose(delayMs = 1200) {
494
+ scheduleAutoClose(delayMs = 3500) {
470
495
  this.skipCloseMark = true;
471
496
  if (this.closeTimeoutId) {
472
497
  window.clearTimeout(this.closeTimeoutId);
@@ -533,14 +558,12 @@ export class T9nCheckout {
533
558
  if (this.successNotified)
534
559
  return;
535
560
  this.successNotified = true;
536
- console.log("[T9N] onSuccess", payload);
537
561
  this.cfg.hooks?.onSuccess?.(payload);
538
562
  }
539
563
  emitFailOnce(payload) {
540
564
  if (this.successNotified || this.failureNotified)
541
565
  return;
542
566
  this.failureNotified = true;
543
- console.log("[T9N] onFail", payload);
544
567
  this.cfg.hooks?.onFail?.(payload);
545
568
  }
546
569
  getApiBaseUrl() {
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.1.9",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",