@cosmoola/cosmoola-native 1.0.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/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # @cosmoola/cosmoola-native
2
+
3
+ React Native SDK for Cosmoola — payments + currency converter.
4
+ Works with Expo and bare React Native (iOS + Android).
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ npm install @cosmoola/cosmoola-native @react-native-async-storage/async-storage
10
+ ```
11
+
12
+ If bare RN (not Expo):
13
+ ```bash
14
+ cd ios && pod install
15
+ ```
16
+
17
+ ---
18
+
19
+ ## Currency Converter
20
+
21
+ ### 1. Wrap your app ONCE in App.js
22
+
23
+ ```jsx
24
+ import { CurrencyProvider } from "@cosmoola/cosmoola-native";
25
+
26
+ export default function App() {
27
+ return (
28
+ <CurrencyProvider apiKey="pk_live_xxx">
29
+ <NavigationContainer>
30
+ <RootNavigator />
31
+ </NavigationContainer>
32
+ </CurrencyProvider>
33
+ );
34
+ }
35
+ ```
36
+
37
+ ### 2. Use anywhere in any screen
38
+
39
+ ```jsx
40
+ import { useCurrency, CsmPrice } from "@cosmoola/cosmoola-native";
41
+ import { Text, View } from "react-native";
42
+
43
+ // Hook
44
+ function ProductScreen() {
45
+ const { convert, currency, setCurrency } = useCurrency();
46
+ return (
47
+ <View>
48
+ <Text>{convert(99.99)}</Text> // → "£79.23"
49
+ <Text>{convert(99.99, "USD")}</Text> // explicit base
50
+ </View>
51
+ );
52
+ }
53
+
54
+ // Drop-in component
55
+ function PriceTag({ amount }) {
56
+ return <CsmPrice amount={amount} from="USD" style={{ fontSize: 18, fontWeight: "bold" }} />;
57
+ }
58
+ ```
59
+
60
+ ---
61
+
62
+ ## Payments
63
+
64
+ ```jsx
65
+ import CosmoolaNative, { useMobilePayment } from "@cosmoola/cosmoola-native";
66
+
67
+ const cosmoola = new CosmoolaNative("pk_live_xxx");
68
+
69
+ // Card payment
70
+ const { paymentIntent, error } = await cosmoola.confirmCardPayment(clientSecret, {
71
+ payment_method: {
72
+ card: { number: "4242424242424242", exp_month: "12", exp_year: "26", cvc: "123" }
73
+ }
74
+ });
75
+
76
+ // Mobile Money hook
77
+ function PayScreen() {
78
+ const { initiate, status, error } = useMobilePayment("pk_live_xxx");
79
+
80
+ const pay = async () => {
81
+ await initiate({
82
+ clientSecret: "pi_xxx_secret_xxx",
83
+ phone: "+254712345678",
84
+ country: "KE",
85
+ amount: 500,
86
+ currency: "KES",
87
+ });
88
+ };
89
+
90
+ return (
91
+ <Button title={status === "polling" ? "Waiting..." : "Pay with M-Pesa"} onPress={pay} />
92
+ );
93
+ }
94
+ ```
95
+
96
+ ---
97
+
98
+ ## Folder structure
99
+
100
+ ```
101
+ sdk/
102
+ cosmoola-js/ ← Web browser SDK
103
+ cosmoola-node/ ← Node.js server SDK
104
+ cosmoola-react/ ← React web SDK (hooks + components)
105
+ cosmoola-native/ ← React Native SDK (this package)
106
+ ```
107
+
108
+ ---
109
+
110
+ ## Differences from cosmoola-react
111
+
112
+ | Feature | cosmoola-react | cosmoola-native |
113
+ |---------|---------------|-----------------|
114
+ | Cache | localStorage | AsyncStorage |
115
+ | Country detect | ipapi.co + navigator.language | ipapi.co |
116
+ | Price component | `<CsmPrice />` (web) | `<CsmPrice />` (RN Text) |
117
+ | Currency switcher | `<CsmSwitcher />` | Build your own with `allCurrencies` |
118
+ | Payment elements | DOM-based CardElement | Use your own RN inputs |
119
+ | No-build | ✓ | ✓ |
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@cosmoola/cosmoola-native",
3
+ "version": "1.0.0",
4
+ "description": "Cosmoola React Native SDK — payments + currency converter",
5
+ "main": "src/index.js",
6
+ "module": "src/index.js",
7
+ "types": "types/index.d.ts",
8
+ "files": [
9
+ "src",
10
+ "types"
11
+ ],
12
+ "keywords": [
13
+ "cosmoola",
14
+ "payments",
15
+ "react-native",
16
+ "currency",
17
+ "mobile-money"
18
+ ],
19
+ "peerDependencies": {
20
+ "@react-native-async-storage/async-storage": "^3.0.2",
21
+ "react": ">=17.0.0",
22
+ "react-native": ">=0.70.0"
23
+ },
24
+ "license": "MIT"
25
+ }
package/src/index.js ADDED
@@ -0,0 +1,539 @@
1
+ /*!
2
+ * @cosmoola/cosmoola-native — React Native SDK v1.0.0
3
+ * ==========================================================
4
+ * Cosmoola SDK for React Native (Expo + bare workflow).
5
+ *
6
+ * USAGE:
7
+ *
8
+ * import { CurrencyProvider, useCurrency, CsmPrice } from "@cosmoola/cosmoola-native";
9
+ *
10
+ * // Wrap your app once in App.js
11
+ * <CurrencyProvider apiKey="pk_live_xxx">
12
+ * <App />
13
+ * </CurrencyProvider>
14
+ *
15
+ * // Use anywhere
16
+ * const { convert, currency, setCurrency } = useCurrency();
17
+ * <Text>{convert(99.99)}</Text>
18
+ *
19
+ * // Drop-in price component
20
+ * <CsmPrice amount={99.99} from="USD" />
21
+ *
22
+ * Payment methods (same as cosmoola-js):
23
+ * const { paymentIntent, error } = await cosmoola.confirmCardPayment(clientSecret, { ... });
24
+ * await cosmoola.confirmMobileMoneyPayment(clientSecret, { phone, country });
25
+ *
26
+ * No DOM, no localStorage — fully React Native compatible.
27
+ * ==========================================================
28
+ */
29
+
30
+ import React, {
31
+ createContext,
32
+ useContext,
33
+ useEffect,
34
+ useState,
35
+ useCallback,
36
+ useRef,
37
+ } from "react";
38
+ import AsyncStorage from "@react-native-async-storage/async-storage";
39
+ import { Text } from "react-native";
40
+
41
+ // ─────────────────────────────────────────────────────────────────────────────
42
+ // CONSTANTS
43
+ // ─────────────────────────────────────────────────────────────────────────────
44
+
45
+ const API_BASE = "https://cosmoola.com/api";
46
+ const CURRENCY_API_BASE = "https://cosmoola.com/currency";
47
+ const SDK_VERSION = "1.0.0";
48
+
49
+ // ─────────────────────────────────────────────────────────────────────────────
50
+ // CACHE — AsyncStorage instead of localStorage
51
+ // ─────────────────────────────────────────────────────────────────────────────
52
+
53
+ const CACHE_KEY = "csm_rates_v1";
54
+ const CACHE_TS_KEY = "csm_rates_ts";
55
+ const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
56
+
57
+ async function _cacheGet() {
58
+ try {
59
+ const ts = parseInt((await AsyncStorage.getItem(CACHE_TS_KEY)) || "0", 10);
60
+ const cached = await AsyncStorage.getItem(CACHE_KEY);
61
+ if (cached && Date.now() - ts < CACHE_TTL) return JSON.parse(cached);
62
+ } catch {}
63
+ return null;
64
+ }
65
+
66
+ async function _cacheSet(data) {
67
+ try {
68
+ await AsyncStorage.setItem(CACHE_KEY, JSON.stringify(data));
69
+ await AsyncStorage.setItem(CACHE_TS_KEY, String(Date.now()));
70
+ } catch {}
71
+ }
72
+
73
+ // ─────────────────────────────────────────────────────────────────────────────
74
+ // COUNTRY DETECTION — IP based, no window/navigator needed
75
+ // ─────────────────────────────────────────────────────────────────────────────
76
+
77
+ async function _detectCountry() {
78
+ try {
79
+ const controller = new AbortController();
80
+ const timeout = setTimeout(() => controller.abort(), 3000);
81
+ const res = await fetch("https://ipapi.co/json/", { signal: controller.signal });
82
+ clearTimeout(timeout);
83
+ const d = await res.json();
84
+ return d.country_code || null;
85
+ } catch {
86
+ return null;
87
+ }
88
+ }
89
+
90
+ // ─────────────────────────────────────────────────────────────────────────────
91
+ // HTTP HELPERS — pure fetch, works in RN
92
+ // ─────────────────────────────────────────────────────────────────────────────
93
+
94
+ async function _apiCall(method, endpoint, key, body = null, base = API_BASE) {
95
+ try {
96
+ const opts = {
97
+ method,
98
+ headers: {
99
+ "Authorization": `Bearer ${key}`,
100
+ "Content-Type": "application/json",
101
+ "Cosmoola-Version": SDK_VERSION,
102
+ },
103
+ };
104
+ if (body && method !== "GET") opts.body = JSON.stringify(body);
105
+ const res = await fetch(`${base}/${endpoint}`, opts);
106
+ const data = await res.json();
107
+ if (!res.ok) return { error: data?.error || data };
108
+ return data;
109
+ } catch (e) {
110
+ return { error: { message: e.message, type: "network_error" } };
111
+ }
112
+ }
113
+
114
+ const _post = (endpoint, key, body, base = API_BASE) =>
115
+ _apiCall("POST", endpoint, key, body, base);
116
+
117
+ const _get = (endpoint, key, params = {}, base = API_BASE) => {
118
+ const qs = Object.keys(params).length
119
+ ? "?" + new URLSearchParams(params).toString()
120
+ : "";
121
+ return _apiCall("GET", `${endpoint}${qs}`, key, null, base);
122
+ };
123
+
124
+ // ─────────────────────────────────────────────────────────────────────────────
125
+ // CURRENCY HELPERS
126
+ // ─────────────────────────────────────────────────────────────────────────────
127
+
128
+ function _convertRaw(amount, from, to, rates) {
129
+ if (!rates || from === to) return amount;
130
+ const fromRate = rates[from] || 1;
131
+ const toRate = rates[to] || 1;
132
+ return (amount / fromRate) * toRate;
133
+ }
134
+
135
+ function _format(amount, currencyCode, symbols) {
136
+ const symbol = symbols?.[currencyCode] || currencyCode;
137
+ const noDecimals = new Set([
138
+ "JPY","KRW","IDR","VND","CLP","ISK","PYG","UGX","TZS",
139
+ "BIF","RWF","GNF","MGA","KMF","XAF","XOF","XPF","DJF",
140
+ ]);
141
+ const decimals = noDecimals.has(currencyCode) ? 0 : 2;
142
+ const formatted = amount
143
+ .toFixed(decimals)
144
+ .replace(/\B(?=(\d{3})+(?!\d))/g, ",");
145
+ return `${symbol}${formatted}`;
146
+ }
147
+
148
+ // ─────────────────────────────────────────────────────────────────────────────
149
+ // MOBILE MONEY COUNTRIES
150
+ // ─────────────────────────────────────────────────────────────────────────────
151
+
152
+ const MOBILE_MONEY_COUNTRIES = {
153
+ KE: { provider:"safaricom", method:"mpesa", label:"M-Pesa", prefix:"254", currency:"KES" },
154
+ TZ: { provider:"vodacom", method:"mpesa", label:"M-Pesa", prefix:"255", currency:"TZS" },
155
+ NG: { provider:"mtn", method:"momo", label:"MTN MoMo", prefix:"234", currency:"NGN" },
156
+ GH: { provider:"mtn", method:"momo", label:"MTN MoMo", prefix:"233", currency:"GHS" },
157
+ UG: { provider:"airtel", method:"airtel", label:"Airtel Money", prefix:"256", currency:"UGX" },
158
+ RW: { provider:"mtn", method:"momo", label:"MTN MoMo", prefix:"250", currency:"RWF" },
159
+ ZM: { provider:"airtel", method:"airtel", label:"Airtel Money", prefix:"260", currency:"ZMW" },
160
+ MW: { provider:"airtel", method:"airtel", label:"Airtel Money", prefix:"265", currency:"MWK" },
161
+ SN: { provider:"orange", method:"orange", label:"Orange Money", prefix:"221", currency:"XOF" },
162
+ CI: { provider:"orange", method:"orange", label:"Orange Money", prefix:"225", currency:"XOF" },
163
+ CM: { provider:"mtn", method:"momo", label:"MTN MoMo", prefix:"237", currency:"XAF" },
164
+ };
165
+
166
+ // ─────────────────────────────────────────────────────────────────────────────
167
+ // CosmoolaNative — main class (mirrors cosmoola-js CosmooolaInstance)
168
+ // Use this if you want the imperative API without React hooks
169
+ // ─────────────────────────────────────────────────────────────────────────────
170
+
171
+ export class CosmoolaNative {
172
+ constructor(publishableKey, options = {}) {
173
+ if (!publishableKey) throw new Error("[Cosmoola] publishableKey is required.");
174
+ if (!publishableKey.startsWith("pk_live_") && !publishableKey.startsWith("pk_test_"))
175
+ throw new Error("[Cosmoola] Invalid key. Must start with pk_live_ or pk_test_");
176
+
177
+ this._key = publishableKey;
178
+ this._mode = publishableKey.includes("_test_") ? "test" : "live";
179
+ this._country = options.country || null;
180
+ this.currency = new NativeCurrency(publishableKey);
181
+ }
182
+
183
+ // ── confirmCardPayment ────────────────────────────────────────────────────
184
+ async confirmCardPayment(clientSecret, data = {}) {
185
+ try {
186
+ const intentId = _extractIntentId(clientSecret);
187
+ const res = await _post("confirmPaymentIntent", this._key, {
188
+ id: intentId,
189
+ payment_method: data.payment_method,
190
+ return_url: data.return_url || "",
191
+ });
192
+ if (res.error) return { error: res.error };
193
+ return { paymentIntent: res };
194
+ } catch (e) {
195
+ return { error: { type: "api_error", message: e.message } };
196
+ }
197
+ }
198
+
199
+ // ── confirmMobileMoneyPayment ─────────────────────────────────────────────
200
+ async confirmMobileMoneyPayment(clientSecret, data = {}) {
201
+ try {
202
+ if (!data.phone) throw new Error("phone is required");
203
+ if (!data.country) throw new Error("country is required");
204
+
205
+ const country = data.country.toUpperCase();
206
+ const config = MOBILE_MONEY_COUNTRIES[country];
207
+ if (!config) return { error: { message: `Mobile money not supported for: ${country}` } };
208
+
209
+ const intentId = _extractIntentId(clientSecret);
210
+ const endpoint = {
211
+ mpesa: "initiateMpesa",
212
+ momo: "initiateMtnMomo",
213
+ airtel: "initiateAirtel",
214
+ orange: "initiateOrange",
215
+ }[config.method];
216
+
217
+ const res = await _post(endpoint, this._key, {
218
+ payment_intent_id: intentId,
219
+ phone: data.phone,
220
+ amount: data.amount,
221
+ currency: data.currency || config.currency,
222
+ country,
223
+ });
224
+
225
+ if (res.error) return { error: res.error };
226
+ return { paymentIntent: { id: intentId, status: "processing" }, mobile_money: res };
227
+ } catch (e) {
228
+ return { error: { type: "api_error", message: e.message } };
229
+ }
230
+ }
231
+
232
+ // ── retrievePaymentIntent ─────────────────────────────────────────────────
233
+ async retrievePaymentIntent(clientSecret) {
234
+ try {
235
+ const intentId = _extractIntentId(clientSecret);
236
+ const res = await _get("getPaymentStatus", this._key, { id: intentId });
237
+ if (res.error) return { error: res.error };
238
+ return { paymentIntent: res };
239
+ } catch (e) {
240
+ return { error: { type: "api_error", message: e.message } };
241
+ }
242
+ }
243
+
244
+ // ── pollPaymentStatus ─────────────────────────────────────────────────────
245
+ async pollPaymentStatus(clientSecret, options = {}) {
246
+ const { interval = 3000, maxChecks = 40, onUpdate } = options;
247
+ const TERMINAL = ["succeeded", "canceled", "failed"];
248
+ let count = 0;
249
+
250
+ return new Promise((resolve) => {
251
+ const timer = setInterval(async () => {
252
+ count++;
253
+ const { paymentIntent, error } = await this.retrievePaymentIntent(clientSecret);
254
+ if (error) { clearInterval(timer); resolve({ error }); return; }
255
+ if (onUpdate) onUpdate(paymentIntent);
256
+ if (TERMINAL.includes(paymentIntent.status)) {
257
+ clearInterval(timer);
258
+ resolve({ paymentIntent });
259
+ return;
260
+ }
261
+ if (count >= maxChecks) {
262
+ clearInterval(timer);
263
+ resolve({ error: { type: "timeout_error", message: "Payment polling timed out." } });
264
+ }
265
+ }, interval);
266
+ });
267
+ }
268
+
269
+ getPublishableKey() { return this._key; }
270
+ getMode() { return this._mode; }
271
+ }
272
+
273
+ // ─────────────────────────────────────────────────────────────────────────────
274
+ // NativeCurrency — currency methods for React Native
275
+ // Accessed via cosmoola.currency or via useCurrency() hook
276
+ // ─────────────────────────────────────────────────────────────────────────────
277
+
278
+ class NativeCurrency {
279
+ constructor(key) {
280
+ this._key = key;
281
+ this._rates = null;
282
+ this._symbols = {};
283
+ this._names = {};
284
+ this._countryMap = {};
285
+ this._active = "USD";
286
+ this._readyPromise = null;
287
+ }
288
+
289
+ async init(country = null) {
290
+ if (this._readyPromise) return this._readyPromise;
291
+ this._readyPromise = (async () => {
292
+ const cached = await _cacheGet();
293
+ if (cached) {
294
+ this._rates = cached.rates;
295
+ this._symbols = cached.symbols || {};
296
+ this._names = cached.names || {};
297
+ this._countryMap = cached.countryMap || {};
298
+ this._active = cached.detected || "USD";
299
+ return;
300
+ }
301
+ const detectedCountry = country || await _detectCountry();
302
+ const params = detectedCountry ? { country: detectedCountry } : {};
303
+ const data = await _get("getCurrencyRates", this._key, params, CURRENCY_API_BASE);
304
+ if (data.error) { console.warn("[Cosmoola] Currency init failed:", data.error.message); return; }
305
+ this._rates = data.rates;
306
+ this._symbols = data.symbols || {};
307
+ this._names = data.names || {};
308
+ this._countryMap = data.countryMap || {};
309
+ this._active = data.detected || "USD";
310
+ await _cacheSet(data);
311
+ })();
312
+ return this._readyPromise;
313
+ }
314
+
315
+ async getRates(country = null) {
316
+ await this.init(country);
317
+ return { rates: this._rates, symbols: this._symbols, names: this._names, countryMap: this._countryMap, detected: this._active, base: "USD" };
318
+ }
319
+
320
+ async convert(amount, from = "USD", to = null) {
321
+ await this.init();
322
+ const target = to || this._active || "USD";
323
+ return _format(_convertRaw(amount, from, target, this._rates || {}), target, this._symbols);
324
+ }
325
+
326
+ async convertRaw(amount, from = "USD", to = null) {
327
+ await this.init();
328
+ const target = to || this._active || "USD";
329
+ return _convertRaw(amount, from, target, this._rates || {});
330
+ }
331
+
332
+ setCurrency(code) { this._active = code.toUpperCase(); }
333
+ getActiveCurrency() { return this._active; }
334
+ getSymbol(code = null) { return this._symbols[code || this._active] || "$"; }
335
+ getAllCurrencies() {
336
+ if (!this._rates) return [];
337
+ return Object.keys(this._rates).map(code => ({
338
+ code, symbol: this._symbols[code] || code, name: this._names[code] || code, rate: this._rates[code],
339
+ }));
340
+ }
341
+ }
342
+
343
+ // ─────────────────────────────────────────────────────────────────────────────
344
+ // React Native Context + Provider
345
+ // ─────────────────────────────────────────────────────────────────────────────
346
+
347
+ const CurrencyContext = createContext(null);
348
+
349
+ /**
350
+ * Wrap your entire app ONCE in App.js — works everywhere inside.
351
+ *
352
+ * @example
353
+ * // App.js
354
+ * import { CurrencyProvider } from "@cosmoola/cosmoola-native";
355
+ *
356
+ * export default function App() {
357
+ * return (
358
+ * <CurrencyProvider apiKey="pk_live_xxx">
359
+ * <NavigationContainer>...</NavigationContainer>
360
+ * </CurrencyProvider>
361
+ * );
362
+ * }
363
+ */
364
+ export function CurrencyProvider({ apiKey, defaultCurrency = "USD", country = null, children }) {
365
+ const [rates, setRates] = useState(null);
366
+ const [symbols, setSymbols] = useState({});
367
+ const [names, setNames] = useState({});
368
+ const [currency, setCurrency] = useState(defaultCurrency);
369
+ const [loading, setLoading] = useState(true);
370
+ const [error, setError] = useState(null);
371
+
372
+ useEffect(() => {
373
+ async function init() {
374
+ try {
375
+ // Try AsyncStorage cache first
376
+ const cached = await _cacheGet();
377
+ if (cached) {
378
+ setRates(cached.rates);
379
+ setSymbols(cached.symbols || {});
380
+ setNames(cached.names || {});
381
+ setCurrency(cached.detected || defaultCurrency);
382
+ setLoading(false);
383
+ return;
384
+ }
385
+
386
+ // Detect country then fetch
387
+ const detectedCountry = country || await _detectCountry();
388
+ const params = detectedCountry ? { country: detectedCountry } : {};
389
+ const data = await _get("getCurrencyRates", apiKey, params, CURRENCY_API_BASE);
390
+
391
+ if (data.error) throw new Error(data.error.message);
392
+
393
+ setRates(data.rates);
394
+ setSymbols(data.symbols || {});
395
+ setNames(data.names || {});
396
+ setCurrency(data.detected || defaultCurrency);
397
+ await _cacheSet(data);
398
+ } catch (e) {
399
+ setError(e.message);
400
+ console.warn("[Cosmoola Native] Failed to load rates:", e.message);
401
+ } finally {
402
+ setLoading(false);
403
+ }
404
+ }
405
+ init();
406
+ }, [apiKey, country]);
407
+
408
+ const convert = useCallback((amount, from = "USD") => {
409
+ if (!rates) return String(amount);
410
+ return _format(_convertRaw(amount, from, currency, rates), currency, symbols);
411
+ }, [rates, symbols, currency]);
412
+
413
+ const convertRaw = useCallback((amount, from = "USD") => {
414
+ if (!rates) return amount;
415
+ return _convertRaw(amount, from, currency, rates);
416
+ }, [rates, currency]);
417
+
418
+ const allCurrencies = rates
419
+ ? Object.keys(rates).map(code => ({ code, symbol: symbols[code] || code, name: names[code] || code, rate: rates[code] }))
420
+ : [];
421
+
422
+ return (
423
+ <CurrencyContext.Provider value={{
424
+ loading, error, currency, setCurrency,
425
+ symbol: symbols[currency] || "$",
426
+ convert, convertRaw,
427
+ rates, symbols, names, allCurrencies,
428
+ }}>
429
+ {children}
430
+ </CurrencyContext.Provider>
431
+ );
432
+ }
433
+
434
+ // ─────────────────────────────────────────────────────────────────────────────
435
+ // useCurrency — hook, use anywhere inside <CurrencyProvider>
436
+ // ─────────────────────────────────────────────────────────────────────────────
437
+
438
+ /**
439
+ * @example
440
+ * const { convert, currency, setCurrency } = useCurrency();
441
+ * <Text>{convert(99.99)}</Text> // → "£79.23"
442
+ */
443
+ export function useCurrency() {
444
+ const ctx = useContext(CurrencyContext);
445
+ if (!ctx) throw new Error("[Cosmoola] useCurrency() must be called inside <CurrencyProvider>");
446
+ return ctx;
447
+ }
448
+
449
+ // ─────────────────────────────────────────────────────────────────────────────
450
+ // CsmPrice — drop-in React Native price component
451
+ // ─────────────────────────────────────────────────────────────────────────────
452
+
453
+ /**
454
+ * @example
455
+ * <CsmPrice amount={99.99} />
456
+ * <CsmPrice amount={99.99} from="USD" style={{ fontSize: 18, fontWeight: "bold" }} />
457
+ */
458
+ export function CsmPrice({ amount, from = "USD", style }) {
459
+ const { convert, loading } = useCurrency();
460
+ return (
461
+ <Text style={style}>
462
+ {loading ? String(amount) : convert(amount, from)}
463
+ </Text>
464
+ );
465
+ }
466
+
467
+ // ─────────────────────────────────────────────────────────────────────────────
468
+ // useMobilePayment — hook for mobile money flows in React Native
469
+ // ─────────────────────────────────────────────────────────────────────────────
470
+
471
+ /**
472
+ * @example
473
+ * const { initiate, status, error } = useMobilePayment("pk_live_xxx");
474
+ *
475
+ * await initiate({
476
+ * clientSecret: "pi_xxx_secret_xxx",
477
+ * phone: "+254712345678",
478
+ * country: "KE",
479
+ * amount: 500,
480
+ * });
481
+ */
482
+ export function useMobilePayment(publishableKey) {
483
+ const cosmoolaRef = useRef(null);
484
+ if (!cosmoolaRef.current) cosmoolaRef.current = new CosmoolaNative(publishableKey);
485
+
486
+ const [status, setStatus] = useState("idle");
487
+ const [error, setError] = useState(null);
488
+ const [receipt, setReceipt] = useState(null);
489
+
490
+ const reset = useCallback(() => {
491
+ setStatus("idle"); setError(null); setReceipt(null);
492
+ }, []);
493
+
494
+ const initiate = useCallback(async ({ clientSecret, phone, country, amount, currency }) => {
495
+ setStatus("pending"); setError(null);
496
+ const cosmoola = cosmoolaRef.current;
497
+
498
+ const result = await cosmoola.confirmMobileMoneyPayment(clientSecret, {
499
+ phone, country, amount, currency,
500
+ });
501
+
502
+ if (result.error) { setError(result.error); setStatus("failed"); return; }
503
+
504
+ setStatus("polling");
505
+ setReceipt(result.mobile_money);
506
+
507
+ const { paymentIntent, error: pollError } = await cosmoola.pollPaymentStatus(clientSecret, {
508
+ interval: 3000, maxChecks: 40,
509
+ });
510
+
511
+ if (pollError) { setError(pollError); setStatus("failed"); }
512
+ else { setStatus(paymentIntent.status === "succeeded" ? "succeeded" : "failed"); }
513
+ }, []);
514
+
515
+ return { initiate, status, error, receipt, reset };
516
+ }
517
+
518
+ // ─────────────────────────────────────────────────────────────────────────────
519
+ // UTILITIES
520
+ // ─────────────────────────────────────────────────────────────────────────────
521
+
522
+ function _extractIntentId(clientSecret) {
523
+ const parts = (clientSecret || "").split("_secret_");
524
+ if (parts.length < 2) throw new Error("[Cosmoola] Invalid client_secret format.");
525
+ return parts[0];
526
+ }
527
+
528
+ // ─────────────────────────────────────────────────────────────────────────────
529
+ // EXPORTS
530
+ // ─────────────────────────────────────────────────────────────────────────────
531
+
532
+ export {
533
+ MOBILE_MONEY_COUNTRIES,
534
+ API_BASE,
535
+ CURRENCY_API_BASE,
536
+ SDK_VERSION,
537
+ };
538
+
539
+ export default CosmoolaNative;
@@ -0,0 +1,156 @@
1
+ // @cosmoola/cosmoola-native — TypeScript definitions v1.0.0
2
+
3
+ import React, { ReactNode } from "react";
4
+ import { TextStyle } from "react-native";
5
+
6
+ // ─── Core ────────────────────────────────────────────────────────────────────
7
+
8
+ export declare const API_BASE: string;
9
+ export declare const CURRENCY_API_BASE: string;
10
+ export declare const SDK_VERSION: string;
11
+
12
+ // ─── Types ───────────────────────────────────────────────────────────────────
13
+
14
+ export interface PaymentIntent {
15
+ id: string;
16
+ status: "requires_payment_method" | "processing" | "succeeded" | "canceled" | "failed" | "requires_action";
17
+ amount: number;
18
+ currency: string;
19
+ client_secret?: string;
20
+ }
21
+
22
+ export interface CosmoolaError {
23
+ type: string;
24
+ code?: string;
25
+ message: string;
26
+ }
27
+
28
+ export interface MobileMoneyResult {
29
+ id: string;
30
+ status: string;
31
+ method: string;
32
+ phone?: string;
33
+ }
34
+
35
+ export interface CurrencyData {
36
+ rates: Record<string, number>;
37
+ symbols: Record<string, string>;
38
+ names: Record<string, string>;
39
+ countryMap: Record<string, string>;
40
+ detected: string;
41
+ base: string;
42
+ }
43
+
44
+ export interface CurrencyItem {
45
+ code: string;
46
+ symbol: string;
47
+ name: string;
48
+ rate: number;
49
+ }
50
+
51
+ // ─── CosmoolaNative ──────────────────────────────────────────────────────────
52
+
53
+ export declare class CosmoolaNative {
54
+ constructor(publishableKey: string, options?: { country?: string });
55
+
56
+ currency: NativeCurrency;
57
+
58
+ confirmCardPayment(
59
+ clientSecret: string,
60
+ data: {
61
+ payment_method: { card: { number: string; exp_month: string; exp_year: string; cvc: string; name?: string } };
62
+ return_url?: string;
63
+ }
64
+ ): Promise<{ paymentIntent?: PaymentIntent; error?: CosmoolaError }>;
65
+
66
+ confirmMobileMoneyPayment(
67
+ clientSecret: string,
68
+ data: { phone: string; country: string; amount?: number; currency?: string }
69
+ ): Promise<{ paymentIntent?: PaymentIntent; mobile_money?: MobileMoneyResult; error?: CosmoolaError }>;
70
+
71
+ retrievePaymentIntent(
72
+ clientSecret: string
73
+ ): Promise<{ paymentIntent?: PaymentIntent; error?: CosmoolaError }>;
74
+
75
+ pollPaymentStatus(
76
+ clientSecret: string,
77
+ options?: { interval?: number; maxChecks?: number; onUpdate?: (pi: PaymentIntent) => void }
78
+ ): Promise<{ paymentIntent?: PaymentIntent; error?: CosmoolaError }>;
79
+
80
+ getPublishableKey(): string;
81
+ getMode(): "live" | "test";
82
+ }
83
+
84
+ // ─── NativeCurrency ──────────────────────────────────────────────────────────
85
+
86
+ export declare class NativeCurrency {
87
+ init(country?: string): Promise<void>;
88
+ getRates(country?: string): Promise<CurrencyData>;
89
+ convert(amount: number, from?: string, to?: string): Promise<string>;
90
+ convertRaw(amount: number, from?: string, to?: string): Promise<number>;
91
+ setCurrency(code: string): void;
92
+ getActiveCurrency(): string;
93
+ getSymbol(code?: string): string;
94
+ getAllCurrencies(): CurrencyItem[];
95
+ }
96
+
97
+ // ─── CurrencyProvider ────────────────────────────────────────────────────────
98
+
99
+ export interface CurrencyContextValue {
100
+ loading: boolean;
101
+ error: string | null;
102
+ currency: string;
103
+ setCurrency: (code: string) => void;
104
+ symbol: string;
105
+ convert: (amount: number, from?: string) => string;
106
+ convertRaw: (amount: number, from?: string) => number;
107
+ rates: Record<string, number> | null;
108
+ symbols: Record<string, string>;
109
+ names: Record<string, string>;
110
+ allCurrencies: CurrencyItem[];
111
+ }
112
+
113
+ export declare function CurrencyProvider(props: {
114
+ apiKey: string;
115
+ defaultCurrency?: string;
116
+ country?: string;
117
+ children: ReactNode;
118
+ }): React.ReactElement;
119
+
120
+ // ─── Hooks ───────────────────────────────────────────────────────────────────
121
+
122
+ export declare function useCurrency(): CurrencyContextValue;
123
+
124
+ export declare function useMobilePayment(publishableKey: string): {
125
+ initiate: (params: {
126
+ clientSecret: string;
127
+ phone: string;
128
+ country: string;
129
+ amount?: number;
130
+ currency?: string;
131
+ }) => Promise<void>;
132
+ status: "idle" | "pending" | "polling" | "succeeded" | "failed";
133
+ error: CosmoolaError | null;
134
+ receipt: MobileMoneyResult | null;
135
+ reset: () => void;
136
+ };
137
+
138
+ // ─── Components ──────────────────────────────────────────────────────────────
139
+
140
+ export declare function CsmPrice(props: {
141
+ amount: number;
142
+ from?: string;
143
+ style?: TextStyle;
144
+ }): React.ReactElement;
145
+
146
+ // ─── Constants ───────────────────────────────────────────────────────────────
147
+
148
+ export declare const MOBILE_MONEY_COUNTRIES: Record<string, {
149
+ provider: string;
150
+ method: string;
151
+ label: string;
152
+ prefix: string;
153
+ currency: string;
154
+ }>;
155
+
156
+ export default CosmoolaNative;