@tineon/t9n 0.1.4 → 0.1.6

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/README.md CHANGED
@@ -26,7 +26,7 @@ npm i @tineon/t9n
26
26
  ## Quickstart
27
27
 
28
28
  ```ts
29
- import { initializeT9n } from "@tineon/t9n";
29
+ import { initializeT9n, verifyT9nTransaction } from "@tineon/t9n";
30
30
 
31
31
  const checkout = initializeT9n({
32
32
  publicKey: "pk_live_xxx",
@@ -41,11 +41,17 @@ const checkout = initializeT9n({
41
41
  },
42
42
  });
43
43
 
44
- checkout.mountButton("#pay-btn-slot", {
45
- text: "Pay with Crypto",
46
- theme: "solid",
47
- });
48
- ```
44
+ checkout.mountButton("#pay-btn-slot", {
45
+ text: "Pay with Crypto",
46
+ theme: "solid",
47
+ });
48
+
49
+ const verification = await verifyT9nTransaction(
50
+ { publicKey: "pk_live_xxx" },
51
+ { reference: "order-123" }
52
+ );
53
+ console.log("verified", verification.status);
54
+ ```
49
55
 
50
56
  ## Triggers
51
57
 
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type CheckoutConfig, type T9nButtonOptions } from "./types.js";
1
+ import { type CheckoutConfig, type T9nButtonOptions, type VerifyTransactionParams, type VerifyTransactionResult } from "./types.js";
2
2
  export declare class T9nCheckout {
3
3
  private cfg;
4
4
  private currencies;
@@ -10,6 +10,8 @@ export declare class T9nCheckout {
10
10
  private isOpen;
11
11
  private lastResultFailed;
12
12
  private hasAttemptedConfirm;
13
+ private successNotified;
14
+ private failureNotified;
13
15
  constructor(config: CheckoutConfig);
14
16
  createButton(options?: T9nButtonOptions): HTMLButtonElement;
15
17
  bindTrigger(target: string | HTMLElement, eventName?: keyof HTMLElementEventMap): () => void;
@@ -21,6 +23,7 @@ export declare class T9nCheckout {
21
23
  private confirmPayment;
22
24
  private startTimer;
23
25
  private startStatusPolling;
26
+ verifyTransaction(params: VerifyTransactionParams): Promise<VerifyTransactionResult>;
24
27
  private defaultNetworkFor;
25
28
  private fetchWithTimeout;
26
29
  private assertSecureConfig;
@@ -30,6 +33,9 @@ export declare class T9nCheckout {
30
33
  private markFailed;
31
34
  private markClosed;
32
35
  private emitStatus;
36
+ private emitSuccessOnce;
37
+ private emitFailOnce;
33
38
  private getApiBaseUrl;
34
39
  }
35
40
  export declare function initializeT9n(config: CheckoutConfig): T9nCheckout;
41
+ export declare function verifyT9nTransaction(config: Pick<CheckoutConfig, "publicKey" | "requestTimeoutMs">, params: VerifyTransactionParams): Promise<VerifyTransactionResult>;
package/dist/index.js CHANGED
@@ -8,6 +8,8 @@ export class T9nCheckout {
8
8
  this.isOpen = false;
9
9
  this.lastResultFailed = false;
10
10
  this.hasAttemptedConfirm = false;
11
+ this.successNotified = false;
12
+ this.failureNotified = false;
11
13
  this.cfg = checkoutConfigSchema.parse(config);
12
14
  this.cfg.apiBaseUrl = this.cfg.apiBaseUrl || resolveDefaultApiBaseUrl();
13
15
  this.currencies = this.cfg.currencies;
@@ -47,6 +49,8 @@ export class T9nCheckout {
47
49
  this.isOpen = true;
48
50
  this.lastResultFailed = false;
49
51
  this.hasAttemptedConfirm = false;
52
+ this.successNotified = false;
53
+ this.failureNotified = false;
50
54
  try {
51
55
  this.cfg.hooks?.onOpen?.();
52
56
  const session = await this.createSession();
@@ -84,6 +88,8 @@ export class T9nCheckout {
84
88
  this.sessionId = "";
85
89
  this.expiresAt = undefined;
86
90
  this.isOpen = false;
91
+ this.successNotified = false;
92
+ this.failureNotified = false;
87
93
  this.emitStatus("closed");
88
94
  this.cfg.hooks?.onClose?.();
89
95
  }
@@ -186,13 +192,17 @@ export class T9nCheckout {
186
192
  this.lastResultFailed = true;
187
193
  this.modal?.setConfirmLabel("Retry check");
188
194
  this.emitStatus("pending_confirmation");
189
- this.cfg.hooks?.onFail?.({ sessionId: this.sessionId || undefined, error: "confirm request failed" });
195
+ this.emitFailOnce({ sessionId: this.sessionId || undefined, error: "confirm request failed" });
190
196
  throw new Error("T9N: failed to confirm payment");
191
197
  }
192
198
  const payload = (await res.json());
193
199
  this.modal?.setConfirmPending(false);
194
- this.emitStatus(this.normalizeStatus(payload.status));
195
- if (payload.status === "pending" || payload.status === "processing") {
200
+ const normalized = this.normalizeStatus(payload.status);
201
+ this.emitStatus(normalized);
202
+ if (payload.status === "pending" ||
203
+ payload.status === "processing" ||
204
+ normalized === "pending_confirmation" ||
205
+ normalized === "awaiting_payment") {
196
206
  this.modal?.showResult("failed", "No payment detected yet. Please try again.");
197
207
  this.lastResultFailed = true;
198
208
  this.modal?.setConfirmLabel("Retry check");
@@ -202,11 +212,12 @@ export class T9nCheckout {
202
212
  if (payload.status === "settled") {
203
213
  this.modal?.showResult("success", "Payment received successfully.");
204
214
  this.lastResultFailed = false;
205
- this.cfg.hooks?.onSuccess?.({ sessionId: this.sessionId, status: payload.status });
215
+ this.emitSuccessOnce({ sessionId: this.sessionId, status: payload.status });
206
216
  }
207
217
  if (payload.status === "expired") {
208
218
  this.modal?.showResult("failed", "This session has expired.");
209
219
  this.lastResultFailed = true;
220
+ this.emitFailOnce({ sessionId: this.sessionId, error: "checkout expired" });
210
221
  }
211
222
  }
212
223
  startTimer() {
@@ -251,13 +262,19 @@ export class T9nCheckout {
251
262
  window.clearInterval(this.intervalId);
252
263
  if (this.statusPollId)
253
264
  window.clearInterval(this.statusPollId);
254
- this.cfg.hooks?.onSuccess?.({ sessionId: this.sessionId, status: payload.status });
265
+ this.emitSuccessOnce({ sessionId: this.sessionId, status: payload.status || "settled" });
255
266
  }
256
267
  if (normalized === "failed") {
257
268
  if (this.hasAttemptedConfirm) {
258
269
  this.modal?.showResult("failed", "No payment detected yet. Please try again.");
259
270
  this.lastResultFailed = true;
260
- this.cfg.hooks?.onFail?.({ sessionId: this.sessionId, error: "checkout failed" });
271
+ this.emitFailOnce({ sessionId: this.sessionId, error: "checkout failed" });
272
+ }
273
+ }
274
+ if (normalized === "pending_confirmation" || normalized === "awaiting_payment") {
275
+ if (this.hasAttemptedConfirm) {
276
+ this.modal?.showResult("failed", "No payment detected yet. Please try again.");
277
+ this.lastResultFailed = true;
261
278
  }
262
279
  }
263
280
  }
@@ -268,14 +285,52 @@ export class T9nCheckout {
268
285
  window.clearInterval(this.intervalId);
269
286
  if (this.statusPollId)
270
287
  window.clearInterval(this.statusPollId);
288
+ this.emitFailOnce({ sessionId: this.sessionId, error: "checkout expired" });
271
289
  }
272
290
  }
273
291
  catch (_) {
274
292
  // keep modal alive even if one poll fails
275
- this.cfg.hooks?.onFail?.({ sessionId: this.sessionId || undefined, error: "session status poll failed" });
276
293
  }
277
294
  }, this.cfg.pollIntervalMs);
278
295
  }
296
+ async verifyTransaction(params) {
297
+ const sessionId = params?.sessionId?.trim();
298
+ const reference = params?.reference?.trim();
299
+ if (!sessionId && !reference) {
300
+ throw new Error("T9N: sessionId or reference is required for verification");
301
+ }
302
+ const search = new URLSearchParams();
303
+ if (sessionId)
304
+ search.set("sessionId", sessionId);
305
+ if (reference)
306
+ search.set("reference", reference);
307
+ const res = await this.fetchWithTimeout(`${this.getApiBaseUrl()}/api/merchant/checkout/verify?${search.toString()}`, {
308
+ method: "GET",
309
+ headers: {
310
+ "x-public-key": this.cfg.publicKey,
311
+ },
312
+ });
313
+ if (!res.ok) {
314
+ let message = "T9N: failed to verify transaction";
315
+ try {
316
+ const data = await res.json();
317
+ if (data?.message)
318
+ message = data.message;
319
+ }
320
+ catch (_) {
321
+ try {
322
+ const text = await res.text();
323
+ if (text)
324
+ message = text;
325
+ }
326
+ catch (_) {
327
+ // ignore
328
+ }
329
+ }
330
+ throw new Error(`T9N_VERIFY_ERROR_${res.status}: ${message}`);
331
+ }
332
+ return (await res.json());
333
+ }
279
334
  defaultNetworkFor(currency) {
280
335
  const map = {
281
336
  BTC: "bitcoin",
@@ -391,6 +446,18 @@ export class T9nCheckout {
391
446
  emitStatus(status) {
392
447
  this.cfg.hooks?.onStatusChange?.(status);
393
448
  }
449
+ emitSuccessOnce(payload) {
450
+ if (this.successNotified)
451
+ return;
452
+ this.successNotified = true;
453
+ this.cfg.hooks?.onSuccess?.(payload);
454
+ }
455
+ emitFailOnce(payload) {
456
+ if (this.successNotified || this.failureNotified)
457
+ return;
458
+ this.failureNotified = true;
459
+ this.cfg.hooks?.onFail?.(payload);
460
+ }
394
461
  getApiBaseUrl() {
395
462
  return this.cfg.apiBaseUrl || resolveDefaultApiBaseUrl();
396
463
  }
@@ -401,3 +468,58 @@ function resolveDefaultApiBaseUrl() {
401
468
  export function initializeT9n(config) {
402
469
  return new T9nCheckout(config);
403
470
  }
471
+ export async function verifyT9nTransaction(config, params) {
472
+ const publicKey = config.publicKey?.trim();
473
+ if (!publicKey || !/^pk_(live|test)_[a-zA-Z0-9_]+$/.test(publicKey)) {
474
+ throw new Error("T9N: publicKey must follow pk_live_* or pk_test_* format");
475
+ }
476
+ const sessionId = params?.sessionId?.trim();
477
+ const reference = params?.reference?.trim();
478
+ if (!sessionId && !reference) {
479
+ throw new Error("T9N: sessionId or reference is required for verification");
480
+ }
481
+ const apiBaseUrl = resolveDefaultApiBaseUrl().trim();
482
+ const parsedURL = new URL(apiBaseUrl);
483
+ if (parsedURL.protocol !== "https:" && parsedURL.hostname !== "localhost" && parsedURL.hostname !== "127.0.0.1") {
484
+ throw new Error("T9N: apiBaseUrl must use HTTPS");
485
+ }
486
+ const search = new URLSearchParams();
487
+ if (sessionId)
488
+ search.set("sessionId", sessionId);
489
+ if (reference)
490
+ search.set("reference", reference);
491
+ const controller = new AbortController();
492
+ const timeout = window.setTimeout(() => controller.abort(), config.requestTimeoutMs || 12000);
493
+ try {
494
+ const res = await fetch(`${apiBaseUrl}/api/merchant/checkout/verify?${search.toString()}`, {
495
+ method: "GET",
496
+ headers: {
497
+ "x-public-key": publicKey,
498
+ },
499
+ signal: controller.signal,
500
+ });
501
+ if (!res.ok) {
502
+ let message = "T9N: failed to verify transaction";
503
+ try {
504
+ const data = await res.json();
505
+ if (data?.message)
506
+ message = data.message;
507
+ }
508
+ catch (_) {
509
+ try {
510
+ const text = await res.text();
511
+ if (text)
512
+ message = text;
513
+ }
514
+ catch (_) {
515
+ // ignore
516
+ }
517
+ }
518
+ throw new Error(`T9N_VERIFY_ERROR_${res.status}: ${message}`);
519
+ }
520
+ return (await res.json());
521
+ }
522
+ finally {
523
+ window.clearTimeout(timeout);
524
+ }
525
+ }
package/dist/types.d.ts CHANGED
@@ -33,6 +33,23 @@ export type T9nHooks = {
33
33
  error: string;
34
34
  }) => void;
35
35
  };
36
+ export type VerifyTransactionParams = {
37
+ sessionId?: string;
38
+ reference?: string;
39
+ };
40
+ export type VerifyTransactionResult = {
41
+ sessionId: string;
42
+ reference?: string;
43
+ status: string;
44
+ selectedCurrency?: string;
45
+ selectedNetwork?: string;
46
+ depositAddress?: string;
47
+ expectedAmountCrypto?: string;
48
+ expiresAt?: string;
49
+ createdAt?: string;
50
+ updatedAt?: string;
51
+ settledAt?: string;
52
+ };
36
53
  export declare const T9N_DEFAULT_API_BASE_URL = "https://slimepay-server.up.railway.app";
37
54
  export declare const checkoutConfigSchema: z.ZodObject<{
38
55
  publicKey: z.ZodEffects<z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>, string, string>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tineon/t9n",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",