@tineon/t9n 0.1.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/README.md +87 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +412 -0
- package/dist/security/integrity.d.ts +1 -0
- package/dist/security/integrity.js +8 -0
- package/dist/types.d.ts +159 -0
- package/dist/types.js +89 -0
- package/dist/ui/modal.d.ts +45 -0
- package/dist/ui/modal.js +502 -0
- package/package.json +23 -0
package/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# `@tineon/t9n`
|
|
2
|
+
|
|
3
|
+
Framework-agnostic checkout SDK for Tineon (T9N). Works in React, Vue, Angular, and plain JavaScript apps.
|
|
4
|
+
|
|
5
|
+
## Modal strategy
|
|
6
|
+
|
|
7
|
+
- Uses `Shadow DOM` (not iframe) for integration flexibility.
|
|
8
|
+
- Uses max overlay z-index (`2147483647`) to stay above host app UI.
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- Button trigger (`createButton` / `mountButton`)
|
|
13
|
+
- Non-button trigger support (`bindTrigger`)
|
|
14
|
+
- Button style overrides (`text`, `theme`, `className`, inline `style`)
|
|
15
|
+
- 15-minute checkout session flow
|
|
16
|
+
- Native-currency-only checkout enforcement
|
|
17
|
+
- Address generation + QR + confirmation flow
|
|
18
|
+
- Lifecycle hooks (`onOpen`, `onStatusChange`, `onSuccess`, `onFail`, etc.)
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm i @tineon/t9n
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quickstart
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { initializeT9n } from "@tineon/t9n";
|
|
30
|
+
|
|
31
|
+
const checkout = initializeT9n({
|
|
32
|
+
publicKey: "pk_live_xxx",
|
|
33
|
+
amountNgn: 50000,
|
|
34
|
+
currencies: ["BTC", "ETH", "TRX"],
|
|
35
|
+
customer: { email: "buyer@example.com", name: "Ada", reference: "order-123" },
|
|
36
|
+
metadata: { orderId: "order-123" },
|
|
37
|
+
callbackUrl: "https://merchant.example.com/payment/success",
|
|
38
|
+
hooks: {
|
|
39
|
+
onSuccess: ({ sessionId }) => console.log("paid", sessionId),
|
|
40
|
+
onFail: ({ error }) => console.error("checkout error", error),
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
checkout.mountButton("#pay-btn-slot", {
|
|
45
|
+
text: "Pay with Crypto",
|
|
46
|
+
theme: "solid",
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Triggers
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
checkout.open();
|
|
54
|
+
|
|
55
|
+
const detach = checkout.bindTrigger("#custom-card-pay", "click");
|
|
56
|
+
// later: detach()
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Runtime config
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
type CheckoutConfig = {
|
|
63
|
+
publicKey: string; // required, pk_live_*
|
|
64
|
+
amountNgn: number;
|
|
65
|
+
currencies: NativeCurrency[]; // required, native only, no duplicates
|
|
66
|
+
customer: { email: string; name?: string; reference?: string }; // email is required
|
|
67
|
+
metadata?: Record<string, unknown>;
|
|
68
|
+
callbackUrl?: string;
|
|
69
|
+
requestTimeoutMs?: number;
|
|
70
|
+
pollIntervalMs?: number;
|
|
71
|
+
hooks?: {
|
|
72
|
+
onOpen?: () => void;
|
|
73
|
+
onClose?: () => void;
|
|
74
|
+
onSessionCreated?: (session) => void;
|
|
75
|
+
onCurrencySelected?: (payload) => void;
|
|
76
|
+
onStatusChange?: (status) => void;
|
|
77
|
+
onSuccess?: (payload) => void;
|
|
78
|
+
onFail?: (payload) => void;
|
|
79
|
+
};
|
|
80
|
+
button?: {
|
|
81
|
+
text?: string;
|
|
82
|
+
className?: string;
|
|
83
|
+
theme?: "solid" | "light" | "outline";
|
|
84
|
+
style?: Record<string, string>;
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type CheckoutConfig, type T9nButtonOptions } from "./types.js";
|
|
2
|
+
export declare class T9nCheckout {
|
|
3
|
+
private cfg;
|
|
4
|
+
private currencies;
|
|
5
|
+
private modal?;
|
|
6
|
+
private sessionId;
|
|
7
|
+
private intervalId?;
|
|
8
|
+
private statusPollId?;
|
|
9
|
+
private expiresAt?;
|
|
10
|
+
private isOpen;
|
|
11
|
+
private lastResultFailed;
|
|
12
|
+
private hasAttemptedConfirm;
|
|
13
|
+
constructor(config: CheckoutConfig);
|
|
14
|
+
createButton(options?: T9nButtonOptions): HTMLButtonElement;
|
|
15
|
+
bindTrigger(target: string | HTMLElement, eventName?: keyof HTMLElementEventMap): () => void;
|
|
16
|
+
mountButton(target: string | HTMLElement, options?: T9nButtonOptions): HTMLButtonElement;
|
|
17
|
+
open(): Promise<void>;
|
|
18
|
+
close(): void;
|
|
19
|
+
private createSession;
|
|
20
|
+
private selectCurrency;
|
|
21
|
+
private confirmPayment;
|
|
22
|
+
private startTimer;
|
|
23
|
+
private startStatusPolling;
|
|
24
|
+
private defaultNetworkFor;
|
|
25
|
+
private fetchWithTimeout;
|
|
26
|
+
private assertSecureConfig;
|
|
27
|
+
private applyButtonStyle;
|
|
28
|
+
private applyButtonTheme;
|
|
29
|
+
private normalizeStatus;
|
|
30
|
+
private markFailed;
|
|
31
|
+
private markClosed;
|
|
32
|
+
private emitStatus;
|
|
33
|
+
private getApiBaseUrl;
|
|
34
|
+
}
|
|
35
|
+
export declare function initializeT9n(config: CheckoutConfig): T9nCheckout;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { checkoutConfigSchema, T9N_DEFAULT_API_BASE_URL, } from "./types.js";
|
|
2
|
+
import { CheckoutModal } from "./ui/modal.js";
|
|
3
|
+
const DEFAULT_BUTTON_TEXT = "Pay with T9N";
|
|
4
|
+
const DEFAULT_THEME = "solid";
|
|
5
|
+
export class T9nCheckout {
|
|
6
|
+
constructor(config) {
|
|
7
|
+
this.sessionId = "";
|
|
8
|
+
this.isOpen = false;
|
|
9
|
+
this.lastResultFailed = false;
|
|
10
|
+
this.hasAttemptedConfirm = false;
|
|
11
|
+
this.cfg = checkoutConfigSchema.parse(config);
|
|
12
|
+
this.cfg.apiBaseUrl = this.cfg.apiBaseUrl || resolveDefaultApiBaseUrl();
|
|
13
|
+
this.currencies = this.cfg.currencies;
|
|
14
|
+
this.assertSecureConfig();
|
|
15
|
+
}
|
|
16
|
+
createButton(options) {
|
|
17
|
+
const btn = document.createElement("button");
|
|
18
|
+
btn.type = "button";
|
|
19
|
+
btn.textContent = options?.text || this.cfg.button?.text || DEFAULT_BUTTON_TEXT;
|
|
20
|
+
btn.className = options?.className || this.cfg.button?.className || "";
|
|
21
|
+
this.applyButtonTheme(btn, options?.theme || this.cfg.button?.theme || DEFAULT_THEME);
|
|
22
|
+
this.applyButtonStyle(btn, this.cfg.button?.style);
|
|
23
|
+
this.applyButtonStyle(btn, options?.style);
|
|
24
|
+
btn.onclick = async () => this.open();
|
|
25
|
+
return btn;
|
|
26
|
+
}
|
|
27
|
+
bindTrigger(target, eventName = "click") {
|
|
28
|
+
const element = typeof target === "string" ? document.querySelector(target) : target;
|
|
29
|
+
if (!element)
|
|
30
|
+
throw new Error("T9N: trigger target not found");
|
|
31
|
+
const handler = () => this.open();
|
|
32
|
+
element.addEventListener(eventName, handler);
|
|
33
|
+
return () => element.removeEventListener(eventName, handler);
|
|
34
|
+
}
|
|
35
|
+
mountButton(target, options) {
|
|
36
|
+
const container = typeof target === "string" ? document.querySelector(target) : target;
|
|
37
|
+
if (!container) {
|
|
38
|
+
throw new Error("T9N: target container not found for checkout button");
|
|
39
|
+
}
|
|
40
|
+
const btn = this.createButton(options);
|
|
41
|
+
container.appendChild(btn);
|
|
42
|
+
return btn;
|
|
43
|
+
}
|
|
44
|
+
async open() {
|
|
45
|
+
if (this.isOpen)
|
|
46
|
+
return;
|
|
47
|
+
this.isOpen = true;
|
|
48
|
+
this.lastResultFailed = false;
|
|
49
|
+
this.hasAttemptedConfirm = false;
|
|
50
|
+
try {
|
|
51
|
+
this.cfg.hooks?.onOpen?.();
|
|
52
|
+
const session = await this.createSession();
|
|
53
|
+
this.sessionId = session.sessionId;
|
|
54
|
+
this.expiresAt = new Date(session.expiresAt);
|
|
55
|
+
this.cfg.hooks?.onSessionCreated?.(session);
|
|
56
|
+
this.emitStatus("created");
|
|
57
|
+
this.modal = new CheckoutModal(this.currencies, {
|
|
58
|
+
onSelectCurrency: async (currency) => this.selectCurrency(currency),
|
|
59
|
+
onConfirmPayment: async () => this.confirmPayment(),
|
|
60
|
+
onClose: () => this.close(),
|
|
61
|
+
});
|
|
62
|
+
this.startTimer();
|
|
63
|
+
this.startStatusPolling();
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
this.isOpen = false;
|
|
67
|
+
this.cfg.hooks?.onFail?.({ sessionId: this.sessionId || undefined, error: error?.message || "open failed" });
|
|
68
|
+
throw new Error(error?.message || "T9N: failed to open checkout");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
close() {
|
|
72
|
+
if (this.intervalId)
|
|
73
|
+
window.clearInterval(this.intervalId);
|
|
74
|
+
if (this.statusPollId)
|
|
75
|
+
window.clearInterval(this.statusPollId);
|
|
76
|
+
if (this.lastResultFailed && this.sessionId) {
|
|
77
|
+
void this.markFailed();
|
|
78
|
+
}
|
|
79
|
+
else if (!this.hasAttemptedConfirm && this.sessionId) {
|
|
80
|
+
void this.markClosed();
|
|
81
|
+
}
|
|
82
|
+
this.modal?.close();
|
|
83
|
+
this.modal = undefined;
|
|
84
|
+
this.sessionId = "";
|
|
85
|
+
this.expiresAt = undefined;
|
|
86
|
+
this.isOpen = false;
|
|
87
|
+
this.emitStatus("closed");
|
|
88
|
+
this.cfg.hooks?.onClose?.();
|
|
89
|
+
}
|
|
90
|
+
async createSession() {
|
|
91
|
+
const res = await this.fetchWithTimeout(`${this.getApiBaseUrl()}/api/merchant/checkout/public/sessions`, {
|
|
92
|
+
method: "POST",
|
|
93
|
+
headers: {
|
|
94
|
+
"Content-Type": "application/json",
|
|
95
|
+
"x-public-key": this.cfg.publicKey,
|
|
96
|
+
},
|
|
97
|
+
body: JSON.stringify({
|
|
98
|
+
amountNgn: this.cfg.amountNgn,
|
|
99
|
+
supportedNativeCurrencies: this.cfg.currencies,
|
|
100
|
+
customer: this.cfg.customer,
|
|
101
|
+
metadata: this.cfg.metadata,
|
|
102
|
+
}),
|
|
103
|
+
});
|
|
104
|
+
if (!res.ok) {
|
|
105
|
+
let message = "T9N: failed to create checkout session";
|
|
106
|
+
try {
|
|
107
|
+
const data = await res.json();
|
|
108
|
+
if (data?.message)
|
|
109
|
+
message = data.message;
|
|
110
|
+
}
|
|
111
|
+
catch (_) {
|
|
112
|
+
try {
|
|
113
|
+
const text = await res.text();
|
|
114
|
+
if (text)
|
|
115
|
+
message = text;
|
|
116
|
+
}
|
|
117
|
+
catch (_) {
|
|
118
|
+
// ignore
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (res.status === 429) {
|
|
122
|
+
throw new Error(`T9N_LIMIT_EXCEEDED: ${message}`);
|
|
123
|
+
}
|
|
124
|
+
if (res.status === 403) {
|
|
125
|
+
throw new Error(`T9N_FORBIDDEN: ${message}`);
|
|
126
|
+
}
|
|
127
|
+
throw new Error(`T9N_ERROR_${res.status}: ${message}`);
|
|
128
|
+
}
|
|
129
|
+
return (await res.json());
|
|
130
|
+
}
|
|
131
|
+
async selectCurrency(currency) {
|
|
132
|
+
const network = this.defaultNetworkFor(currency);
|
|
133
|
+
const res = await this.fetchWithTimeout(`${this.getApiBaseUrl()}/api/merchant/checkout/sessions/${this.sessionId}/select-currency`, {
|
|
134
|
+
method: "POST",
|
|
135
|
+
headers: {
|
|
136
|
+
"Content-Type": "application/json",
|
|
137
|
+
"x-public-key": this.cfg.publicKey,
|
|
138
|
+
},
|
|
139
|
+
body: JSON.stringify({ currency, network }),
|
|
140
|
+
});
|
|
141
|
+
if (!res.ok) {
|
|
142
|
+
let message = "T9N: failed to select currency";
|
|
143
|
+
try {
|
|
144
|
+
const data = await res.json();
|
|
145
|
+
if (data?.message)
|
|
146
|
+
message = data.message;
|
|
147
|
+
}
|
|
148
|
+
catch (_) {
|
|
149
|
+
try {
|
|
150
|
+
const text = await res.text();
|
|
151
|
+
if (text)
|
|
152
|
+
message = text;
|
|
153
|
+
}
|
|
154
|
+
catch (_) {
|
|
155
|
+
// ignore
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
throw new Error(`T9N_SELECT_ERROR_${res.status}: ${message}`);
|
|
159
|
+
}
|
|
160
|
+
const payload = (await res.json());
|
|
161
|
+
this.modal?.setAddress(payload.depositAddress);
|
|
162
|
+
this.modal?.setQrPayload(payload.depositAddress);
|
|
163
|
+
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.`);
|
|
164
|
+
this.cfg.hooks?.onCurrencySelected?.({
|
|
165
|
+
currency: payload.currency,
|
|
166
|
+
network,
|
|
167
|
+
depositAddress: payload.depositAddress,
|
|
168
|
+
expectedAmountCrypto: payload.expectedAmountCrypto,
|
|
169
|
+
});
|
|
170
|
+
this.emitStatus("awaiting_payment");
|
|
171
|
+
}
|
|
172
|
+
async confirmPayment() {
|
|
173
|
+
this.hasAttemptedConfirm = true;
|
|
174
|
+
this.modal?.setConfirmPending(true);
|
|
175
|
+
const res = await this.fetchWithTimeout(`${this.getApiBaseUrl()}/api/merchant/checkout/sessions/${this.sessionId}/confirm-payment`, {
|
|
176
|
+
method: "POST",
|
|
177
|
+
headers: {
|
|
178
|
+
"Content-Type": "application/json",
|
|
179
|
+
"x-public-key": this.cfg.publicKey,
|
|
180
|
+
},
|
|
181
|
+
body: "{}",
|
|
182
|
+
});
|
|
183
|
+
if (!res.ok) {
|
|
184
|
+
this.modal?.setConfirmPending(false);
|
|
185
|
+
this.modal?.showResult("failed", "No payment detected yet. Please try again.");
|
|
186
|
+
this.lastResultFailed = true;
|
|
187
|
+
this.modal?.setConfirmLabel("Retry check");
|
|
188
|
+
this.emitStatus("pending_confirmation");
|
|
189
|
+
this.cfg.hooks?.onFail?.({ sessionId: this.sessionId || undefined, error: "confirm request failed" });
|
|
190
|
+
throw new Error("T9N: failed to confirm payment");
|
|
191
|
+
}
|
|
192
|
+
const payload = (await res.json());
|
|
193
|
+
this.modal?.setConfirmPending(false);
|
|
194
|
+
this.emitStatus(this.normalizeStatus(payload.status));
|
|
195
|
+
if (payload.status === "pending" || payload.status === "processing") {
|
|
196
|
+
this.modal?.showResult("failed", "No payment detected yet. Please try again.");
|
|
197
|
+
this.lastResultFailed = true;
|
|
198
|
+
this.modal?.setConfirmLabel("Retry check");
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
this.modal?.setConfirmLabel("I have made the payment");
|
|
202
|
+
if (payload.status === "settled") {
|
|
203
|
+
this.modal?.showResult("success", "Payment received successfully.");
|
|
204
|
+
this.lastResultFailed = false;
|
|
205
|
+
this.cfg.hooks?.onSuccess?.({ sessionId: this.sessionId, status: payload.status });
|
|
206
|
+
}
|
|
207
|
+
if (payload.status === "expired") {
|
|
208
|
+
this.modal?.showResult("failed", "This session has expired.");
|
|
209
|
+
this.lastResultFailed = true;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
startTimer() {
|
|
213
|
+
this.intervalId = window.setInterval(() => {
|
|
214
|
+
if (!this.expiresAt)
|
|
215
|
+
return;
|
|
216
|
+
const ms = this.expiresAt.getTime() - Date.now();
|
|
217
|
+
if (ms <= 0) {
|
|
218
|
+
this.modal?.setTimer("00:00");
|
|
219
|
+
this.modal?.setStatus("Session expired");
|
|
220
|
+
this.close();
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const minutes = Math.floor(ms / 60000)
|
|
224
|
+
.toString()
|
|
225
|
+
.padStart(2, "0");
|
|
226
|
+
const seconds = Math.floor((ms % 60000) / 1000)
|
|
227
|
+
.toString()
|
|
228
|
+
.padStart(2, "0");
|
|
229
|
+
this.modal?.setTimer(`${minutes}:${seconds}`);
|
|
230
|
+
}, 1000);
|
|
231
|
+
}
|
|
232
|
+
startStatusPolling() {
|
|
233
|
+
this.statusPollId = window.setInterval(async () => {
|
|
234
|
+
if (!this.sessionId)
|
|
235
|
+
return;
|
|
236
|
+
try {
|
|
237
|
+
const res = await this.fetchWithTimeout(`${this.getApiBaseUrl()}/api/merchant/checkout/sessions/${this.sessionId}`, {
|
|
238
|
+
method: "GET",
|
|
239
|
+
headers: { "x-public-key": this.cfg.publicKey },
|
|
240
|
+
});
|
|
241
|
+
if (!res.ok)
|
|
242
|
+
return;
|
|
243
|
+
const payload = (await res.json());
|
|
244
|
+
if (payload.status) {
|
|
245
|
+
const normalized = this.normalizeStatus(payload.status);
|
|
246
|
+
this.emitStatus(normalized);
|
|
247
|
+
if (normalized === "settled") {
|
|
248
|
+
this.modal?.showResult("success", "Payment received successfully.");
|
|
249
|
+
this.lastResultFailed = false;
|
|
250
|
+
if (this.intervalId)
|
|
251
|
+
window.clearInterval(this.intervalId);
|
|
252
|
+
if (this.statusPollId)
|
|
253
|
+
window.clearInterval(this.statusPollId);
|
|
254
|
+
this.cfg.hooks?.onSuccess?.({ sessionId: this.sessionId, status: payload.status });
|
|
255
|
+
}
|
|
256
|
+
if (normalized === "failed") {
|
|
257
|
+
if (this.hasAttemptedConfirm) {
|
|
258
|
+
this.modal?.showResult("failed", "No payment detected yet. Please try again.");
|
|
259
|
+
this.lastResultFailed = true;
|
|
260
|
+
this.cfg.hooks?.onFail?.({ sessionId: this.sessionId, error: "checkout failed" });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (payload.status === "expired") {
|
|
265
|
+
this.modal?.showResult("failed", "This session has expired.");
|
|
266
|
+
this.lastResultFailed = true;
|
|
267
|
+
if (this.intervalId)
|
|
268
|
+
window.clearInterval(this.intervalId);
|
|
269
|
+
if (this.statusPollId)
|
|
270
|
+
window.clearInterval(this.statusPollId);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
catch (_) {
|
|
274
|
+
// keep modal alive even if one poll fails
|
|
275
|
+
this.cfg.hooks?.onFail?.({ sessionId: this.sessionId || undefined, error: "session status poll failed" });
|
|
276
|
+
}
|
|
277
|
+
}, this.cfg.pollIntervalMs);
|
|
278
|
+
}
|
|
279
|
+
defaultNetworkFor(currency) {
|
|
280
|
+
const map = {
|
|
281
|
+
BTC: "bitcoin",
|
|
282
|
+
ETH: "ethereum",
|
|
283
|
+
BNB: "bsc",
|
|
284
|
+
MATIC: "polygon",
|
|
285
|
+
AVAX: "avax",
|
|
286
|
+
SOL: "solana",
|
|
287
|
+
TRX: "tron",
|
|
288
|
+
LSK: "lisk",
|
|
289
|
+
FLOW: "flow",
|
|
290
|
+
RON: "ronin",
|
|
291
|
+
SEI: "sei",
|
|
292
|
+
FTM: "fantom",
|
|
293
|
+
HBAR: "hedera",
|
|
294
|
+
};
|
|
295
|
+
return map[currency] || "ethereum";
|
|
296
|
+
}
|
|
297
|
+
async fetchWithTimeout(input, init) {
|
|
298
|
+
const controller = new AbortController();
|
|
299
|
+
const timeout = window.setTimeout(() => controller.abort(), this.cfg.requestTimeoutMs);
|
|
300
|
+
try {
|
|
301
|
+
return await fetch(input, { ...init, signal: controller.signal });
|
|
302
|
+
}
|
|
303
|
+
finally {
|
|
304
|
+
window.clearTimeout(timeout);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
assertSecureConfig() {
|
|
308
|
+
const apiURL = new URL(this.getApiBaseUrl());
|
|
309
|
+
const isLocalHost = apiURL.hostname === "localhost" || apiURL.hostname === "127.0.0.1";
|
|
310
|
+
if (!isLocalHost && apiURL.protocol !== "https:") {
|
|
311
|
+
throw new Error("T9N: apiBaseUrl must use HTTPS outside local development.");
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
applyButtonStyle(btn, style) {
|
|
315
|
+
if (!style)
|
|
316
|
+
return;
|
|
317
|
+
Object.entries(style).forEach(([key, value]) => {
|
|
318
|
+
btn.style.setProperty(key, value);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
applyButtonTheme(btn, theme) {
|
|
322
|
+
btn.style.borderRadius = "10px";
|
|
323
|
+
btn.style.padding = "12px 18px";
|
|
324
|
+
btn.style.fontSize = "14px";
|
|
325
|
+
btn.style.fontWeight = "700";
|
|
326
|
+
btn.style.cursor = "pointer";
|
|
327
|
+
btn.style.transition = "all .18s ease";
|
|
328
|
+
switch (theme) {
|
|
329
|
+
case "light":
|
|
330
|
+
btn.style.border = "1px solid #dbe4f0";
|
|
331
|
+
btn.style.background = "#ffffff";
|
|
332
|
+
btn.style.color = "#0f172a";
|
|
333
|
+
break;
|
|
334
|
+
case "outline":
|
|
335
|
+
btn.style.border = "1px solid #0f172a";
|
|
336
|
+
btn.style.background = "transparent";
|
|
337
|
+
btn.style.color = "#0f172a";
|
|
338
|
+
break;
|
|
339
|
+
default:
|
|
340
|
+
btn.style.border = "1px solid #0f172a";
|
|
341
|
+
btn.style.background = "#0f172a";
|
|
342
|
+
btn.style.color = "#ffffff";
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
normalizeStatus(value) {
|
|
347
|
+
switch (value) {
|
|
348
|
+
case "created":
|
|
349
|
+
case "address_generated":
|
|
350
|
+
return "awaiting_payment";
|
|
351
|
+
case "awaiting_payment":
|
|
352
|
+
case "pending_confirmation":
|
|
353
|
+
case "closed":
|
|
354
|
+
case "expired":
|
|
355
|
+
case "settled":
|
|
356
|
+
case "failed":
|
|
357
|
+
return value;
|
|
358
|
+
default:
|
|
359
|
+
return "failed";
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
async markFailed() {
|
|
363
|
+
try {
|
|
364
|
+
await this.fetchWithTimeout(`${this.getApiBaseUrl()}/api/merchant/checkout/sessions/${this.sessionId}/fail`, {
|
|
365
|
+
method: "POST",
|
|
366
|
+
headers: {
|
|
367
|
+
"Content-Type": "application/json",
|
|
368
|
+
"x-public-key": this.cfg.publicKey,
|
|
369
|
+
},
|
|
370
|
+
body: "{}",
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
catch (_) {
|
|
374
|
+
// ignore best-effort failure mark
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
async markClosed() {
|
|
378
|
+
try {
|
|
379
|
+
await this.fetchWithTimeout(`${this.getApiBaseUrl()}/api/merchant/checkout/sessions/${this.sessionId}/close`, {
|
|
380
|
+
method: "POST",
|
|
381
|
+
headers: {
|
|
382
|
+
"Content-Type": "application/json",
|
|
383
|
+
"x-public-key": this.cfg.publicKey,
|
|
384
|
+
},
|
|
385
|
+
body: "{}",
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
catch (_) {
|
|
389
|
+
// ignore best-effort close mark
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
emitStatus(status) {
|
|
393
|
+
this.cfg.hooks?.onStatusChange?.(status);
|
|
394
|
+
}
|
|
395
|
+
getApiBaseUrl() {
|
|
396
|
+
return this.cfg.apiBaseUrl || resolveDefaultApiBaseUrl();
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
function resolveDefaultApiBaseUrl() {
|
|
400
|
+
if (typeof window === "undefined") {
|
|
401
|
+
return T9N_DEFAULT_API_BASE_URL;
|
|
402
|
+
}
|
|
403
|
+
const host = window.location.hostname.toLowerCase();
|
|
404
|
+
const isLocal = host === "localhost" || host === "127.0.0.1" || host === "::1";
|
|
405
|
+
if (isLocal) {
|
|
406
|
+
return "http://localhost:8090";
|
|
407
|
+
}
|
|
408
|
+
return T9N_DEFAULT_API_BASE_URL;
|
|
409
|
+
}
|
|
410
|
+
export function initializeT9n(config) {
|
|
411
|
+
return new T9nCheckout(config);
|
|
412
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function sha256Hex(input: string): Promise<string>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export async function sha256Hex(input) {
|
|
2
|
+
const enc = new TextEncoder();
|
|
3
|
+
const data = enc.encode(input);
|
|
4
|
+
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
5
|
+
return Array.from(new Uint8Array(hash))
|
|
6
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
7
|
+
.join("");
|
|
8
|
+
}
|