@jazadev/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/dist/index.cjs ADDED
@@ -0,0 +1,1988 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
31
+
32
+ // src/index.ts
33
+ var index_exports = {};
34
+ __export(index_exports, {
35
+ DEFAULT_API_BASE_URL: () => DEFAULT_API_BASE_URL,
36
+ DIAL_CODES: () => DIAL_CODES,
37
+ JazaBalanceWidget: () => JazaBalanceWidget,
38
+ JazaProvider: () => JazaProvider,
39
+ JazaSdkError: () => JazaSdkError,
40
+ JazaTopUpButton: () => JazaTopUpButton,
41
+ PublicClient: () => PublicClient,
42
+ VERSION: () => VERSION,
43
+ buildE164: () => buildE164,
44
+ darkTheme: () => darkTheme,
45
+ enrichCountries: () => enrichCountries,
46
+ formatCredits: () => formatCredits,
47
+ formatCurrencyAmount: () => formatCurrencyAmount,
48
+ formatLocalAmount: () => formatLocalAmount,
49
+ formatUsd: () => formatUsd,
50
+ getDialCode: () => getDialCode,
51
+ isDepositTerminal: () => isDepositTerminal,
52
+ iso2ToFlag: () => iso2ToFlag,
53
+ lightTheme: () => lightTheme,
54
+ pickDefaultCurrencyCode: () => pickDefaultCurrencyCode,
55
+ useJaza: () => useJaza
56
+ });
57
+ module.exports = __toCommonJS(index_exports);
58
+
59
+ // src/provider/JazaProvider.tsx
60
+ var import_react8 = require("react");
61
+ var import_react_native10 = require("react-native");
62
+
63
+ // src/api/errors.ts
64
+ var JazaSdkError = class _JazaSdkError extends Error {
65
+ constructor(statusCode, message, raw) {
66
+ super(message);
67
+ __publicField(this, "statusCode");
68
+ __publicField(this, "code");
69
+ __publicField(this, "raw");
70
+ this.name = "JazaSdkError";
71
+ this.statusCode = statusCode;
72
+ this.code = `HTTP_${statusCode}`;
73
+ this.raw = raw;
74
+ }
75
+ static fromResponse(status, body) {
76
+ let message = `Request failed (${status})`;
77
+ if (typeof body === "object" && body !== null) {
78
+ const m = body.message;
79
+ if (typeof m === "string") {
80
+ message = m;
81
+ } else if (typeof m === "object" && m?.message) {
82
+ message = m.message;
83
+ }
84
+ }
85
+ return new _JazaSdkError(status, message, body);
86
+ }
87
+ };
88
+
89
+ // src/version.ts
90
+ var VERSION = "0.1.0";
91
+ var DEFAULT_API_BASE_URL = "https://api.jaza.dev";
92
+
93
+ // src/api/publicClient.ts
94
+ var PublicClient = class {
95
+ constructor(config) {
96
+ __publicField(this, "apiBaseUrl");
97
+ __publicField(this, "publishableKey");
98
+ __publicField(this, "topUpToken", null);
99
+ this.apiBaseUrl = (config.apiBaseUrl ?? DEFAULT_API_BASE_URL).replace(
100
+ /\/$/,
101
+ ""
102
+ );
103
+ this.publishableKey = config.publishableKey;
104
+ }
105
+ setTopUpToken(token) {
106
+ this.topUpToken = token;
107
+ }
108
+ async listCountries() {
109
+ return this.request("GET", "/v1/catalog/countries");
110
+ }
111
+ async listBundles() {
112
+ return this.authedRequest("GET", "/v1/public/bundles");
113
+ }
114
+ async predictProvider(phoneNumber) {
115
+ return this.authedRequest(
116
+ "POST",
117
+ "/v1/public/payments/predict",
118
+ { phoneNumber }
119
+ );
120
+ }
121
+ async quotePayment(body) {
122
+ return this.authedRequest(
123
+ "POST",
124
+ "/v1/public/payments/quote",
125
+ body
126
+ );
127
+ }
128
+ async createDeposit(body) {
129
+ return this.authedRequest(
130
+ "POST",
131
+ "/v1/public/payments/deposits",
132
+ body
133
+ );
134
+ }
135
+ async getDeposit(paymentRequestId) {
136
+ return this.authedRequest(
137
+ "GET",
138
+ `/v1/public/payments/deposits/${paymentRequestId}`
139
+ );
140
+ }
141
+ async authedRequest(method, path, body) {
142
+ if (!this.topUpToken) {
143
+ throw new JazaSdkError(401, "Top-up session token is not set");
144
+ }
145
+ return this.request(method, path, body, this.topUpToken);
146
+ }
147
+ async request(method, path, body, token) {
148
+ const headers = {
149
+ Accept: "application/json",
150
+ "User-Agent": `@jazadev/react-native/${VERSION}`
151
+ };
152
+ if (token) {
153
+ headers.Authorization = `Bearer ${token}`;
154
+ headers["X-Jaza-Public-Key"] = this.publishableKey;
155
+ }
156
+ if (body !== void 0) {
157
+ headers["Content-Type"] = "application/json";
158
+ }
159
+ const response = await fetch(`${this.apiBaseUrl}${path}`, {
160
+ method,
161
+ headers,
162
+ body: body === void 0 ? void 0 : JSON.stringify(body)
163
+ });
164
+ const text = await response.text();
165
+ let parsed = null;
166
+ if (text) {
167
+ try {
168
+ parsed = JSON.parse(text);
169
+ } catch {
170
+ parsed = { message: text };
171
+ }
172
+ }
173
+ if (!response.ok) {
174
+ throw JazaSdkError.fromResponse(response.status, parsed);
175
+ }
176
+ return parsed;
177
+ }
178
+ };
179
+
180
+ // src/data/dialCodes.ts
181
+ var DIAL_CODES = {
182
+ BJ: "229",
183
+ BF: "226",
184
+ CM: "237",
185
+ CI: "225",
186
+ CD: "243",
187
+ CG: "242",
188
+ ET: "251",
189
+ GA: "241",
190
+ GH: "233",
191
+ KE: "254",
192
+ MW: "265",
193
+ MZ: "258",
194
+ NG: "234",
195
+ RW: "250",
196
+ SN: "221",
197
+ SL: "232",
198
+ TZ: "255",
199
+ UG: "256",
200
+ ZM: "260"
201
+ };
202
+ function iso2ToFlag(iso2) {
203
+ const code = iso2.toUpperCase();
204
+ if (code.length !== 2) return "";
205
+ const points = [...code].map((c) => 127462 - 65 + c.charCodeAt(0));
206
+ return String.fromCodePoint(...points);
207
+ }
208
+ function getDialCode(iso2) {
209
+ return DIAL_CODES[iso2.toUpperCase()];
210
+ }
211
+
212
+ // src/utils/helpers.ts
213
+ function enrichCountries(countries) {
214
+ return countries.filter((c) => c.isActive && getDialCode(c.iso2)).map((c) => {
215
+ const dialCode = getDialCode(c.iso2);
216
+ const currencies = c.currencies?.map((cc) => cc.currency).filter((cur) => cur?.isActive).map((cur) => ({
217
+ code: cur.code,
218
+ decimals: cur.decimals,
219
+ name: cur.name
220
+ })) ?? [];
221
+ return {
222
+ id: c.id,
223
+ name: c.name,
224
+ iso2: c.iso2,
225
+ iso3: c.iso3,
226
+ dialCode,
227
+ flag: iso2ToFlag(c.iso2),
228
+ currencies
229
+ };
230
+ }).filter((c) => c.currencies.length > 0).sort((a, b) => a.name.localeCompare(b.name));
231
+ }
232
+ function buildE164(dialCode, nationalNumber) {
233
+ const digits = nationalNumber.replace(/\D/g, "");
234
+ const code = dialCode.replace(/\D/g, "");
235
+ if (digits.length < 6) {
236
+ throw new Error("Phone number too short");
237
+ }
238
+ return `${code}${digits}`;
239
+ }
240
+ function pickDefaultCurrencyCode(codes) {
241
+ if (codes.length === 0) {
242
+ throw new Error("No currencies available");
243
+ }
244
+ const local = codes.find((c) => c.toUpperCase() !== "USD");
245
+ return (local ?? codes[0]).toUpperCase();
246
+ }
247
+ function isDepositTerminal(status) {
248
+ return status === "COMPLETED" || status === "FAILED" || status === "EXPIRED" || status === "CANCELLED";
249
+ }
250
+ function formatCredits(n) {
251
+ return n.toLocaleString("en-US");
252
+ }
253
+ function formatUsd(priceUsd) {
254
+ const n = Number(priceUsd);
255
+ if (Number.isNaN(n)) return priceUsd;
256
+ return formatCurrencyAmount(n, "USD", 2);
257
+ }
258
+ function resolveCurrencyFractionDigits(currencyCode) {
259
+ return currencyCode.toUpperCase() === "USD" ? 2 : 0;
260
+ }
261
+ function formatCurrencyAmount(amount, currencyCode, _decimals) {
262
+ const n = typeof amount === "string" ? Number(amount) : amount;
263
+ const code = currencyCode.toUpperCase();
264
+ if (Number.isNaN(n)) return `${amount} ${code}`;
265
+ const fractionDigits = resolveCurrencyFractionDigits(code);
266
+ try {
267
+ return new Intl.NumberFormat("en-US", {
268
+ style: "currency",
269
+ currency: code,
270
+ minimumFractionDigits: fractionDigits,
271
+ maximumFractionDigits: fractionDigits
272
+ }).format(n);
273
+ } catch {
274
+ const grouped = n.toLocaleString("en-US", {
275
+ minimumFractionDigits: fractionDigits,
276
+ maximumFractionDigits: fractionDigits
277
+ });
278
+ return `${grouped} ${code}`;
279
+ }
280
+ }
281
+ function formatLocalAmount(amount, currencyCode, decimals) {
282
+ return formatCurrencyAmount(amount, currencyCode, decimals);
283
+ }
284
+
285
+ // src/theme/tokens.ts
286
+ var spacing = { xs: 4, sm: 8, md: 16, lg: 24, xl: 40, gutter: 20 };
287
+ var radius = { md: 8, lg: 12, xl: 16, full: 9999 };
288
+ var darkTheme = {
289
+ mode: "dark",
290
+ spacing,
291
+ radius,
292
+ colors: {
293
+ background: "#121414",
294
+ surface: "#121414",
295
+ surfaceContainer: "#1e2020",
296
+ surfaceContainerLow: "#1a1c1c",
297
+ surfaceContainerHigh: "#282a2b",
298
+ surfaceContainerHighest: "#333535",
299
+ surfaceVariant: "#333535",
300
+ onSurface: "#e2e2e2",
301
+ onSurfaceVariant: "#bacac5",
302
+ primary: "#57f1db",
303
+ onPrimary: "#003731",
304
+ primaryContainer: "#2dd4bf",
305
+ onPrimaryContainer: "#00574d",
306
+ secondaryContainer: "#00bd85",
307
+ onSecondaryContainer: "#00452e",
308
+ outline: "#859490",
309
+ outlineVariant: "#3c4a46",
310
+ error: "#ffb4ab",
311
+ success: "#45dfa4",
312
+ overlay: "rgba(12,15,15,0.8)",
313
+ bundleBorder: "#262626",
314
+ bundleBorderSelected: "#2dd4bf",
315
+ bundleBg: "#111111"
316
+ }
317
+ };
318
+ var lightTheme = {
319
+ ...darkTheme,
320
+ mode: "light"
321
+ };
322
+ function resolveTheme(preference, systemScheme) {
323
+ if (preference === "system") {
324
+ return systemScheme === "dark" ? darkTheme : lightTheme;
325
+ }
326
+ return preference === "dark" ? darkTheme : lightTheme;
327
+ }
328
+
329
+ // src/provider/JazaContext.tsx
330
+ var import_react = require("react");
331
+ var JazaContext = (0, import_react.createContext)(null);
332
+ function useJaza() {
333
+ const ctx = (0, import_react.useContext)(JazaContext);
334
+ if (!ctx) {
335
+ throw new Error("useJaza must be used within JazaProvider");
336
+ }
337
+ return ctx;
338
+ }
339
+
340
+ // src/sheet/TopUpBottomSheet.tsx
341
+ var import_react7 = require("react");
342
+ var import_react_native9 = require("react-native");
343
+ var import_react_native_gesture_handler = require("react-native-gesture-handler");
344
+ var import_bottom_sheet2 = __toESM(require("@gorhom/bottom-sheet"), 1);
345
+ var import_react_native_safe_area_context3 = require("react-native-safe-area-context");
346
+
347
+ // src/sheet/steps/OfferStep.tsx
348
+ var import_react_native = require("react-native");
349
+
350
+ // src/components/Icon.tsx
351
+ var import_MaterialIcons = __toESM(require("@expo/vector-icons/MaterialIcons"), 1);
352
+ var import_jsx_runtime = require("react/jsx-runtime");
353
+ function Icon({ name, size = 24, color }) {
354
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_MaterialIcons.default, { name, size, color });
355
+ }
356
+
357
+ // src/sheet/steps/OfferStep.tsx
358
+ var import_jsx_runtime2 = require("react/jsx-runtime");
359
+ function OfferStep() {
360
+ const {
361
+ theme,
362
+ balance,
363
+ balanceLoading,
364
+ bundles,
365
+ bundlesLoading,
366
+ bundlesError,
367
+ selectedBundle,
368
+ setSelectedBundle,
369
+ goToPayment
370
+ } = useJaza();
371
+ const { colors, spacing: spacing2, radius: radius2 } = theme;
372
+ const styles = import_react_native.StyleSheet.create({
373
+ balanceCard: {
374
+ backgroundColor: colors.surfaceContainer,
375
+ borderRadius: radius2.xl,
376
+ padding: spacing2.md,
377
+ marginBottom: spacing2.lg
378
+ },
379
+ balanceLabel: {
380
+ color: colors.onSurfaceVariant,
381
+ fontSize: 14,
382
+ marginBottom: spacing2.xs
383
+ },
384
+ balanceRow: {
385
+ flexDirection: "row",
386
+ alignItems: "center",
387
+ gap: spacing2.sm
388
+ },
389
+ balanceValue: {
390
+ color: colors.onSurface,
391
+ fontSize: 48,
392
+ fontWeight: "700"
393
+ },
394
+ title: {
395
+ color: colors.onSurface,
396
+ fontSize: 24,
397
+ fontWeight: "600",
398
+ marginBottom: spacing2.md
399
+ },
400
+ bundle: {
401
+ flexDirection: "row",
402
+ alignItems: "center",
403
+ justifyContent: "space-between",
404
+ padding: spacing2.md,
405
+ borderRadius: radius2.xl,
406
+ backgroundColor: colors.bundleBg,
407
+ borderWidth: 1,
408
+ borderColor: colors.bundleBorder,
409
+ marginBottom: spacing2.sm
410
+ },
411
+ bundleSelected: {
412
+ borderColor: colors.bundleBorderSelected,
413
+ borderWidth: 2
414
+ },
415
+ bundleLabel: {
416
+ color: colors.onSurface,
417
+ fontSize: 18,
418
+ fontWeight: "600"
419
+ },
420
+ bundleLabelSelected: {
421
+ color: colors.primaryContainer
422
+ },
423
+ bundleSub: {
424
+ color: colors.onSurfaceVariant,
425
+ fontSize: 14,
426
+ marginTop: 2
427
+ },
428
+ bundleCredits: {
429
+ color: colors.onSurface,
430
+ fontSize: 18,
431
+ fontWeight: "600"
432
+ },
433
+ bundlePrice: {
434
+ color: colors.onSurfaceVariant,
435
+ fontSize: 12,
436
+ fontFamily: "monospace"
437
+ },
438
+ cta: {
439
+ backgroundColor: colors.primaryContainer,
440
+ borderRadius: radius2.full,
441
+ paddingVertical: spacing2.md,
442
+ marginTop: spacing2.lg,
443
+ flexDirection: "row",
444
+ alignItems: "center",
445
+ justifyContent: "center",
446
+ gap: spacing2.sm
447
+ },
448
+ ctaText: {
449
+ color: colors.onPrimaryContainer,
450
+ fontSize: 16,
451
+ fontWeight: "600"
452
+ },
453
+ loading: { padding: spacing2.xl, alignItems: "center" },
454
+ error: {
455
+ color: colors.error,
456
+ fontSize: 14,
457
+ marginBottom: spacing2.md,
458
+ lineHeight: 20
459
+ }
460
+ });
461
+ const renderBundle = (bundle) => {
462
+ const selected = selectedBundle?.id === bundle.id;
463
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
464
+ import_react_native.Pressable,
465
+ {
466
+ style: [styles.bundle, selected && styles.bundleSelected],
467
+ onPress: () => setSelectedBundle(bundle),
468
+ children: [
469
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native.View, { children: [
470
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
471
+ import_react_native.Text,
472
+ {
473
+ style: [styles.bundleLabel, selected && styles.bundleLabelSelected],
474
+ children: bundle.label ?? "Bundle"
475
+ }
476
+ ),
477
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native.Text, { style: styles.bundleSub, children: [
478
+ formatCredits(bundle.credits),
479
+ " credits"
480
+ ] })
481
+ ] }),
482
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native.View, { style: { alignItems: "flex-end" }, children: [
483
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native.Text, { style: styles.bundleCredits, children: formatCredits(bundle.credits) }),
484
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native.Text, { style: styles.bundlePrice, children: [
485
+ formatUsd(bundle.priceUsd),
486
+ " USD"
487
+ ] })
488
+ ] })
489
+ ]
490
+ },
491
+ bundle.id
492
+ );
493
+ };
494
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native.View, { children: [
495
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native.View, { style: styles.balanceCard, children: [
496
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native.Text, { style: styles.balanceLabel, children: "Current Balance" }),
497
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native.View, { style: styles.balanceRow, children: [
498
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Icon, { name: "bolt", size: 28, color: colors.primary }),
499
+ balanceLoading && balance === null ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native.ActivityIndicator, { color: colors.primary }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native.Text, { style: styles.balanceValue, children: balance !== null ? formatCredits(balance) : "\u2014" })
500
+ ] })
501
+ ] }),
502
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native.Text, { style: styles.title, children: "Top-up Credits" }),
503
+ bundlesError ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native.Text, { style: styles.error, children: bundlesError }) : null,
504
+ bundlesLoading ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native.View, { style: styles.loading, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native.ActivityIndicator, { color: colors.primary }) }) : bundles.map(renderBundle),
505
+ !bundlesLoading && !bundlesError && bundles.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native.Text, { style: styles.error, children: "No active bundles for this app." }) : null,
506
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
507
+ import_react_native.Pressable,
508
+ {
509
+ style: styles.cta,
510
+ onPress: goToPayment,
511
+ disabled: !selectedBundle,
512
+ children: [
513
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native.Text, { style: styles.ctaText, children: [
514
+ "Continue with ",
515
+ selectedBundle?.label ?? "bundle"
516
+ ] }),
517
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Icon, { name: "arrow-forward", size: 20, color: colors.onPrimaryContainer })
518
+ ]
519
+ }
520
+ )
521
+ ] });
522
+ }
523
+
524
+ // src/sheet/steps/PaymentStep.tsx
525
+ var import_react4 = require("react");
526
+ var import_react_native6 = require("react-native");
527
+
528
+ // src/components/PhoneDigitInput.tsx
529
+ var import_react2 = require("react");
530
+ var import_react_native2 = require("react-native");
531
+ var import_bottom_sheet = require("@gorhom/bottom-sheet");
532
+ var import_jsx_runtime3 = require("react/jsx-runtime");
533
+ var MAX_DIGITS = 10;
534
+ function PhoneDigitInput({
535
+ theme,
536
+ value,
537
+ onChangeText,
538
+ dialPressable,
539
+ trailing
540
+ }) {
541
+ const { colors, spacing: spacing2 } = theme;
542
+ const inputRef = (0, import_react2.useRef)(null);
543
+ const [focused, setFocused] = (0, import_react2.useState)(false);
544
+ const digits = value.replace(/\D/g, "").slice(0, MAX_DIGITS);
545
+ const activeIndex = digits.length >= MAX_DIGITS ? MAX_DIGITS - 1 : digits.length;
546
+ const styles = import_react_native2.StyleSheet.create({
547
+ label: {
548
+ color: colors.onSurfaceVariant,
549
+ fontSize: 12,
550
+ letterSpacing: 0.5,
551
+ marginBottom: spacing2.sm,
552
+ textTransform: "uppercase"
553
+ },
554
+ row: {
555
+ flexDirection: "row",
556
+ alignItems: "center",
557
+ width: "100%",
558
+ gap: spacing2.sm
559
+ },
560
+ slots: {
561
+ flex: 1,
562
+ flexDirection: "row",
563
+ alignItems: "center",
564
+ justifyContent: "space-between",
565
+ minHeight: 40
566
+ },
567
+ slot: {
568
+ flex: 1,
569
+ alignItems: "center",
570
+ justifyContent: "center"
571
+ },
572
+ slotChar: {
573
+ fontSize: 22,
574
+ fontWeight: "600",
575
+ color: colors.onSurfaceVariant,
576
+ fontVariant: ["tabular-nums"]
577
+ },
578
+ slotCharFocused: {
579
+ color: colors.primaryContainer
580
+ },
581
+ hiddenInput: {
582
+ position: "absolute",
583
+ opacity: 0,
584
+ height: 1,
585
+ width: 1
586
+ }
587
+ });
588
+ const focusInput = () => {
589
+ inputRef.current?.focus();
590
+ };
591
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native2.View, { children: [
592
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native2.Text, { style: styles.label, children: "Phone Number" }),
593
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native2.Pressable, { style: styles.row, onPress: focusInput, children: [
594
+ dialPressable,
595
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native2.View, { style: styles.slots, pointerEvents: "none", children: Array.from({ length: MAX_DIGITS }, (_, i) => {
596
+ const char = digits[i];
597
+ const isFocused = focused && i === activeIndex;
598
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native2.View, { style: styles.slot, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
599
+ import_react_native2.Text,
600
+ {
601
+ style: [
602
+ styles.slotChar,
603
+ isFocused ? styles.slotCharFocused : null
604
+ ],
605
+ children: char ?? "_"
606
+ }
607
+ ) }, i);
608
+ }) }),
609
+ trailing,
610
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
611
+ import_bottom_sheet.BottomSheetTextInput,
612
+ {
613
+ ref: inputRef,
614
+ style: styles.hiddenInput,
615
+ value: digits,
616
+ onChangeText: (text) => onChangeText(text.replace(/\D/g, "").slice(0, MAX_DIGITS)),
617
+ onFocus: () => setFocused(true),
618
+ onBlur: () => setFocused(false),
619
+ keyboardType: "phone-pad",
620
+ autoComplete: "tel",
621
+ textContentType: "telephoneNumber",
622
+ maxLength: MAX_DIGITS,
623
+ caretHidden: true
624
+ }
625
+ )
626
+ ] })
627
+ ] });
628
+ }
629
+
630
+ // src/sheet/CountryPickerSheet.tsx
631
+ var import_react3 = require("react");
632
+ var import_react_native4 = require("react-native");
633
+ var import_react_native_safe_area_context = require("react-native-safe-area-context");
634
+
635
+ // src/theme/listStyles.ts
636
+ var import_react_native3 = require("react-native");
637
+ function createSelectableRowStyles(theme) {
638
+ const { colors, spacing: spacing2 } = theme;
639
+ return import_react_native3.StyleSheet.create({
640
+ row: {
641
+ flexDirection: "row",
642
+ alignItems: "center",
643
+ justifyContent: "space-between",
644
+ paddingVertical: spacing2.md,
645
+ borderBottomWidth: import_react_native3.StyleSheet.hairlineWidth,
646
+ borderBottomColor: colors.outlineVariant,
647
+ gap: spacing2.md
648
+ },
649
+ rowSelected: {
650
+ borderBottomColor: colors.primaryContainer
651
+ },
652
+ label: {
653
+ color: colors.onSurface,
654
+ fontSize: 16,
655
+ fontWeight: "500",
656
+ flexShrink: 1
657
+ },
658
+ labelSelected: {
659
+ color: colors.primaryContainer,
660
+ fontWeight: "600"
661
+ },
662
+ sublabel: {
663
+ color: colors.onSurfaceVariant,
664
+ fontSize: 14
665
+ }
666
+ });
667
+ }
668
+
669
+ // src/sheet/CountryPickerSheet.tsx
670
+ var import_jsx_runtime4 = require("react/jsx-runtime");
671
+ function CountryPickerSheet({
672
+ visible,
673
+ onClose
674
+ }) {
675
+ const { theme, countries, selectedCountry, setSelectedCountry } = useJaza();
676
+ const { colors, spacing: spacing2, radius: radius2 } = theme;
677
+ const insets = (0, import_react_native_safe_area_context.useSafeAreaInsets)();
678
+ const [query, setQuery] = (0, import_react3.useState)("");
679
+ const rowStyles = createSelectableRowStyles(theme);
680
+ (0, import_react3.useEffect)(() => {
681
+ if (!visible) setQuery("");
682
+ }, [visible]);
683
+ const filtered = (0, import_react3.useMemo)(() => {
684
+ const q = query.trim().toLowerCase();
685
+ if (!q) return countries;
686
+ return countries.filter(
687
+ (c) => c.name.toLowerCase().includes(q) || c.iso2.toLowerCase().includes(q) || c.dialCode.includes(q)
688
+ );
689
+ }, [countries, query]);
690
+ const styles = import_react_native4.StyleSheet.create({
691
+ root: {
692
+ flex: 1,
693
+ justifyContent: "flex-end",
694
+ backgroundColor: colors.overlay
695
+ },
696
+ sheet: {
697
+ backgroundColor: colors.surface,
698
+ borderTopLeftRadius: radius2.xl,
699
+ borderTopRightRadius: radius2.xl,
700
+ paddingHorizontal: spacing2.gutter,
701
+ paddingTop: spacing2.md,
702
+ paddingBottom: Math.max(insets.bottom, spacing2.md),
703
+ maxHeight: "85%",
704
+ minHeight: 320
705
+ },
706
+ handle: {
707
+ width: 48,
708
+ height: 4,
709
+ borderRadius: 2,
710
+ backgroundColor: colors.surfaceContainerHighest,
711
+ alignSelf: "center",
712
+ marginBottom: spacing2.md
713
+ },
714
+ search: {
715
+ backgroundColor: colors.surfaceContainerHigh,
716
+ borderRadius: radius2.lg,
717
+ paddingHorizontal: spacing2.md,
718
+ minHeight: 48,
719
+ fontSize: 16,
720
+ lineHeight: 22,
721
+ color: colors.onSurface,
722
+ marginBottom: spacing2.md
723
+ },
724
+ list: {
725
+ flexGrow: 1,
726
+ flexShrink: 1
727
+ },
728
+ nameRow: {
729
+ flex: 1,
730
+ flexDirection: "row",
731
+ alignItems: "center",
732
+ gap: spacing2.md,
733
+ minWidth: 0
734
+ },
735
+ flag: {
736
+ fontSize: 24,
737
+ flexShrink: 0
738
+ },
739
+ dial: {
740
+ color: colors.onSurfaceVariant,
741
+ fontSize: 14,
742
+ fontFamily: "monospace",
743
+ flexShrink: 0,
744
+ marginLeft: spacing2.sm
745
+ },
746
+ empty: {
747
+ color: colors.onSurfaceVariant,
748
+ textAlign: "center",
749
+ paddingVertical: spacing2.lg
750
+ }
751
+ });
752
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
753
+ import_react_native4.Modal,
754
+ {
755
+ visible,
756
+ transparent: true,
757
+ animationType: "fade",
758
+ onRequestClose: onClose,
759
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
760
+ import_react_native4.KeyboardAvoidingView,
761
+ {
762
+ style: styles.root,
763
+ behavior: import_react_native4.Platform.OS === "ios" ? "padding" : void 0,
764
+ children: [
765
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Pressable, { style: import_react_native4.StyleSheet.absoluteFill, onPress: onClose }),
766
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native4.View, { style: styles.sheet, children: [
767
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.View, { style: styles.handle }),
768
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
769
+ import_react_native4.TextInput,
770
+ {
771
+ style: styles.search,
772
+ placeholder: "Search country",
773
+ placeholderTextColor: colors.onSurfaceVariant,
774
+ value: query,
775
+ onChangeText: setQuery,
776
+ autoCorrect: false,
777
+ autoCapitalize: "none",
778
+ clearButtonMode: "while-editing",
779
+ returnKeyType: "search"
780
+ }
781
+ ),
782
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
783
+ import_react_native4.FlatList,
784
+ {
785
+ style: styles.list,
786
+ data: filtered,
787
+ keyExtractor: (item) => item.id,
788
+ keyboardShouldPersistTaps: "handled",
789
+ keyboardDismissMode: "on-drag",
790
+ renderItem: ({ item }) => {
791
+ const selected = selectedCountry?.id === item.id;
792
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
793
+ import_react_native4.Pressable,
794
+ {
795
+ style: [rowStyles.row, selected && rowStyles.rowSelected],
796
+ onPress: () => {
797
+ setSelectedCountry(item);
798
+ onClose();
799
+ },
800
+ children: [
801
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native4.View, { style: styles.nameRow, children: [
802
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: styles.flag, children: item.flag }),
803
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
804
+ import_react_native4.Text,
805
+ {
806
+ style: [
807
+ rowStyles.label,
808
+ selected && rowStyles.labelSelected
809
+ ],
810
+ numberOfLines: 1,
811
+ ellipsizeMode: "tail",
812
+ children: item.name
813
+ }
814
+ )
815
+ ] }),
816
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native4.Text, { style: styles.dial, children: [
817
+ "+",
818
+ item.dialCode
819
+ ] })
820
+ ]
821
+ }
822
+ );
823
+ },
824
+ ListEmptyComponent: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: styles.empty, children: "No countries match" })
825
+ }
826
+ )
827
+ ] })
828
+ ]
829
+ }
830
+ )
831
+ }
832
+ );
833
+ }
834
+
835
+ // src/sheet/CurrencyPickerSheet.tsx
836
+ var import_react_native5 = require("react-native");
837
+ var import_react_native_safe_area_context2 = require("react-native-safe-area-context");
838
+ var import_jsx_runtime5 = require("react/jsx-runtime");
839
+ function CurrencyPickerSheet({
840
+ visible,
841
+ currencies,
842
+ selectedCode,
843
+ onSelect,
844
+ onClose
845
+ }) {
846
+ const { theme } = useJaza();
847
+ const { colors, spacing: spacing2, radius: radius2 } = theme;
848
+ const insets = (0, import_react_native_safe_area_context2.useSafeAreaInsets)();
849
+ const rowStyles = createSelectableRowStyles(theme);
850
+ const styles = import_react_native5.StyleSheet.create({
851
+ backdrop: {
852
+ flex: 1,
853
+ backgroundColor: colors.overlay,
854
+ justifyContent: "flex-end"
855
+ },
856
+ sheet: {
857
+ backgroundColor: colors.surface,
858
+ borderTopLeftRadius: radius2.xl,
859
+ borderTopRightRadius: radius2.xl,
860
+ maxHeight: "50%",
861
+ paddingHorizontal: spacing2.gutter,
862
+ paddingTop: spacing2.md,
863
+ paddingBottom: insets.bottom + spacing2.md
864
+ },
865
+ handle: {
866
+ width: 48,
867
+ height: 4,
868
+ borderRadius: 2,
869
+ backgroundColor: colors.surfaceContainerHighest,
870
+ alignSelf: "center",
871
+ marginBottom: spacing2.md
872
+ },
873
+ title: {
874
+ color: colors.onSurface,
875
+ fontSize: 18,
876
+ fontWeight: "600",
877
+ marginBottom: spacing2.md
878
+ }
879
+ });
880
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
881
+ import_react_native5.Modal,
882
+ {
883
+ visible,
884
+ transparent: true,
885
+ animationType: "fade",
886
+ onRequestClose: onClose,
887
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Pressable, { style: styles.backdrop, onPress: onClose, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native5.Pressable, { style: styles.sheet, onPress: (e) => e.stopPropagation(), children: [
888
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.View, { style: styles.handle }),
889
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { style: styles.title, children: "Select currency" }),
890
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
891
+ import_react_native5.FlatList,
892
+ {
893
+ data: currencies,
894
+ keyExtractor: (item) => item.code,
895
+ keyboardShouldPersistTaps: "handled",
896
+ renderItem: ({ item }) => {
897
+ const selected = selectedCode === item.code;
898
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
899
+ import_react_native5.Pressable,
900
+ {
901
+ style: [rowStyles.row, selected && rowStyles.rowSelected],
902
+ onPress: () => onSelect(item.code),
903
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
904
+ import_react_native5.Text,
905
+ {
906
+ style: [rowStyles.label, selected && rowStyles.labelSelected],
907
+ children: item.code
908
+ }
909
+ )
910
+ }
911
+ );
912
+ }
913
+ }
914
+ )
915
+ ] }) })
916
+ }
917
+ );
918
+ }
919
+
920
+ // src/sheet/steps/PaymentStep.tsx
921
+ var import_jsx_runtime6 = require("react/jsx-runtime");
922
+ function PaymentStep() {
923
+ const {
924
+ theme,
925
+ balance,
926
+ selectedBundle,
927
+ selectedCountry,
928
+ selectedCurrencyCode,
929
+ setSelectedCurrencyCode,
930
+ phoneNational,
931
+ setPhoneNational,
932
+ predict,
933
+ predictLoading,
934
+ predictError,
935
+ quote,
936
+ quoteLoading,
937
+ quoteError,
938
+ goToOffer,
939
+ submitDeposit
940
+ } = useJaza();
941
+ const { colors, spacing: spacing2, radius: radius2 } = theme;
942
+ const [countryPickerOpen, setCountryPickerOpen] = (0, import_react4.useState)(false);
943
+ const [currencyPickerOpen, setCurrencyPickerOpen] = (0, import_react4.useState)(false);
944
+ const currencyOptions = (0, import_react4.useMemo)(() => {
945
+ if (predict?.currencies?.length) {
946
+ return predict.currencies;
947
+ }
948
+ return selectedCountry?.currencies ?? [];
949
+ }, [predict, selectedCountry]);
950
+ const showCurrencyPicker = currencyOptions.length > 1;
951
+ const formattedQuote = quote ? formatCurrencyAmount(quote.totalLocal, quote.currencyCode) : null;
952
+ const buyLabel = quote ? `Buy ${formattedQuote}` : quoteLoading ? "Loading\u2026" : selectedBundle ? `Buy ${formatUsd(selectedBundle.priceUsd)}` : "Buy";
953
+ const styles = import_react_native6.StyleSheet.create({
954
+ header: {
955
+ flexDirection: "row",
956
+ alignItems: "center",
957
+ justifyContent: "space-between",
958
+ marginBottom: spacing2.lg
959
+ },
960
+ headerLeft: {
961
+ flexDirection: "row",
962
+ alignItems: "center",
963
+ gap: spacing2.sm,
964
+ flex: 1
965
+ },
966
+ backBtn: {
967
+ width: 40,
968
+ height: 40,
969
+ borderRadius: radius2.full,
970
+ backgroundColor: colors.surfaceContainer,
971
+ alignItems: "center",
972
+ justifyContent: "center"
973
+ },
974
+ headerTitle: {
975
+ color: colors.onSurface,
976
+ fontSize: 24,
977
+ fontWeight: "600",
978
+ flexShrink: 1
979
+ },
980
+ balanceChip: {
981
+ flexDirection: "row",
982
+ alignItems: "center",
983
+ gap: spacing2.xs,
984
+ backgroundColor: colors.surfaceContainer,
985
+ paddingHorizontal: spacing2.sm,
986
+ paddingVertical: spacing2.xs,
987
+ borderRadius: radius2.full
988
+ },
989
+ balanceChipText: {
990
+ color: colors.primary,
991
+ fontSize: 12,
992
+ fontWeight: "500"
993
+ },
994
+ dialBtn: {
995
+ flexDirection: "row",
996
+ alignItems: "center",
997
+ gap: spacing2.xs
998
+ },
999
+ dialText: {
1000
+ color: colors.primaryContainer,
1001
+ fontSize: 22,
1002
+ fontWeight: "700"
1003
+ },
1004
+ providerText: {
1005
+ color: colors.primaryContainer,
1006
+ fontSize: 14,
1007
+ fontWeight: "500",
1008
+ marginTop: spacing2.sm
1009
+ },
1010
+ providerError: {
1011
+ color: colors.error,
1012
+ fontSize: 14,
1013
+ marginTop: spacing2.sm
1014
+ },
1015
+ payCard: {
1016
+ backgroundColor: colors.surfaceContainerLow,
1017
+ borderRadius: radius2.xl,
1018
+ borderWidth: 1,
1019
+ borderColor: colors.outlineVariant,
1020
+ padding: spacing2.md,
1021
+ marginTop: spacing2.lg
1022
+ },
1023
+ payRow: {
1024
+ flexDirection: "row",
1025
+ alignItems: "center",
1026
+ justifyContent: "space-between",
1027
+ marginBottom: spacing2.md
1028
+ },
1029
+ currencyBtn: {
1030
+ flexDirection: "row",
1031
+ alignItems: "center",
1032
+ gap: spacing2.xs
1033
+ },
1034
+ currencyCode: {
1035
+ color: colors.primaryContainer,
1036
+ fontSize: 18,
1037
+ fontWeight: "600"
1038
+ },
1039
+ amountText: {
1040
+ color: colors.onSurface,
1041
+ fontSize: 18,
1042
+ fontWeight: "600"
1043
+ },
1044
+ cta: {
1045
+ backgroundColor: colors.primaryContainer,
1046
+ borderRadius: radius2.full,
1047
+ paddingVertical: spacing2.md,
1048
+ flexDirection: "row",
1049
+ alignItems: "center",
1050
+ justifyContent: "center",
1051
+ gap: spacing2.sm
1052
+ },
1053
+ ctaDisabled: { opacity: 0.5 },
1054
+ ctaText: {
1055
+ color: colors.onPrimaryContainer,
1056
+ fontSize: 16,
1057
+ fontWeight: "600"
1058
+ },
1059
+ quoteError: {
1060
+ color: colors.error,
1061
+ fontSize: 12,
1062
+ marginBottom: spacing2.sm
1063
+ }
1064
+ });
1065
+ const canSubmit = !!predict && !!quote && !quoteLoading && !predictLoading && phoneNational.replace(/\D/g, "").length >= 6;
1066
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { children: [
1067
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: styles.header, children: [
1068
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: styles.headerLeft, children: [
1069
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Pressable, { style: styles.backBtn, onPress: goToOffer, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Icon, { name: "arrow-back", size: 22, color: colors.onSurfaceVariant }) }),
1070
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: styles.headerTitle, numberOfLines: 1, children: selectedBundle?.label ?? "Top-up Balance" })
1071
+ ] }),
1072
+ balance !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: styles.balanceChip, children: [
1073
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Icon, { name: "bolt", size: 16, color: colors.primary }),
1074
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: styles.balanceChipText, children: balance.toLocaleString() })
1075
+ ] }) : null
1076
+ ] }),
1077
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1078
+ PhoneDigitInput,
1079
+ {
1080
+ theme,
1081
+ value: phoneNational,
1082
+ onChangeText: setPhoneNational,
1083
+ dialPressable: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1084
+ import_react_native6.Pressable,
1085
+ {
1086
+ style: styles.dialBtn,
1087
+ onPress: () => setCountryPickerOpen(true),
1088
+ children: [
1089
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.Text, { style: styles.dialText, children: [
1090
+ "+",
1091
+ selectedCountry?.dialCode ?? "\u2026"
1092
+ ] }),
1093
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Icon, { name: "expand-more", size: 24, color: colors.primaryContainer })
1094
+ ]
1095
+ }
1096
+ ),
1097
+ trailing: predict && !predictLoading ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Icon, { name: "check-circle", size: 24, color: colors.primary }) : predictLoading ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.ActivityIndicator, { color: colors.primary, size: "small" }) : null
1098
+ }
1099
+ ),
1100
+ predict ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: styles.providerText, children: predict.provider.displayName }) : predictError ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: styles.providerError, children: predictError }) : null,
1101
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: styles.payCard, children: [
1102
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: styles.payRow, children: [
1103
+ showCurrencyPicker ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1104
+ import_react_native6.Pressable,
1105
+ {
1106
+ style: styles.currencyBtn,
1107
+ onPress: () => setCurrencyPickerOpen(true),
1108
+ children: [
1109
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: styles.currencyCode, children: selectedCurrencyCode ?? "\u2014" }),
1110
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Icon, { name: "expand-more", size: 20, color: colors.primaryContainer })
1111
+ ]
1112
+ }
1113
+ ) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: styles.currencyCode, children: selectedCurrencyCode ?? quote?.currencyCode ?? "USD" }),
1114
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: styles.amountText, children: formattedQuote ?? (selectedBundle ? formatUsd(selectedBundle.priceUsd) : "\u2014") })
1115
+ ] }),
1116
+ quoteError ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: styles.quoteError, children: quoteError }) : null,
1117
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1118
+ import_react_native6.Pressable,
1119
+ {
1120
+ style: [styles.cta, !canSubmit && styles.ctaDisabled],
1121
+ onPress: () => void submitDeposit(),
1122
+ disabled: !canSubmit,
1123
+ children: [
1124
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Icon, { name: "lock", size: 18, color: colors.onPrimaryContainer }),
1125
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: styles.ctaText, children: buyLabel })
1126
+ ]
1127
+ }
1128
+ )
1129
+ ] }),
1130
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1131
+ CountryPickerSheet,
1132
+ {
1133
+ visible: countryPickerOpen,
1134
+ onClose: () => setCountryPickerOpen(false)
1135
+ }
1136
+ ),
1137
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1138
+ CurrencyPickerSheet,
1139
+ {
1140
+ visible: currencyPickerOpen,
1141
+ currencies: currencyOptions,
1142
+ selectedCode: selectedCurrencyCode,
1143
+ onSelect: (code) => {
1144
+ setSelectedCurrencyCode(code);
1145
+ setCurrencyPickerOpen(false);
1146
+ },
1147
+ onClose: () => setCurrencyPickerOpen(false)
1148
+ }
1149
+ )
1150
+ ] });
1151
+ }
1152
+
1153
+ // src/sheet/steps/ResultStep.tsx
1154
+ var import_react6 = require("react");
1155
+ var import_react_native8 = require("react-native");
1156
+
1157
+ // src/components/ProcessingSpinner.tsx
1158
+ var import_react5 = require("react");
1159
+ var import_react_native7 = require("react-native");
1160
+ var import_jsx_runtime7 = require("react/jsx-runtime");
1161
+ function ProcessingSpinner({
1162
+ theme,
1163
+ size = 64
1164
+ }) {
1165
+ const { colors } = theme;
1166
+ const spin = (0, import_react5.useRef)(new import_react_native7.Animated.Value(0)).current;
1167
+ (0, import_react5.useEffect)(() => {
1168
+ const loop = import_react_native7.Animated.loop(
1169
+ import_react_native7.Animated.timing(spin, {
1170
+ toValue: 1,
1171
+ duration: 900,
1172
+ easing: import_react_native7.Easing.linear,
1173
+ useNativeDriver: true
1174
+ })
1175
+ );
1176
+ loop.start();
1177
+ return () => loop.stop();
1178
+ }, [spin]);
1179
+ const rotate = spin.interpolate({
1180
+ inputRange: [0, 1],
1181
+ outputRange: ["0deg", "360deg"]
1182
+ });
1183
+ const ring = size;
1184
+ const stroke = 4;
1185
+ const styles = import_react_native7.StyleSheet.create({
1186
+ wrap: {
1187
+ width: ring,
1188
+ height: ring,
1189
+ alignItems: "center",
1190
+ justifyContent: "center"
1191
+ },
1192
+ ring: {
1193
+ position: "absolute",
1194
+ width: ring,
1195
+ height: ring,
1196
+ borderRadius: ring / 2,
1197
+ borderWidth: stroke,
1198
+ borderColor: colors.surfaceContainerHighest,
1199
+ borderTopColor: colors.primary
1200
+ },
1201
+ icon: {
1202
+ position: "absolute",
1203
+ alignItems: "center",
1204
+ justifyContent: "center"
1205
+ }
1206
+ });
1207
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native7.View, { style: styles.wrap, children: [
1208
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Animated.View, { style: [styles.ring, { transform: [{ rotate }] }] }),
1209
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.View, { style: styles.icon, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Icon, { name: "lock", size: Math.round(size * 0.38), color: colors.primary }) })
1210
+ ] });
1211
+ }
1212
+
1213
+ // src/sheet/steps/ResultStep.tsx
1214
+ var import_jsx_runtime8 = require("react/jsx-runtime");
1215
+ function ResultStep() {
1216
+ const {
1217
+ theme,
1218
+ resultPhase,
1219
+ selectedBundle,
1220
+ deposit,
1221
+ failureReason,
1222
+ depositError,
1223
+ closeTopUp,
1224
+ retryPayment
1225
+ } = useJaza();
1226
+ const { colors, spacing: spacing2, radius: radius2 } = theme;
1227
+ const scale = (0, import_react6.useRef)(new import_react_native8.Animated.Value(0)).current;
1228
+ (0, import_react6.useEffect)(() => {
1229
+ if (resultPhase === "success") {
1230
+ scale.setValue(0);
1231
+ import_react_native8.Animated.spring(scale, {
1232
+ toValue: 1,
1233
+ useNativeDriver: true,
1234
+ friction: 6
1235
+ }).start();
1236
+ }
1237
+ }, [resultPhase, scale]);
1238
+ const styles = import_react_native8.StyleSheet.create({
1239
+ container: {
1240
+ alignItems: "center",
1241
+ paddingVertical: spacing2.xl
1242
+ },
1243
+ spinnerWrap: {
1244
+ marginBottom: spacing2.lg
1245
+ },
1246
+ title: {
1247
+ color: colors.onSurface,
1248
+ fontSize: 20,
1249
+ fontWeight: "600",
1250
+ textAlign: "center"
1251
+ },
1252
+ titlePulse: {
1253
+ opacity: 0.9
1254
+ },
1255
+ subtitle: {
1256
+ color: colors.onSurfaceVariant,
1257
+ fontSize: 14,
1258
+ marginTop: spacing2.xs,
1259
+ textAlign: "center"
1260
+ },
1261
+ successIcon: {
1262
+ width: 80,
1263
+ height: 80,
1264
+ borderRadius: radius2.full,
1265
+ backgroundColor: `${colors.success}20`,
1266
+ alignItems: "center",
1267
+ justifyContent: "center",
1268
+ marginBottom: spacing2.lg
1269
+ },
1270
+ failIcon: {
1271
+ width: 80,
1272
+ height: 80,
1273
+ borderRadius: radius2.full,
1274
+ backgroundColor: `${colors.error}20`,
1275
+ alignItems: "center",
1276
+ justifyContent: "center",
1277
+ marginBottom: spacing2.lg
1278
+ },
1279
+ doneBtn: {
1280
+ backgroundColor: colors.surfaceContainerHigh,
1281
+ borderRadius: radius2.full,
1282
+ paddingHorizontal: spacing2.xl,
1283
+ paddingVertical: spacing2.sm,
1284
+ marginTop: spacing2.lg
1285
+ },
1286
+ doneText: {
1287
+ color: colors.onSurface,
1288
+ fontSize: 12,
1289
+ fontWeight: "500",
1290
+ letterSpacing: 0.5
1291
+ },
1292
+ retryBtn: {
1293
+ backgroundColor: colors.primaryContainer,
1294
+ borderRadius: radius2.full,
1295
+ paddingHorizontal: spacing2.xl,
1296
+ paddingVertical: spacing2.sm,
1297
+ marginTop: spacing2.md
1298
+ },
1299
+ retryText: {
1300
+ color: colors.onPrimaryContainer,
1301
+ fontSize: 14,
1302
+ fontWeight: "600"
1303
+ }
1304
+ });
1305
+ if (resultPhase === "loading") {
1306
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native8.View, { style: styles.container, children: [
1307
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.View, { style: styles.spinnerWrap, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ProcessingSpinner, { theme, size: 64 }) }),
1308
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: [styles.title, styles.titlePulse], children: "Processing payment..." }),
1309
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: styles.subtitle, children: "Please authorize on your device" })
1310
+ ] });
1311
+ }
1312
+ if (resultPhase === "failure") {
1313
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native8.View, { style: styles.container, children: [
1314
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.View, { style: styles.failIcon, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Icon, { name: "error-outline", size: 48, color: colors.error }) }),
1315
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: styles.title, children: "Payment failed" }),
1316
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: styles.subtitle, children: failureReason ?? depositError ?? "Something went wrong" }),
1317
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Pressable, { style: styles.retryBtn, onPress: retryPayment, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: styles.retryText, children: "Try again" }) }),
1318
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Pressable, { style: styles.doneBtn, onPress: closeTopUp, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: styles.doneText, children: "Dismiss" }) })
1319
+ ] });
1320
+ }
1321
+ const credits = deposit?.credits ?? selectedBundle?.credits ?? 0;
1322
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native8.View, { style: styles.container, children: [
1323
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Animated.View, { style: [styles.successIcon, { transform: [{ scale }] }], children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Icon, { name: "check-circle", size: 48, color: colors.success }) }),
1324
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: styles.title, children: "Top-up Successful" }),
1325
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native8.Text, { style: styles.subtitle, children: [
1326
+ formatCredits(credits),
1327
+ " credits have been added to your balance."
1328
+ ] }),
1329
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Pressable, { style: styles.doneBtn, onPress: closeTopUp, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.Text, { style: styles.doneText, children: "Done" }) })
1330
+ ] });
1331
+ }
1332
+
1333
+ // src/sheet/TopUpBottomSheet.tsx
1334
+ var import_jsx_runtime9 = require("react/jsx-runtime");
1335
+ function TopUpBottomSheet() {
1336
+ const { theme, sheetOpen, step, closeTopUp } = useJaza();
1337
+ const { colors, spacing: spacing2 } = theme;
1338
+ const insets = (0, import_react_native_safe_area_context3.useSafeAreaInsets)();
1339
+ const ref = (0, import_react7.useRef)(null);
1340
+ const snapPoints = (0, import_react7.useMemo)(() => ["92%"], []);
1341
+ const renderBackdrop = (0, import_react7.useCallback)(
1342
+ (props) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1343
+ import_bottom_sheet2.BottomSheetBackdrop,
1344
+ {
1345
+ ...props,
1346
+ disappearsOnIndex: -1,
1347
+ appearsOnIndex: 0,
1348
+ opacity: 0.6,
1349
+ pressBehavior: "close"
1350
+ }
1351
+ ),
1352
+ []
1353
+ );
1354
+ const styles = import_react_native9.StyleSheet.create({
1355
+ root: {
1356
+ flex: 1
1357
+ },
1358
+ handle: {
1359
+ width: 48,
1360
+ height: 4,
1361
+ borderRadius: 2,
1362
+ backgroundColor: colors.surfaceContainerHighest,
1363
+ alignSelf: "center",
1364
+ marginTop: spacing2.sm,
1365
+ marginBottom: spacing2.md
1366
+ },
1367
+ content: {
1368
+ paddingHorizontal: spacing2.gutter,
1369
+ paddingBottom: insets.bottom + spacing2.xl
1370
+ }
1371
+ });
1372
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1373
+ import_react_native9.Modal,
1374
+ {
1375
+ visible: sheetOpen,
1376
+ transparent: true,
1377
+ animationType: "fade",
1378
+ statusBarTranslucent: true,
1379
+ onRequestClose: closeTopUp,
1380
+ children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native_gesture_handler.GestureHandlerRootView, { style: styles.root, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1381
+ import_bottom_sheet2.default,
1382
+ {
1383
+ ref,
1384
+ index: 0,
1385
+ snapPoints,
1386
+ enablePanDownToClose: true,
1387
+ onClose: closeTopUp,
1388
+ backdropComponent: renderBackdrop,
1389
+ keyboardBehavior: "interactive",
1390
+ keyboardBlurBehavior: "restore",
1391
+ android_keyboardInputMode: "adjustResize",
1392
+ backgroundStyle: { backgroundColor: colors.surface },
1393
+ handleComponent: () => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native9.View, { style: styles.handle }),
1394
+ children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
1395
+ import_bottom_sheet2.BottomSheetScrollView,
1396
+ {
1397
+ keyboardShouldPersistTaps: "handled",
1398
+ contentContainerStyle: styles.content,
1399
+ children: [
1400
+ step === "offer" ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(OfferStep, {}) : null,
1401
+ step === "payment" ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(PaymentStep, {}) : null,
1402
+ step === "processing" ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ResultStep, {}) : null
1403
+ ]
1404
+ }
1405
+ )
1406
+ }
1407
+ ) })
1408
+ }
1409
+ );
1410
+ }
1411
+
1412
+ // src/provider/JazaProvider.tsx
1413
+ var import_jsx_runtime10 = require("react/jsx-runtime");
1414
+ var PREDICT_DEBOUNCE_MS = 500;
1415
+ var POLL_INTERVAL_MS = 2e3;
1416
+ var POLL_TIMEOUT_MS = 12e4;
1417
+ function JazaProvider({
1418
+ publishableKey,
1419
+ apiBaseUrl,
1420
+ getBalance,
1421
+ onTopUpComplete,
1422
+ theme: themePreference = "system",
1423
+ children
1424
+ }) {
1425
+ const client = (0, import_react8.useMemo)(
1426
+ () => new PublicClient({ publishableKey, apiBaseUrl }),
1427
+ [publishableKey, apiBaseUrl]
1428
+ );
1429
+ const [systemScheme, setSystemScheme] = (0, import_react8.useState)(
1430
+ () => {
1431
+ const scheme = import_react_native10.Appearance.getColorScheme();
1432
+ return scheme === "dark" ? "dark" : "light";
1433
+ }
1434
+ );
1435
+ (0, import_react8.useEffect)(() => {
1436
+ const sub = import_react_native10.Appearance.addChangeListener(({ colorScheme }) => {
1437
+ setSystemScheme(colorScheme === "dark" ? "dark" : "light");
1438
+ });
1439
+ return () => sub.remove();
1440
+ }, []);
1441
+ const theme = (0, import_react8.useMemo)(
1442
+ () => resolveTheme(themePreference, systemScheme),
1443
+ [themePreference, systemScheme]
1444
+ );
1445
+ const [balance, setBalance] = (0, import_react8.useState)(null);
1446
+ const [balanceLoading, setBalanceLoading] = (0, import_react8.useState)(false);
1447
+ const [balanceError, setBalanceError] = (0, import_react8.useState)(null);
1448
+ const refreshBalance = (0, import_react8.useCallback)(async () => {
1449
+ setBalanceLoading(true);
1450
+ setBalanceError(null);
1451
+ try {
1452
+ const value2 = await getBalance();
1453
+ setBalance(value2);
1454
+ } catch (err) {
1455
+ setBalanceError(
1456
+ err instanceof Error ? err.message : "Failed to load balance"
1457
+ );
1458
+ } finally {
1459
+ setBalanceLoading(false);
1460
+ }
1461
+ }, [getBalance]);
1462
+ (0, import_react8.useEffect)(() => {
1463
+ void refreshBalance();
1464
+ }, [refreshBalance]);
1465
+ const [sheetOpen, setSheetOpen] = (0, import_react8.useState)(false);
1466
+ const [step, setStep] = (0, import_react8.useState)("offer");
1467
+ const [resultPhase, setResultPhase] = (0, import_react8.useState)("loading");
1468
+ const [topUpToken, setTopUpToken] = (0, import_react8.useState)(null);
1469
+ const [bundles, setBundles] = (0, import_react8.useState)([]);
1470
+ const [bundlesLoading, setBundlesLoading] = (0, import_react8.useState)(false);
1471
+ const [bundlesError, setBundlesError] = (0, import_react8.useState)(null);
1472
+ const [selectedBundle, setSelectedBundle] = (0, import_react8.useState)(null);
1473
+ const [countries, setCountries] = (0, import_react8.useState)([]);
1474
+ const [selectedCountry, setSelectedCountryState] = (0, import_react8.useState)(null);
1475
+ const [selectedCurrencyCode, setSelectedCurrencyCode] = (0, import_react8.useState)(null);
1476
+ const [phoneNational, setPhoneNational] = (0, import_react8.useState)("");
1477
+ const [predict, setPredict] = (0, import_react8.useState)(
1478
+ null
1479
+ );
1480
+ const [predictLoading, setPredictLoading] = (0, import_react8.useState)(false);
1481
+ const [predictError, setPredictError] = (0, import_react8.useState)(null);
1482
+ const [quote, setQuote] = (0, import_react8.useState)(null);
1483
+ const [quoteLoading, setQuoteLoading] = (0, import_react8.useState)(false);
1484
+ const [quoteError, setQuoteError] = (0, import_react8.useState)(null);
1485
+ const [deposit, setDeposit] = (0, import_react8.useState)(null);
1486
+ const [depositError, setDepositError] = (0, import_react8.useState)(null);
1487
+ const [failureReason, setFailureReason] = (0, import_react8.useState)(null);
1488
+ const predictTimer = (0, import_react8.useRef)(null);
1489
+ const pollTimer = (0, import_react8.useRef)(null);
1490
+ const pollStarted = (0, import_react8.useRef)(null);
1491
+ const clearPoll = (0, import_react8.useCallback)(() => {
1492
+ if (pollTimer.current) {
1493
+ clearInterval(pollTimer.current);
1494
+ pollTimer.current = null;
1495
+ }
1496
+ pollStarted.current = null;
1497
+ }, []);
1498
+ const resetPaymentState = (0, import_react8.useCallback)(() => {
1499
+ setPredict(null);
1500
+ setPredictError(null);
1501
+ setQuote(null);
1502
+ setQuoteError(null);
1503
+ setDeposit(null);
1504
+ setDepositError(null);
1505
+ setFailureReason(null);
1506
+ setResultPhase("loading");
1507
+ }, []);
1508
+ const setSelectedCountry = (0, import_react8.useCallback)((country) => {
1509
+ setSelectedCountryState(country);
1510
+ setPhoneNational("");
1511
+ setPredict(null);
1512
+ setPredictError(null);
1513
+ if (country && country.currencies.length > 0) {
1514
+ const codes = country.currencies.map((c) => c.code);
1515
+ setSelectedCurrencyCode(pickDefaultCurrencyCode(codes));
1516
+ } else {
1517
+ setSelectedCurrencyCode(null);
1518
+ }
1519
+ }, []);
1520
+ const loadSessionData = (0, import_react8.useCallback)(async () => {
1521
+ setBundlesLoading(true);
1522
+ setBundlesError(null);
1523
+ try {
1524
+ const [bundleList, catalogCountries] = await Promise.all([
1525
+ client.listBundles(),
1526
+ client.listCountries()
1527
+ ]);
1528
+ const active = bundleList.filter((b) => b.isActive).sort((a, b) => a.sortOrder - b.sortOrder);
1529
+ setBundles(active);
1530
+ if (active.length > 0) {
1531
+ setSelectedBundle(active[0]);
1532
+ } else {
1533
+ setSelectedBundle(null);
1534
+ }
1535
+ const enriched = enrichCountries(catalogCountries);
1536
+ setCountries(enriched);
1537
+ if (enriched.length > 0) {
1538
+ setSelectedCountryState((prev) => {
1539
+ if (prev) return prev;
1540
+ return enriched.find((c) => c.iso2 === "CD") ?? enriched[0];
1541
+ });
1542
+ setSelectedCurrencyCode((prev) => {
1543
+ if (prev) return prev;
1544
+ const country = enriched.find((c) => c.iso2 === "CD") ?? enriched[0];
1545
+ const codes = country.currencies.map((c) => c.code);
1546
+ return pickDefaultCurrencyCode(codes);
1547
+ });
1548
+ }
1549
+ } catch (err) {
1550
+ setBundles([]);
1551
+ setSelectedBundle(null);
1552
+ setBundlesError(
1553
+ err instanceof Error ? err.message : "Failed to load bundles"
1554
+ );
1555
+ } finally {
1556
+ setBundlesLoading(false);
1557
+ }
1558
+ }, [client]);
1559
+ const openTopUp = (0, import_react8.useCallback)(
1560
+ async (token) => {
1561
+ client.setTopUpToken(token);
1562
+ setTopUpToken(token);
1563
+ setStep("offer");
1564
+ resetPaymentState();
1565
+ setSelectedCountryState(null);
1566
+ setSelectedCurrencyCode(null);
1567
+ setPhoneNational("");
1568
+ setBundlesError(null);
1569
+ setSheetOpen(true);
1570
+ void loadSessionData();
1571
+ void refreshBalance();
1572
+ },
1573
+ [client, loadSessionData, refreshBalance, resetPaymentState]
1574
+ );
1575
+ const closeTopUp = (0, import_react8.useCallback)(() => {
1576
+ clearPoll();
1577
+ setSheetOpen(false);
1578
+ client.setTopUpToken(null);
1579
+ setTopUpToken(null);
1580
+ setStep("offer");
1581
+ resetPaymentState();
1582
+ }, [clearPoll, client, resetPaymentState]);
1583
+ const goToOffer = (0, import_react8.useCallback)(() => {
1584
+ clearPoll();
1585
+ setStep("offer");
1586
+ resetPaymentState();
1587
+ }, [clearPoll, resetPaymentState]);
1588
+ const goToPayment = (0, import_react8.useCallback)(() => {
1589
+ setStep("payment");
1590
+ resetPaymentState();
1591
+ }, [resetPaymentState]);
1592
+ const startPoll = (0, import_react8.useCallback)(
1593
+ (depositId, credits) => {
1594
+ clearPoll();
1595
+ pollStarted.current = Date.now();
1596
+ pollTimer.current = setInterval(() => {
1597
+ void (async () => {
1598
+ if (pollStarted.current && Date.now() - pollStarted.current > POLL_TIMEOUT_MS) {
1599
+ clearPoll();
1600
+ setResultPhase("failure");
1601
+ setFailureReason("Payment timed out. Please try again.");
1602
+ return;
1603
+ }
1604
+ try {
1605
+ const updated = await client.getDeposit(depositId);
1606
+ setDeposit(updated);
1607
+ if (updated.status === "COMPLETED") {
1608
+ clearPoll();
1609
+ setResultPhase("success");
1610
+ await refreshBalance();
1611
+ onTopUpComplete?.({
1612
+ depositId: updated.id,
1613
+ credits,
1614
+ status: updated.status
1615
+ });
1616
+ } else if (isDepositTerminal(updated.status)) {
1617
+ clearPoll();
1618
+ setResultPhase("failure");
1619
+ setFailureReason(
1620
+ updated.failureReason ?? `Payment ${updated.status.toLowerCase()}`
1621
+ );
1622
+ }
1623
+ } catch {
1624
+ }
1625
+ })();
1626
+ }, POLL_INTERVAL_MS);
1627
+ },
1628
+ [clearPoll, client, onTopUpComplete, refreshBalance]
1629
+ );
1630
+ const submitDeposit = (0, import_react8.useCallback)(async () => {
1631
+ if (!selectedBundle || !predict || !quote || !selectedCountry) return;
1632
+ const e164 = buildE164(selectedCountry.dialCode, phoneNational);
1633
+ setStep("processing");
1634
+ setResultPhase("loading");
1635
+ setDepositError(null);
1636
+ try {
1637
+ const created = await client.createDeposit({
1638
+ bundleId: selectedBundle.id,
1639
+ currencyCode: quote.currencyCode,
1640
+ paymentGatewayCode: predict.provider.code,
1641
+ phoneNumber: e164
1642
+ });
1643
+ setDeposit(created);
1644
+ if (created.status === "COMPLETED") {
1645
+ setResultPhase("success");
1646
+ await refreshBalance();
1647
+ onTopUpComplete?.({
1648
+ depositId: created.id,
1649
+ credits: created.credits,
1650
+ status: created.status
1651
+ });
1652
+ return;
1653
+ }
1654
+ if (isDepositTerminal(created.status)) {
1655
+ setResultPhase("failure");
1656
+ setFailureReason(
1657
+ created.failureReason ?? `Payment ${created.status.toLowerCase()}`
1658
+ );
1659
+ return;
1660
+ }
1661
+ startPoll(created.id, created.credits);
1662
+ } catch (err) {
1663
+ setResultPhase("failure");
1664
+ setDepositError(
1665
+ err instanceof Error ? err.message : "Failed to start payment"
1666
+ );
1667
+ setFailureReason(
1668
+ err instanceof Error ? err.message : "Failed to start payment"
1669
+ );
1670
+ }
1671
+ }, [
1672
+ client,
1673
+ onTopUpComplete,
1674
+ phoneNational,
1675
+ predict,
1676
+ quote,
1677
+ refreshBalance,
1678
+ selectedBundle,
1679
+ selectedCountry,
1680
+ startPoll
1681
+ ]);
1682
+ const retryPayment = (0, import_react8.useCallback)(() => {
1683
+ clearPoll();
1684
+ setStep("payment");
1685
+ setResultPhase("loading");
1686
+ setDepositError(null);
1687
+ setFailureReason(null);
1688
+ }, [clearPoll]);
1689
+ (0, import_react8.useEffect)(() => {
1690
+ if (step !== "payment" || !selectedCountry) return;
1691
+ const digits = phoneNational.replace(/\D/g, "");
1692
+ if (digits.length < 6) {
1693
+ setPredict(null);
1694
+ setPredictError(null);
1695
+ return;
1696
+ }
1697
+ if (predictTimer.current) clearTimeout(predictTimer.current);
1698
+ predictTimer.current = setTimeout(() => {
1699
+ void (async () => {
1700
+ setPredictLoading(true);
1701
+ setPredictError(null);
1702
+ try {
1703
+ const e164 = buildE164(selectedCountry.dialCode, phoneNational);
1704
+ const result = await client.predictProvider(e164);
1705
+ setPredict(result);
1706
+ const codes = result.currencies.map((c) => c.code);
1707
+ if (selectedCurrencyCode && codes.includes(selectedCurrencyCode)) {
1708
+ } else {
1709
+ setSelectedCurrencyCode(pickDefaultCurrencyCode(codes));
1710
+ }
1711
+ } catch (err) {
1712
+ setPredict(null);
1713
+ setPredictError(
1714
+ err instanceof Error ? err.message : "Could not detect provider"
1715
+ );
1716
+ } finally {
1717
+ setPredictLoading(false);
1718
+ }
1719
+ })();
1720
+ }, PREDICT_DEBOUNCE_MS);
1721
+ return () => {
1722
+ if (predictTimer.current) clearTimeout(predictTimer.current);
1723
+ };
1724
+ }, [
1725
+ client,
1726
+ phoneNational,
1727
+ selectedCountry,
1728
+ selectedCurrencyCode,
1729
+ step
1730
+ ]);
1731
+ (0, import_react8.useEffect)(() => {
1732
+ if (step !== "payment" || !selectedBundle || !predict || !selectedCurrencyCode) {
1733
+ setQuote(null);
1734
+ return;
1735
+ }
1736
+ void (async () => {
1737
+ setQuoteLoading(true);
1738
+ setQuoteError(null);
1739
+ try {
1740
+ const q = await client.quotePayment({
1741
+ bundleId: selectedBundle.id,
1742
+ currencyCode: selectedCurrencyCode,
1743
+ paymentGatewayCode: predict.provider.code
1744
+ });
1745
+ setQuote(q);
1746
+ } catch (err) {
1747
+ setQuote(null);
1748
+ setQuoteError(
1749
+ err instanceof Error ? err.message : "Could not load quote"
1750
+ );
1751
+ } finally {
1752
+ setQuoteLoading(false);
1753
+ }
1754
+ })();
1755
+ }, [client, predict, selectedBundle, selectedCurrencyCode, step]);
1756
+ (0, import_react8.useEffect)(() => () => clearPoll(), [clearPoll]);
1757
+ const value = (0, import_react8.useMemo)(
1758
+ () => ({
1759
+ theme,
1760
+ themePreference,
1761
+ publishableKey,
1762
+ client,
1763
+ balance,
1764
+ balanceLoading,
1765
+ balanceError,
1766
+ refreshBalance,
1767
+ sheetOpen,
1768
+ step,
1769
+ resultPhase,
1770
+ topUpToken,
1771
+ bundles,
1772
+ bundlesLoading,
1773
+ bundlesError,
1774
+ selectedBundle,
1775
+ setSelectedBundle,
1776
+ countries,
1777
+ selectedCountry,
1778
+ setSelectedCountry,
1779
+ selectedCurrencyCode,
1780
+ setSelectedCurrencyCode,
1781
+ phoneNational,
1782
+ setPhoneNational,
1783
+ predict,
1784
+ predictLoading,
1785
+ predictError,
1786
+ quote,
1787
+ quoteLoading,
1788
+ quoteError,
1789
+ deposit,
1790
+ depositError,
1791
+ failureReason,
1792
+ openTopUp,
1793
+ closeTopUp,
1794
+ goToOffer,
1795
+ goToPayment,
1796
+ submitDeposit,
1797
+ retryPayment,
1798
+ onTopUpComplete
1799
+ }),
1800
+ [
1801
+ theme,
1802
+ themePreference,
1803
+ publishableKey,
1804
+ client,
1805
+ balance,
1806
+ balanceLoading,
1807
+ balanceError,
1808
+ refreshBalance,
1809
+ sheetOpen,
1810
+ step,
1811
+ resultPhase,
1812
+ topUpToken,
1813
+ bundles,
1814
+ bundlesLoading,
1815
+ bundlesError,
1816
+ selectedBundle,
1817
+ countries,
1818
+ selectedCountry,
1819
+ setSelectedCountry,
1820
+ selectedCurrencyCode,
1821
+ phoneNational,
1822
+ predict,
1823
+ predictLoading,
1824
+ predictError,
1825
+ quote,
1826
+ quoteLoading,
1827
+ quoteError,
1828
+ deposit,
1829
+ depositError,
1830
+ failureReason,
1831
+ openTopUp,
1832
+ closeTopUp,
1833
+ goToOffer,
1834
+ goToPayment,
1835
+ submitDeposit,
1836
+ retryPayment,
1837
+ onTopUpComplete
1838
+ ]
1839
+ );
1840
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(JazaContext.Provider, { value, children: [
1841
+ children,
1842
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(TopUpBottomSheet, {})
1843
+ ] });
1844
+ }
1845
+
1846
+ // src/widgets/JazaBalanceWidget.tsx
1847
+ var import_react9 = require("react");
1848
+ var import_react_native11 = require("react-native");
1849
+ var import_jsx_runtime11 = require("react/jsx-runtime");
1850
+ function JazaBalanceWidget({ style }) {
1851
+ const { theme, balance, balanceLoading, balanceError, refreshBalance } = useJaza();
1852
+ const { colors, spacing: spacing2, radius: radius2 } = theme;
1853
+ (0, import_react9.useEffect)(() => {
1854
+ void refreshBalance();
1855
+ }, [refreshBalance]);
1856
+ const styles = import_react_native11.StyleSheet.create({
1857
+ card: {
1858
+ backgroundColor: colors.surfaceContainer,
1859
+ borderRadius: radius2.xl,
1860
+ padding: spacing2.md,
1861
+ ...style
1862
+ },
1863
+ label: {
1864
+ color: colors.onSurfaceVariant,
1865
+ fontSize: 14,
1866
+ marginBottom: spacing2.xs
1867
+ },
1868
+ row: {
1869
+ flexDirection: "row",
1870
+ alignItems: "center",
1871
+ gap: spacing2.sm
1872
+ },
1873
+ value: {
1874
+ color: colors.onSurface,
1875
+ fontSize: 48,
1876
+ fontWeight: "700",
1877
+ letterSpacing: -1
1878
+ },
1879
+ error: {
1880
+ color: colors.error,
1881
+ fontSize: 14,
1882
+ marginTop: spacing2.xs
1883
+ }
1884
+ });
1885
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_react_native11.View, { style: styles.card, children: [
1886
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_native11.Text, { style: styles.label, children: "Current Balance" }),
1887
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_react_native11.View, { style: styles.row, children: [
1888
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Icon, { name: "bolt", size: 28, color: colors.primary }),
1889
+ balanceLoading && balance === null ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_native11.ActivityIndicator, { color: colors.primary }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_native11.Text, { style: styles.value, children: balance !== null ? formatCredits(balance) : "\u2014" })
1890
+ ] }),
1891
+ balanceError ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_native11.Text, { style: styles.error, children: balanceError }) : null
1892
+ ] });
1893
+ }
1894
+
1895
+ // src/widgets/JazaTopUpButton.tsx
1896
+ var import_react10 = require("react");
1897
+ var import_react_native12 = require("react-native");
1898
+ var import_jsx_runtime12 = require("react/jsx-runtime");
1899
+ function JazaTopUpButton({
1900
+ onRequestToken,
1901
+ label = "Top up credits",
1902
+ style
1903
+ }) {
1904
+ const { theme, openTopUp } = useJaza();
1905
+ const { colors, spacing: spacing2, radius: radius2 } = theme;
1906
+ const [loading, setLoading] = (0, import_react10.useState)(false);
1907
+ const [error, setError] = (0, import_react10.useState)(null);
1908
+ const styles = import_react_native12.StyleSheet.create({
1909
+ button: {
1910
+ backgroundColor: colors.primaryContainer,
1911
+ borderRadius: radius2.full,
1912
+ paddingVertical: spacing2.md,
1913
+ paddingHorizontal: spacing2.lg,
1914
+ flexDirection: "row",
1915
+ alignItems: "center",
1916
+ justifyContent: "center",
1917
+ gap: spacing2.sm,
1918
+ ...style
1919
+ },
1920
+ pressed: { opacity: 0.9, transform: [{ scale: 0.98 }] },
1921
+ label: {
1922
+ color: colors.onPrimaryContainer,
1923
+ fontSize: 16,
1924
+ fontWeight: "600"
1925
+ },
1926
+ error: {
1927
+ color: colors.error,
1928
+ fontSize: 12,
1929
+ marginTop: spacing2.xs,
1930
+ textAlign: "center"
1931
+ }
1932
+ });
1933
+ const handlePress = async () => {
1934
+ setError(null);
1935
+ setLoading(true);
1936
+ try {
1937
+ const token = await onRequestToken();
1938
+ if (!token?.trim()) {
1939
+ throw new Error("Top-up token was empty");
1940
+ }
1941
+ await openTopUp(token.trim());
1942
+ } catch (err) {
1943
+ setError(err instanceof Error ? err.message : "Could not start top-up");
1944
+ } finally {
1945
+ setLoading(false);
1946
+ }
1947
+ };
1948
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_jsx_runtime12.Fragment, { children: [
1949
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1950
+ import_react_native12.Pressable,
1951
+ {
1952
+ style: ({ pressed }) => [styles.button, pressed && styles.pressed],
1953
+ onPress: () => void handlePress(),
1954
+ disabled: loading,
1955
+ children: loading ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_react_native12.ActivityIndicator, { color: colors.onPrimaryContainer }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_jsx_runtime12.Fragment, { children: [
1956
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_react_native12.Text, { style: styles.label, children: label }),
1957
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Icon, { name: "arrow-forward", size: 20, color: colors.onPrimaryContainer })
1958
+ ] })
1959
+ }
1960
+ ),
1961
+ error ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_react_native12.Text, { style: styles.error, children: error }) : null
1962
+ ] });
1963
+ }
1964
+ // Annotate the CommonJS export names for ESM import in node:
1965
+ 0 && (module.exports = {
1966
+ DEFAULT_API_BASE_URL,
1967
+ DIAL_CODES,
1968
+ JazaBalanceWidget,
1969
+ JazaProvider,
1970
+ JazaSdkError,
1971
+ JazaTopUpButton,
1972
+ PublicClient,
1973
+ VERSION,
1974
+ buildE164,
1975
+ darkTheme,
1976
+ enrichCountries,
1977
+ formatCredits,
1978
+ formatCurrencyAmount,
1979
+ formatLocalAmount,
1980
+ formatUsd,
1981
+ getDialCode,
1982
+ isDepositTerminal,
1983
+ iso2ToFlag,
1984
+ lightTheme,
1985
+ pickDefaultCurrencyCode,
1986
+ useJaza
1987
+ });
1988
+ //# sourceMappingURL=index.cjs.map