@tineon/t9n 0.1.4 → 0.1.5

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,7 +192,7 @@ 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());
@@ -202,11 +208,12 @@ export class T9nCheckout {
202
208
  if (payload.status === "settled") {
203
209
  this.modal?.showResult("success", "Payment received successfully.");
204
210
  this.lastResultFailed = false;
205
- this.cfg.hooks?.onSuccess?.({ sessionId: this.sessionId, status: payload.status });
211
+ this.emitSuccessOnce({ sessionId: this.sessionId, status: payload.status });
206
212
  }
207
213
  if (payload.status === "expired") {
208
214
  this.modal?.showResult("failed", "This session has expired.");
209
215
  this.lastResultFailed = true;
216
+ this.emitFailOnce({ sessionId: this.sessionId, error: "checkout expired" });
210
217
  }
211
218
  }
212
219
  startTimer() {
@@ -251,13 +258,13 @@ export class T9nCheckout {
251
258
  window.clearInterval(this.intervalId);
252
259
  if (this.statusPollId)
253
260
  window.clearInterval(this.statusPollId);
254
- this.cfg.hooks?.onSuccess?.({ sessionId: this.sessionId, status: payload.status });
261
+ this.emitSuccessOnce({ sessionId: this.sessionId, status: payload.status || "settled" });
255
262
  }
256
263
  if (normalized === "failed") {
257
264
  if (this.hasAttemptedConfirm) {
258
265
  this.modal?.showResult("failed", "No payment detected yet. Please try again.");
259
266
  this.lastResultFailed = true;
260
- this.cfg.hooks?.onFail?.({ sessionId: this.sessionId, error: "checkout failed" });
267
+ this.emitFailOnce({ sessionId: this.sessionId, error: "checkout failed" });
261
268
  }
262
269
  }
263
270
  }
@@ -268,14 +275,52 @@ export class T9nCheckout {
268
275
  window.clearInterval(this.intervalId);
269
276
  if (this.statusPollId)
270
277
  window.clearInterval(this.statusPollId);
278
+ this.emitFailOnce({ sessionId: this.sessionId, error: "checkout expired" });
271
279
  }
272
280
  }
273
281
  catch (_) {
274
282
  // keep modal alive even if one poll fails
275
- this.cfg.hooks?.onFail?.({ sessionId: this.sessionId || undefined, error: "session status poll failed" });
276
283
  }
277
284
  }, this.cfg.pollIntervalMs);
278
285
  }
286
+ async verifyTransaction(params) {
287
+ const sessionId = params?.sessionId?.trim();
288
+ const reference = params?.reference?.trim();
289
+ if (!sessionId && !reference) {
290
+ throw new Error("T9N: sessionId or reference is required for verification");
291
+ }
292
+ const search = new URLSearchParams();
293
+ if (sessionId)
294
+ search.set("sessionId", sessionId);
295
+ if (reference)
296
+ search.set("reference", reference);
297
+ const res = await this.fetchWithTimeout(`${this.getApiBaseUrl()}/api/merchant/checkout/verify?${search.toString()}`, {
298
+ method: "GET",
299
+ headers: {
300
+ "x-public-key": this.cfg.publicKey,
301
+ },
302
+ });
303
+ if (!res.ok) {
304
+ let message = "T9N: failed to verify transaction";
305
+ try {
306
+ const data = await res.json();
307
+ if (data?.message)
308
+ message = data.message;
309
+ }
310
+ catch (_) {
311
+ try {
312
+ const text = await res.text();
313
+ if (text)
314
+ message = text;
315
+ }
316
+ catch (_) {
317
+ // ignore
318
+ }
319
+ }
320
+ throw new Error(`T9N_VERIFY_ERROR_${res.status}: ${message}`);
321
+ }
322
+ return (await res.json());
323
+ }
279
324
  defaultNetworkFor(currency) {
280
325
  const map = {
281
326
  BTC: "bitcoin",
@@ -391,6 +436,18 @@ export class T9nCheckout {
391
436
  emitStatus(status) {
392
437
  this.cfg.hooks?.onStatusChange?.(status);
393
438
  }
439
+ emitSuccessOnce(payload) {
440
+ if (this.successNotified)
441
+ return;
442
+ this.successNotified = true;
443
+ this.cfg.hooks?.onSuccess?.(payload);
444
+ }
445
+ emitFailOnce(payload) {
446
+ if (this.successNotified || this.failureNotified)
447
+ return;
448
+ this.failureNotified = true;
449
+ this.cfg.hooks?.onFail?.(payload);
450
+ }
394
451
  getApiBaseUrl() {
395
452
  return this.cfg.apiBaseUrl || resolveDefaultApiBaseUrl();
396
453
  }
@@ -401,3 +458,58 @@ function resolveDefaultApiBaseUrl() {
401
458
  export function initializeT9n(config) {
402
459
  return new T9nCheckout(config);
403
460
  }
461
+ export async function verifyT9nTransaction(config, params) {
462
+ const publicKey = config.publicKey?.trim();
463
+ if (!publicKey || !/^pk_(live|test)_[a-zA-Z0-9_]+$/.test(publicKey)) {
464
+ throw new Error("T9N: publicKey must follow pk_live_* or pk_test_* format");
465
+ }
466
+ const sessionId = params?.sessionId?.trim();
467
+ const reference = params?.reference?.trim();
468
+ if (!sessionId && !reference) {
469
+ throw new Error("T9N: sessionId or reference is required for verification");
470
+ }
471
+ const apiBaseUrl = resolveDefaultApiBaseUrl().trim();
472
+ const parsedURL = new URL(apiBaseUrl);
473
+ if (parsedURL.protocol !== "https:" && parsedURL.hostname !== "localhost" && parsedURL.hostname !== "127.0.0.1") {
474
+ throw new Error("T9N: apiBaseUrl must use HTTPS");
475
+ }
476
+ const search = new URLSearchParams();
477
+ if (sessionId)
478
+ search.set("sessionId", sessionId);
479
+ if (reference)
480
+ search.set("reference", reference);
481
+ const controller = new AbortController();
482
+ const timeout = window.setTimeout(() => controller.abort(), config.requestTimeoutMs || 12000);
483
+ try {
484
+ const res = await fetch(`${apiBaseUrl}/api/merchant/checkout/verify?${search.toString()}`, {
485
+ method: "GET",
486
+ headers: {
487
+ "x-public-key": publicKey,
488
+ },
489
+ signal: controller.signal,
490
+ });
491
+ if (!res.ok) {
492
+ let message = "T9N: failed to verify transaction";
493
+ try {
494
+ const data = await res.json();
495
+ if (data?.message)
496
+ message = data.message;
497
+ }
498
+ catch (_) {
499
+ try {
500
+ const text = await res.text();
501
+ if (text)
502
+ message = text;
503
+ }
504
+ catch (_) {
505
+ // ignore
506
+ }
507
+ }
508
+ throw new Error(`T9N_VERIFY_ERROR_${res.status}: ${message}`);
509
+ }
510
+ return (await res.json());
511
+ }
512
+ finally {
513
+ window.clearTimeout(timeout);
514
+ }
515
+ }
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.5",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",