@gbitx/pay-react-native 0.1.0
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/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +247 -0
- package/SECURITY.md +28 -0
- package/dist/index.cjs +693 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.mts +98 -0
- package/dist/index.d.ts +98 -0
- package/dist/index.mjs +685 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +79 -0
- package/src/CheckoutModal.tsx +334 -0
- package/src/GbitXPay.ts +16 -0
- package/src/GbitXPayProvider.tsx +29 -0
- package/src/config.ts +219 -0
- package/src/controller.ts +100 -0
- package/src/errors.ts +89 -0
- package/src/index.ts +33 -0
- package/src/messageHandler.ts +124 -0
- package/src/redact.ts +58 -0
- package/src/types.ts +99 -0
- package/src/useGbitXPay.ts +16 -0
- package/src/validation.ts +78 -0
- package/src/version.ts +4 -0
- package/src/webviewPolicy.ts +55 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,693 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var reactNative = require('react-native');
|
|
5
|
+
var reactNativeWebview = require('react-native-webview');
|
|
6
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
7
|
+
|
|
8
|
+
// src/errors.ts
|
|
9
|
+
var GbitXPayError = class _GbitXPayError extends Error {
|
|
10
|
+
constructor(code, message, init) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "GbitXPayError";
|
|
13
|
+
this.code = code;
|
|
14
|
+
const meta = {};
|
|
15
|
+
if (init == null ? void 0 : init.onboardingStatus) meta.onboardingStatus = init.onboardingStatus;
|
|
16
|
+
if (typeof (init == null ? void 0 : init.retryAfterSec) === "number") meta.retryAfterSec = init.retryAfterSec;
|
|
17
|
+
this.meta = Object.keys(meta).length ? meta : void 0;
|
|
18
|
+
if (init && "cause" in init) {
|
|
19
|
+
Object.defineProperty(this, "cause", { value: init.cause, enumerable: false, writable: false, configurable: true });
|
|
20
|
+
}
|
|
21
|
+
Object.setPrototypeOf(this, _GbitXPayError.prototype);
|
|
22
|
+
}
|
|
23
|
+
// Custom serialization so a crash reporter or JSON.stringify() can never
|
|
24
|
+
// leak `cause` (which might hold the checkout URL with #t=<clientToken>).
|
|
25
|
+
// Only the safe, non-sensitive fields are emitted.
|
|
26
|
+
toJSON() {
|
|
27
|
+
var _a, _b;
|
|
28
|
+
const out = {
|
|
29
|
+
name: this.name,
|
|
30
|
+
code: this.code,
|
|
31
|
+
message: this.message
|
|
32
|
+
};
|
|
33
|
+
if ((_a = this.meta) == null ? void 0 : _a.onboardingStatus) out.onboardingStatus = this.meta.onboardingStatus;
|
|
34
|
+
if (typeof ((_b = this.meta) == null ? void 0 : _b.retryAfterSec) === "number") out.retryAfterSec = this.meta.retryAfterSec;
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
function isGbitXPayError(e) {
|
|
39
|
+
return e instanceof GbitXPayError || typeof e === "object" && e !== null && e.name === "GbitXPayError";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/validation.ts
|
|
43
|
+
var PUBLISHABLE_KEY_RE = /^pk_(live|test)_[0-9a-f]{64}$/;
|
|
44
|
+
var PAYMENT_ID_RE = /^[A-Za-z0-9_-]{8,64}$/;
|
|
45
|
+
var CLIENT_TOKEN_RE = /^[A-Za-z0-9]{16,128}$/;
|
|
46
|
+
function assertPublishableKey(publishableKey) {
|
|
47
|
+
if (typeof publishableKey !== "string" || publishableKey.trim() === "") {
|
|
48
|
+
throw new GbitXPayError("invalid_key", "A publishableKey string is required.");
|
|
49
|
+
}
|
|
50
|
+
const key = publishableKey.trim();
|
|
51
|
+
if (key.startsWith("gk_")) {
|
|
52
|
+
throw new GbitXPayError(
|
|
53
|
+
"secret_key_used",
|
|
54
|
+
"A SECRET key (gk_) was passed to the SDK. Use a publishable key (pk_) only, and remove the secret key from your app immediately."
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
if (!PUBLISHABLE_KEY_RE.test(key)) {
|
|
58
|
+
throw new GbitXPayError("invalid_key", "publishableKey must look like pk_live_\u2026 or pk_test_\u2026.");
|
|
59
|
+
}
|
|
60
|
+
const environment = key.startsWith("pk_live_") ? "live" : "test";
|
|
61
|
+
return { key, environment };
|
|
62
|
+
}
|
|
63
|
+
function assertPaymentArgs(paymentId, clientToken) {
|
|
64
|
+
const id = typeof paymentId === "string" ? paymentId.trim() : "";
|
|
65
|
+
const token = typeof clientToken === "string" ? clientToken.trim() : "";
|
|
66
|
+
if (!PAYMENT_ID_RE.test(id)) {
|
|
67
|
+
throw new GbitXPayError("invalid_arguments", "paymentId is missing or malformed.");
|
|
68
|
+
}
|
|
69
|
+
if (!CLIENT_TOKEN_RE.test(token)) {
|
|
70
|
+
throw new GbitXPayError("invalid_arguments", "clientToken is missing or malformed.");
|
|
71
|
+
}
|
|
72
|
+
return { paymentId: id, clientToken: token };
|
|
73
|
+
}
|
|
74
|
+
function assertHttpsOrigin(raw, label) {
|
|
75
|
+
if (typeof raw !== "string" || raw.trim() === "") {
|
|
76
|
+
throw new GbitXPayError("invalid_arguments", `${label} is required.`);
|
|
77
|
+
}
|
|
78
|
+
let u;
|
|
79
|
+
try {
|
|
80
|
+
u = new URL(raw.trim());
|
|
81
|
+
} catch {
|
|
82
|
+
throw new GbitXPayError("invalid_arguments", `${label} is not a valid URL.`);
|
|
83
|
+
}
|
|
84
|
+
if (u.protocol !== "https:") {
|
|
85
|
+
throw new GbitXPayError("invalid_arguments", `${label} must be https.`);
|
|
86
|
+
}
|
|
87
|
+
return u.origin;
|
|
88
|
+
}
|
|
89
|
+
function buildCheckoutUrl(checkoutOrigin, paymentId, clientToken) {
|
|
90
|
+
return `${checkoutOrigin}/p/${encodeURIComponent(paymentId)}?embed=native#t=${encodeURIComponent(clientToken)}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/redact.ts
|
|
94
|
+
function redactUrl(url) {
|
|
95
|
+
try {
|
|
96
|
+
const u = new URL(url);
|
|
97
|
+
return `${u.origin}${u.pathname}`;
|
|
98
|
+
} catch {
|
|
99
|
+
return "[unparseable-url]";
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function redactSecrets(input) {
|
|
103
|
+
return input.replace(/([gp]k_(?:live|test)_)[0-9a-f]{6,}/gi, "$1redacted").replace(/([#?&]t=)[^\s&"']+/g, "$1redacted").replace(/(Bearer\s+)[^\s"']+/gi, "$1redacted");
|
|
104
|
+
}
|
|
105
|
+
function safeStringify(value) {
|
|
106
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
107
|
+
try {
|
|
108
|
+
return JSON.stringify(value, (_k, v) => {
|
|
109
|
+
if (typeof v === "object" && v !== null) {
|
|
110
|
+
if (seen.has(v)) return "[circular]";
|
|
111
|
+
seen.add(v);
|
|
112
|
+
}
|
|
113
|
+
return v;
|
|
114
|
+
});
|
|
115
|
+
} catch {
|
|
116
|
+
return String(value);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
var _debug = false;
|
|
120
|
+
function setDebug(on) {
|
|
121
|
+
_debug = !!on;
|
|
122
|
+
}
|
|
123
|
+
function debugLog(...parts) {
|
|
124
|
+
if (!_debug) return;
|
|
125
|
+
try {
|
|
126
|
+
const line = parts.map((p) => typeof p === "string" ? p : safeStringify(p)).join(" ");
|
|
127
|
+
console.log("[GbitXPay]", redactSecrets(line));
|
|
128
|
+
} catch {
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/config.ts
|
|
133
|
+
var DEFAULT_API_BASE = "https://gateway.gbitx.com";
|
|
134
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
135
|
+
var MIN_TIMEOUT_MS = 1e3;
|
|
136
|
+
var MAX_TIMEOUT_MS = 6e4;
|
|
137
|
+
var _state = "unconfigured";
|
|
138
|
+
var _config = null;
|
|
139
|
+
var _inflight = null;
|
|
140
|
+
var _inflightKey = null;
|
|
141
|
+
function isConfigured() {
|
|
142
|
+
return _state === "configured" && _config !== null;
|
|
143
|
+
}
|
|
144
|
+
function getResolvedConfig() {
|
|
145
|
+
return _config;
|
|
146
|
+
}
|
|
147
|
+
function normalizeTimeout(ms) {
|
|
148
|
+
if (typeof ms !== "number" || !Number.isFinite(ms)) return DEFAULT_TIMEOUT_MS;
|
|
149
|
+
return Math.min(MAX_TIMEOUT_MS, Math.max(MIN_TIMEOUT_MS, Math.floor(ms)));
|
|
150
|
+
}
|
|
151
|
+
function safeMessage(msg, fallback) {
|
|
152
|
+
return typeof msg === "string" && msg.trim() ? redactSecrets(msg.trim()) : fallback;
|
|
153
|
+
}
|
|
154
|
+
function mapConfigError(status, body, retryAfterHeader) {
|
|
155
|
+
const b = body || {};
|
|
156
|
+
if (status === 403 && b.code === "ONBOARDING_REQUIRED") {
|
|
157
|
+
return new GbitXPayError("onboarding_required", safeMessage(b.message, "This publishable key is not active yet. Complete onboarding to activate."), {
|
|
158
|
+
onboardingStatus: typeof b.onboardingStatus === "string" ? b.onboardingStatus : void 0
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
if (status === 403) {
|
|
162
|
+
return new GbitXPayError("merchant_suspended", safeMessage(b.message, "This merchant account is not active."));
|
|
163
|
+
}
|
|
164
|
+
if (status === 429) {
|
|
165
|
+
const ra = Number(retryAfterHeader);
|
|
166
|
+
return new GbitXPayError("rate_limited", safeMessage(b.message, "Too many requests. Please try again shortly."), {
|
|
167
|
+
retryAfterSec: Number.isFinite(ra) && ra > 0 ? ra : void 0
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
if (status >= 500) {
|
|
171
|
+
return new GbitXPayError("server_error", safeMessage(b.message, "GbitXPay is temporarily unavailable. Please try again."));
|
|
172
|
+
}
|
|
173
|
+
return new GbitXPayError("invalid_key", safeMessage(b.message, "The publishable key was rejected."));
|
|
174
|
+
}
|
|
175
|
+
function parseConfigSuccess(body) {
|
|
176
|
+
const b = body;
|
|
177
|
+
if (!b || b.success !== true) {
|
|
178
|
+
throw new GbitXPayError("server_error", "Unexpected response while validating the key.");
|
|
179
|
+
}
|
|
180
|
+
if (b.environment !== "live" && b.environment !== "test") {
|
|
181
|
+
throw new GbitXPayError("server_error", "Unexpected config response (environment).");
|
|
182
|
+
}
|
|
183
|
+
let checkoutOrigin;
|
|
184
|
+
try {
|
|
185
|
+
checkoutOrigin = assertHttpsOrigin(b.checkoutBaseUrl, "checkoutBaseUrl");
|
|
186
|
+
} catch {
|
|
187
|
+
throw new GbitXPayError("server_error", "Insecure or invalid checkout origin from server.");
|
|
188
|
+
}
|
|
189
|
+
const m = b.merchant;
|
|
190
|
+
if (!m || typeof m !== "object" || Array.isArray(m)) {
|
|
191
|
+
throw new GbitXPayError("server_error", "Unexpected config response (merchant).");
|
|
192
|
+
}
|
|
193
|
+
const name = typeof m.name === "string" && m.name.trim() ? m.name.trim() : "Merchant";
|
|
194
|
+
const website = typeof m.website === "string" && m.website.trim() ? m.website.trim() : null;
|
|
195
|
+
return { environment: b.environment, checkoutOrigin, merchant: { name, website } };
|
|
196
|
+
}
|
|
197
|
+
async function fetchConfig(key, apiBase, timeoutMs) {
|
|
198
|
+
var _a, _b;
|
|
199
|
+
const url = `${apiBase}/v1/sdk/config`;
|
|
200
|
+
const controller = new AbortController();
|
|
201
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
202
|
+
let res;
|
|
203
|
+
try {
|
|
204
|
+
res = await fetch(url, {
|
|
205
|
+
method: "GET",
|
|
206
|
+
signal: controller.signal,
|
|
207
|
+
headers: { Authorization: `Bearer ${key}`, Accept: "application/json" }
|
|
208
|
+
});
|
|
209
|
+
} catch (cause) {
|
|
210
|
+
throw new GbitXPayError("network_error", "Could not reach GbitXPay to validate the publishable key.", { cause });
|
|
211
|
+
} finally {
|
|
212
|
+
clearTimeout(timer);
|
|
213
|
+
}
|
|
214
|
+
const body = await res.json().catch(() => null);
|
|
215
|
+
if (!res.ok) {
|
|
216
|
+
throw mapConfigError(res.status, body, (_b = (_a = res.headers) == null ? void 0 : _a.get) == null ? void 0 : _b.call(_a, "retry-after"));
|
|
217
|
+
}
|
|
218
|
+
const parsed = parseConfigSuccess(body);
|
|
219
|
+
return {
|
|
220
|
+
publishableKey: key,
|
|
221
|
+
environment: parsed.environment,
|
|
222
|
+
checkoutOrigin: parsed.checkoutOrigin,
|
|
223
|
+
merchant: parsed.merchant
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
async function configure(options) {
|
|
227
|
+
if (options && options.debug) setDebug(true);
|
|
228
|
+
const { key, environment: prefixEnv } = assertPublishableKey(options == null ? void 0 : options.publishableKey);
|
|
229
|
+
if (options.environment && options.environment !== prefixEnv) {
|
|
230
|
+
throw new GbitXPayError("environment_mismatch", "The environment option does not match the publishable key.");
|
|
231
|
+
}
|
|
232
|
+
const timeoutMs = normalizeTimeout(options.timeoutMs);
|
|
233
|
+
const apiBase = options.apiBaseUrl ? assertHttpsOrigin(options.apiBaseUrl, "apiBaseUrl") : DEFAULT_API_BASE;
|
|
234
|
+
if (_inflight && _inflightKey === key) {
|
|
235
|
+
await _inflight;
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
_state = "configuring";
|
|
239
|
+
debugLog("configure: validating key against", `${apiBase}/v1/sdk/config`);
|
|
240
|
+
const run = (async () => {
|
|
241
|
+
const resolved = await fetchConfig(key, apiBase, timeoutMs);
|
|
242
|
+
if (resolved.environment !== prefixEnv) {
|
|
243
|
+
throw new GbitXPayError("environment_mismatch", "Key environment and server environment disagree.");
|
|
244
|
+
}
|
|
245
|
+
if (options.environment && options.environment !== resolved.environment) {
|
|
246
|
+
throw new GbitXPayError("environment_mismatch", "The environment option does not match the server environment.");
|
|
247
|
+
}
|
|
248
|
+
return resolved;
|
|
249
|
+
})();
|
|
250
|
+
_inflight = run;
|
|
251
|
+
_inflightKey = key;
|
|
252
|
+
try {
|
|
253
|
+
const resolved = await run;
|
|
254
|
+
if (_inflight === run) {
|
|
255
|
+
_config = resolved;
|
|
256
|
+
_state = "configured";
|
|
257
|
+
debugLog("configure: ok, environment =", resolved.environment);
|
|
258
|
+
}
|
|
259
|
+
} catch (err) {
|
|
260
|
+
if (_inflight === run) {
|
|
261
|
+
_state = "configure_failed";
|
|
262
|
+
_config = null;
|
|
263
|
+
}
|
|
264
|
+
throw err;
|
|
265
|
+
} finally {
|
|
266
|
+
if (_inflight === run) {
|
|
267
|
+
_inflight = null;
|
|
268
|
+
_inflightKey = null;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// src/controller.ts
|
|
274
|
+
var _host = null;
|
|
275
|
+
var _active = null;
|
|
276
|
+
function registerHost(listener) {
|
|
277
|
+
_host = listener;
|
|
278
|
+
if (_active) listener(_active);
|
|
279
|
+
return () => {
|
|
280
|
+
if (_host === listener) _host = null;
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
function present(options) {
|
|
284
|
+
return new Promise((resolve, reject) => {
|
|
285
|
+
if (!isConfigured()) {
|
|
286
|
+
reject(new GbitXPayError("not_configured", "Call and await GbitXPay.configure({ publishableKey }) before present()."));
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (!_host) {
|
|
290
|
+
reject(new GbitXPayError("not_mounted", "Mount <GbitXPayProvider> once near your app root before calling present()."));
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (_active) {
|
|
294
|
+
reject(new GbitXPayError("invalid_arguments", "A checkout is already open."));
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
let args;
|
|
298
|
+
try {
|
|
299
|
+
args = assertPaymentArgs(options == null ? void 0 : options.paymentId, options == null ? void 0 : options.clientToken);
|
|
300
|
+
} catch (e) {
|
|
301
|
+
reject(e);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const config = getResolvedConfig();
|
|
305
|
+
if (!config) {
|
|
306
|
+
reject(new GbitXPayError("not_configured", "Configuration was lost. Call configure() again."));
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const url = buildCheckoutUrl(config.checkoutOrigin, args.paymentId, args.clientToken);
|
|
310
|
+
let done = false;
|
|
311
|
+
const session = {
|
|
312
|
+
paymentId: args.paymentId,
|
|
313
|
+
clientToken: args.clientToken,
|
|
314
|
+
url,
|
|
315
|
+
config,
|
|
316
|
+
onLoaded: options.onLoaded,
|
|
317
|
+
onEvent: options.onEvent,
|
|
318
|
+
settle: (result) => {
|
|
319
|
+
if (done) return;
|
|
320
|
+
done = true;
|
|
321
|
+
_active = null;
|
|
322
|
+
_host == null ? void 0 : _host(null);
|
|
323
|
+
resolve(result);
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
_active = session;
|
|
327
|
+
_host(session);
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// src/GbitXPay.ts
|
|
332
|
+
var GbitXPay = {
|
|
333
|
+
configure,
|
|
334
|
+
present
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// src/messageHandler.ts
|
|
338
|
+
var KNOWN_TYPES = {
|
|
339
|
+
"gbitx:payment:loaded": "loaded",
|
|
340
|
+
"gbitx:payment:confirmed": "confirmed",
|
|
341
|
+
"gbitx:payment:cancelled": "cancelled",
|
|
342
|
+
"gbitx:payment:expired": "expired",
|
|
343
|
+
"gbitx:payment:failed": "failed",
|
|
344
|
+
"gbitx:payment:closed": "closed"
|
|
345
|
+
};
|
|
346
|
+
var MAX_MESSAGE_BYTES = 8192;
|
|
347
|
+
var PAYMENT_STATUSES = [
|
|
348
|
+
"PENDING",
|
|
349
|
+
"AWAITING_PAYMENT",
|
|
350
|
+
"UNDERPAID",
|
|
351
|
+
"CONFIRMING",
|
|
352
|
+
"CONFIRMED",
|
|
353
|
+
"EXPIRED",
|
|
354
|
+
"FAILED",
|
|
355
|
+
"CANCELLED"
|
|
356
|
+
];
|
|
357
|
+
function asString(v) {
|
|
358
|
+
return typeof v === "string" ? v : null;
|
|
359
|
+
}
|
|
360
|
+
function asDecimalString(v) {
|
|
361
|
+
if (typeof v === "string") return v;
|
|
362
|
+
if (typeof v === "number" && Number.isFinite(v)) {
|
|
363
|
+
const s = String(v);
|
|
364
|
+
return s.includes("e") || s.includes("E") ? null : s;
|
|
365
|
+
}
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
function sanitizePayment(input) {
|
|
369
|
+
var _a, _b, _c;
|
|
370
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return null;
|
|
371
|
+
const p = input;
|
|
372
|
+
const id = asString(p.id);
|
|
373
|
+
const status = asString(p.status);
|
|
374
|
+
if (!id || !status || !PAYMENT_STATUSES.includes(status)) return null;
|
|
375
|
+
const mode = p.mode === "LIVE" || p.mode === "SANDBOX" ? p.mode : "SANDBOX";
|
|
376
|
+
return {
|
|
377
|
+
id,
|
|
378
|
+
status,
|
|
379
|
+
amount: (_a = asDecimalString(p.amount)) != null ? _a : "",
|
|
380
|
+
currency: (_b = asString(p.currency)) != null ? _b : "",
|
|
381
|
+
crypto: asString(p.crypto),
|
|
382
|
+
network: asString(p.network),
|
|
383
|
+
amountCrypto: asDecimalString(p.amountCrypto),
|
|
384
|
+
amountPaid: asDecimalString(p.amountPaid),
|
|
385
|
+
mode,
|
|
386
|
+
confirmedAt: asString(p.confirmedAt),
|
|
387
|
+
expiresAt: (_c = asString(p.expiresAt)) != null ? _c : ""
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
function parseBridgeMessage(raw) {
|
|
391
|
+
if (typeof raw !== "string" || raw.length === 0 || raw.length > MAX_MESSAGE_BYTES) return null;
|
|
392
|
+
let obj;
|
|
393
|
+
try {
|
|
394
|
+
obj = JSON.parse(raw);
|
|
395
|
+
} catch {
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj)) return null;
|
|
399
|
+
const type = obj.type;
|
|
400
|
+
if (typeof type !== "string") return null;
|
|
401
|
+
const mapped = KNOWN_TYPES[type];
|
|
402
|
+
if (!mapped) return null;
|
|
403
|
+
return {
|
|
404
|
+
type: mapped,
|
|
405
|
+
payment: sanitizePayment(obj.payment)
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
function terminalResult(type, payment) {
|
|
409
|
+
switch (type) {
|
|
410
|
+
case "confirmed":
|
|
411
|
+
return { status: "confirmed", payment };
|
|
412
|
+
case "cancelled":
|
|
413
|
+
case "closed":
|
|
414
|
+
return { status: "cancelled", reason: "user_closed", payment };
|
|
415
|
+
case "expired":
|
|
416
|
+
return { status: "expired", reason: "expired", payment };
|
|
417
|
+
case "failed":
|
|
418
|
+
return { status: "failed", payment };
|
|
419
|
+
case "loaded":
|
|
420
|
+
default:
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// src/webviewPolicy.ts
|
|
426
|
+
function parse(url) {
|
|
427
|
+
if (typeof url !== "string" || url === "") return null;
|
|
428
|
+
try {
|
|
429
|
+
return new URL(url);
|
|
430
|
+
} catch {
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
function mayLoadInWebView(checkoutOrigin, url) {
|
|
435
|
+
const u = parse(url);
|
|
436
|
+
return !!u && u.protocol === "https:" && u.origin === checkoutOrigin;
|
|
437
|
+
}
|
|
438
|
+
function isTrustedMessageOrigin(checkoutOrigin, url) {
|
|
439
|
+
const u = parse(url);
|
|
440
|
+
return !!u && u.protocol === "https:" && u.origin === checkoutOrigin;
|
|
441
|
+
}
|
|
442
|
+
function isTrustedMessageSource(checkoutOrigin, url) {
|
|
443
|
+
const u = parse(url);
|
|
444
|
+
return !!u && u.protocol === "https:" && u.origin === checkoutOrigin && u.pathname.startsWith("/p/");
|
|
445
|
+
}
|
|
446
|
+
var SESSION_MAX_MS = 65 * 60 * 1e3;
|
|
447
|
+
function useTheme() {
|
|
448
|
+
const scheme = reactNative.useColorScheme();
|
|
449
|
+
return scheme === "dark" ? { bg: "#0a0f0c", text: "#f2f5f3", sub: "#9aa5a0", border: "#1c2620", spinner: "#3ecf6b" } : { bg: "#ffffff", text: "#0a0f0c", sub: "#5b6660", border: "#e6ebe8", spinner: "#07811f" };
|
|
450
|
+
}
|
|
451
|
+
function CheckoutModal({ session }) {
|
|
452
|
+
var _a;
|
|
453
|
+
const theme = useTheme();
|
|
454
|
+
const checkoutOrigin = session.config.checkoutOrigin;
|
|
455
|
+
const [pageReady, setPageReady] = react.useState(false);
|
|
456
|
+
const [loadError, setLoadError] = react.useState(false);
|
|
457
|
+
const settledRef = react.useRef(false);
|
|
458
|
+
const loadedFiredRef = react.useRef(false);
|
|
459
|
+
const committedUrlRef = react.useRef(session.url);
|
|
460
|
+
const backstopRef = react.useRef(null);
|
|
461
|
+
const webViewRef = react.useRef(null);
|
|
462
|
+
const settle = react.useCallback(
|
|
463
|
+
(result) => {
|
|
464
|
+
var _a2;
|
|
465
|
+
if (settledRef.current) return;
|
|
466
|
+
settledRef.current = true;
|
|
467
|
+
if (backstopRef.current) {
|
|
468
|
+
clearTimeout(backstopRef.current);
|
|
469
|
+
backstopRef.current = null;
|
|
470
|
+
}
|
|
471
|
+
committedUrlRef.current = "";
|
|
472
|
+
debugLog("settle", result.status, "reason" in result ? (_a2 = result.reason) != null ? _a2 : "" : "");
|
|
473
|
+
session.settle(result);
|
|
474
|
+
},
|
|
475
|
+
[session]
|
|
476
|
+
);
|
|
477
|
+
const handleShouldStart = react.useCallback(
|
|
478
|
+
(req) => mayLoadInWebView(checkoutOrigin, req.url),
|
|
479
|
+
[checkoutOrigin]
|
|
480
|
+
);
|
|
481
|
+
const handleOpenWindow = react.useCallback(() => {
|
|
482
|
+
}, []);
|
|
483
|
+
const handleMessage = react.useCallback(
|
|
484
|
+
(event) => {
|
|
485
|
+
var _a2, _b, _c, _d, _e;
|
|
486
|
+
const senderUrl = (_a2 = event == null ? void 0 : event.nativeEvent) == null ? void 0 : _a2.url;
|
|
487
|
+
const committedOnCheckout = isTrustedMessageSource(checkoutOrigin, committedUrlRef.current || session.url);
|
|
488
|
+
if (typeof senderUrl === "string" && senderUrl !== "") {
|
|
489
|
+
if (!isTrustedMessageOrigin(checkoutOrigin, senderUrl) || !committedOnCheckout) return;
|
|
490
|
+
} else if (!committedOnCheckout) {
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
const parsed = parseBridgeMessage((_b = event == null ? void 0 : event.nativeEvent) == null ? void 0 : _b.data);
|
|
494
|
+
if (!parsed) return;
|
|
495
|
+
if (parsed.payment && parsed.payment.id !== session.paymentId) return;
|
|
496
|
+
(_d = session.onEvent) == null ? void 0 : _d.call(session, { type: parsed.type, payment: (_c = parsed.payment) != null ? _c : void 0 });
|
|
497
|
+
if (parsed.type === "loaded") {
|
|
498
|
+
setPageReady(true);
|
|
499
|
+
if (!loadedFiredRef.current) {
|
|
500
|
+
loadedFiredRef.current = true;
|
|
501
|
+
(_e = session.onLoaded) == null ? void 0 : _e.call(session);
|
|
502
|
+
}
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
const result = terminalResult(parsed.type, parsed.payment);
|
|
506
|
+
if (result) settle(result);
|
|
507
|
+
},
|
|
508
|
+
[checkoutOrigin, session, settle]
|
|
509
|
+
);
|
|
510
|
+
const isMainFrameUrl = react.useCallback(
|
|
511
|
+
(url) => {
|
|
512
|
+
if (!url) return true;
|
|
513
|
+
try {
|
|
514
|
+
const u = new URL(url);
|
|
515
|
+
return u.origin === checkoutOrigin && u.pathname.startsWith("/p/");
|
|
516
|
+
} catch {
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
[checkoutOrigin]
|
|
521
|
+
);
|
|
522
|
+
const handleLoadError = react.useCallback(
|
|
523
|
+
(e) => {
|
|
524
|
+
var _a2;
|
|
525
|
+
if (isMainFrameUrl((_a2 = e == null ? void 0 : e.nativeEvent) == null ? void 0 : _a2.url)) setLoadError(true);
|
|
526
|
+
},
|
|
527
|
+
[isMainFrameUrl]
|
|
528
|
+
);
|
|
529
|
+
const handleCrash = react.useCallback(() => {
|
|
530
|
+
setLoadError(true);
|
|
531
|
+
}, []);
|
|
532
|
+
const retry = react.useCallback(() => {
|
|
533
|
+
var _a2;
|
|
534
|
+
setLoadError(false);
|
|
535
|
+
setPageReady(false);
|
|
536
|
+
(_a2 = webViewRef.current) == null ? void 0 : _a2.reload();
|
|
537
|
+
}, []);
|
|
538
|
+
react.useEffect(() => {
|
|
539
|
+
backstopRef.current = setTimeout(() => settle({ status: "expired", reason: "client_backstop" }), SESSION_MAX_MS);
|
|
540
|
+
const onBack = () => {
|
|
541
|
+
settle({ status: "cancelled", reason: "back_button" });
|
|
542
|
+
return true;
|
|
543
|
+
};
|
|
544
|
+
const sub = reactNative.BackHandler.addEventListener("hardwareBackPress", onBack);
|
|
545
|
+
return () => {
|
|
546
|
+
var _a2, _b;
|
|
547
|
+
sub.remove();
|
|
548
|
+
if (backstopRef.current) {
|
|
549
|
+
clearTimeout(backstopRef.current);
|
|
550
|
+
backstopRef.current = null;
|
|
551
|
+
}
|
|
552
|
+
(_b = (_a2 = webViewRef.current) == null ? void 0 : _a2.stopLoading) == null ? void 0 : _b.call(_a2);
|
|
553
|
+
if (!settledRef.current) {
|
|
554
|
+
settledRef.current = true;
|
|
555
|
+
session.settle({ status: "cancelled", reason: "dismissed" });
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
}, []);
|
|
559
|
+
react.useEffect(() => {
|
|
560
|
+
debugLog("present", redactUrl(session.url));
|
|
561
|
+
}, []);
|
|
562
|
+
const topInset = reactNative.Platform.OS === "android" ? (_a = reactNative.StatusBar.currentHeight) != null ? _a : 0 : 0;
|
|
563
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
564
|
+
reactNative.Modal,
|
|
565
|
+
{
|
|
566
|
+
visible: true,
|
|
567
|
+
animationType: "slide",
|
|
568
|
+
presentationStyle: "pageSheet",
|
|
569
|
+
statusBarTranslucent: true,
|
|
570
|
+
onRequestClose: () => settle({ status: "cancelled", reason: "back_button" }),
|
|
571
|
+
onDismiss: () => {
|
|
572
|
+
if (!settledRef.current) settle({ status: "cancelled", reason: "dismissed" });
|
|
573
|
+
},
|
|
574
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles.container, { backgroundColor: theme.bg, paddingTop: topInset }], children: [
|
|
575
|
+
/* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles.header, { borderBottomColor: theme.border }], children: [
|
|
576
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.title, { color: theme.text }], numberOfLines: 1, children: session.config.merchant.name }),
|
|
577
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
578
|
+
reactNative.TouchableOpacity,
|
|
579
|
+
{
|
|
580
|
+
onPress: () => settle({ status: "cancelled", reason: "user_closed" }),
|
|
581
|
+
accessibilityRole: "button",
|
|
582
|
+
accessibilityLabel: "Close checkout",
|
|
583
|
+
hitSlop: { top: 12, bottom: 12, left: 12, right: 12 },
|
|
584
|
+
style: styles.closeBtn,
|
|
585
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.closeIcon, { color: theme.text }], children: "\u2715" })
|
|
586
|
+
}
|
|
587
|
+
)
|
|
588
|
+
] }),
|
|
589
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles.body, children: loadError ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles.center, accessibilityLabel: "Checkout failed to load", children: [
|
|
590
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.errorTitle, { color: theme.text }], children: "Could not load checkout" }),
|
|
591
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.errorSub, { color: theme.sub }], children: "Check your connection and try again." }),
|
|
592
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.TouchableOpacity, { onPress: retry, accessibilityRole: "button", style: [styles.retryBtn, { borderColor: theme.border }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.retryText, { color: theme.text }], children: "Retry" }) }),
|
|
593
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
594
|
+
reactNative.TouchableOpacity,
|
|
595
|
+
{
|
|
596
|
+
onPress: () => settle({ status: "cancelled", reason: "load_failed" }),
|
|
597
|
+
accessibilityRole: "button",
|
|
598
|
+
style: styles.closeLink,
|
|
599
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.closeLinkText, { color: theme.sub }], children: "Close" })
|
|
600
|
+
}
|
|
601
|
+
)
|
|
602
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
603
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
604
|
+
reactNativeWebview.WebView,
|
|
605
|
+
{
|
|
606
|
+
ref: webViewRef,
|
|
607
|
+
source: { uri: session.url },
|
|
608
|
+
originWhitelist: ["*"],
|
|
609
|
+
onShouldStartLoadWithRequest: handleShouldStart,
|
|
610
|
+
onNavigationStateChange: (s) => {
|
|
611
|
+
var _a2, _b;
|
|
612
|
+
committedUrlRef.current = s.url;
|
|
613
|
+
if (typeof s.url === "string" && /^https?:/i.test(s.url) && !mayLoadInWebView(checkoutOrigin, s.url)) {
|
|
614
|
+
if (!settledRef.current) {
|
|
615
|
+
(_b = (_a2 = webViewRef.current) == null ? void 0 : _a2.stopLoading) == null ? void 0 : _b.call(_a2);
|
|
616
|
+
setLoadError(true);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
},
|
|
620
|
+
onMessage: handleMessage,
|
|
621
|
+
onOpenWindow: handleOpenWindow,
|
|
622
|
+
setSupportMultipleWindows: false,
|
|
623
|
+
javaScriptCanOpenWindowsAutomatically: false,
|
|
624
|
+
allowFileAccess: false,
|
|
625
|
+
allowFileAccessFromFileURLs: false,
|
|
626
|
+
allowUniversalAccessFromFileURLs: false,
|
|
627
|
+
thirdPartyCookiesEnabled: false,
|
|
628
|
+
sharedCookiesEnabled: false,
|
|
629
|
+
incognito: false,
|
|
630
|
+
mixedContentMode: "never",
|
|
631
|
+
allowsInlineMediaPlayback: true,
|
|
632
|
+
mediaPlaybackRequiresUserAction: true,
|
|
633
|
+
onError: handleLoadError,
|
|
634
|
+
onHttpError: handleLoadError,
|
|
635
|
+
onContentProcessDidTerminate: handleCrash,
|
|
636
|
+
onRenderProcessGone: handleCrash,
|
|
637
|
+
onLoadEnd: () => setPageReady(true),
|
|
638
|
+
style: { flex: 1, backgroundColor: theme.bg }
|
|
639
|
+
}
|
|
640
|
+
),
|
|
641
|
+
!pageReady ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles.loadingOverlay, { backgroundColor: theme.bg }], accessibilityLabel: "Loading checkout", children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { size: "large", color: theme.spinner }) }) : null
|
|
642
|
+
] }) })
|
|
643
|
+
] })
|
|
644
|
+
}
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
var styles = reactNative.StyleSheet.create({
|
|
648
|
+
container: { flex: 1 },
|
|
649
|
+
header: {
|
|
650
|
+
flexDirection: "row",
|
|
651
|
+
alignItems: "center",
|
|
652
|
+
justifyContent: "space-between",
|
|
653
|
+
paddingHorizontal: 16,
|
|
654
|
+
height: 52,
|
|
655
|
+
borderBottomWidth: reactNative.StyleSheet.hairlineWidth
|
|
656
|
+
},
|
|
657
|
+
title: { fontSize: 15, fontWeight: "500", flex: 1, marginRight: 12 },
|
|
658
|
+
closeBtn: { width: 32, height: 32, alignItems: "center", justifyContent: "center" },
|
|
659
|
+
closeIcon: { fontSize: 18, fontWeight: "400" },
|
|
660
|
+
body: { flex: 1 },
|
|
661
|
+
loadingOverlay: { ...reactNative.StyleSheet.absoluteFillObject, alignItems: "center", justifyContent: "center" },
|
|
662
|
+
center: { flex: 1, alignItems: "center", justifyContent: "center", padding: 32 },
|
|
663
|
+
errorTitle: { fontSize: 16, fontWeight: "500", marginBottom: 8 },
|
|
664
|
+
errorSub: { fontSize: 13, textAlign: "center", marginBottom: 20 },
|
|
665
|
+
retryBtn: { paddingHorizontal: 24, paddingVertical: 10, borderRadius: 10, borderWidth: 1 },
|
|
666
|
+
retryText: { fontSize: 14, fontWeight: "500" },
|
|
667
|
+
closeLink: { marginTop: 14, padding: 8 },
|
|
668
|
+
closeLinkText: { fontSize: 13 }
|
|
669
|
+
});
|
|
670
|
+
function GbitXPayProvider({ children }) {
|
|
671
|
+
const [session, setSession] = react.useState(null);
|
|
672
|
+
react.useEffect(() => registerHost(setSession), []);
|
|
673
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
674
|
+
children,
|
|
675
|
+
session ? /* @__PURE__ */ jsxRuntime.jsx(CheckoutModal, { session }, session.paymentId) : null
|
|
676
|
+
] });
|
|
677
|
+
}
|
|
678
|
+
function useGbitXPay() {
|
|
679
|
+
return react.useMemo(() => ({ configure, present }), []);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// src/version.ts
|
|
683
|
+
var SDK_VERSION = "0.1.0";
|
|
684
|
+
|
|
685
|
+
exports.GbitXPay = GbitXPay;
|
|
686
|
+
exports.GbitXPayError = GbitXPayError;
|
|
687
|
+
exports.GbitXPayProvider = GbitXPayProvider;
|
|
688
|
+
exports.SDK_VERSION = SDK_VERSION;
|
|
689
|
+
exports.isGbitXPayError = isGbitXPayError;
|
|
690
|
+
exports.setDebug = setDebug;
|
|
691
|
+
exports.useGbitXPay = useGbitXPay;
|
|
692
|
+
//# sourceMappingURL=index.cjs.map
|
|
693
|
+
//# sourceMappingURL=index.cjs.map
|