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