UNPKG

@iamjr15/react-currency-localizer

Version:

A React hook to display prices in a user's local currency using HTTPS-compatible IP geolocation with robust validation and error handling

684 lines (683 loc) 20.9 kB
import { useQuery as K, QueryClientProvider as fe, QueryClient as de } from "@tanstack/react-query"; import me, { useState as ie, useMemo as W, useRef as ee, useEffect as Q } from "react"; const he = ({ basePrice: r, baseCurrency: t, apiKey: n, manualCurrency: s, onSuccess: i, onError: c }) => { const l = t.toUpperCase(), u = s?.toUpperCase(), [a, d] = ie( u || null ), p = W(() => !n || n.trim() === "" ? new Error("API key is missing. Please provide a valid key from exchangerate-api.com.") : null, [n]), w = ee(i), y = ee(c); Q(() => { w.current = i, y.current = c; }); const { data: b, error: A, isLoading: T } = K({ queryKey: ["geolocation"], queryFn: async ({ signal: I }) => { const S = await fetch( "https://ipapi.co/json/", { signal: I } ); if (!S.ok) throw new Error(`Geolocation API error: ${S.status}`); const E = await S.json(); if (E.error) throw new Error(E.reason || "Geolocation detection failed"); return { status: "success", currency: E.currency, countryCode: E.country_code }; }, // Only run this query if manual currency is NOT provided AND API key is valid enabled: !u && !p, // Data is considered fresh for 24 hours (geolocation doesn't change frequently) staleTime: 1e3 * 60 * 60 * 24, // 24 hours // Keep data in cache indefinitely (will be cleared by browser storage limits) gcTime: 1 / 0, // Don't retry on failure to avoid spamming the API retry: !1, // Don't refetch on mount if data exists refetchOnMount: !1, // Don't refetch on window focus refetchOnWindowFocus: !1 }), { data: g, error: v, isLoading: F } = K({ queryKey: ["exchange-rates", l, a, n], queryFn: async ({ signal: I }) => { if (a === l) return { result: "success", base_code: l, conversion_rates: { [l]: 1 } }; const S = await fetch( `https://v6.exchangerate-api.com/v6/${n}/latest/${l}`, { signal: I } ); if (!S.ok) throw new Error(`Exchange rate API error: ${S.status}`); const E = await S.json(); if (E.result === "error") throw new Error( E["error-message"] || `Exchange rate fetch failed: ${E.error_type}` ); return E; }, // This query will only run if localCurrency is determined and API key is valid enabled: !!a && !p, // Exchange rates are considered fresh for 1 hour staleTime: 1e3 * 60 * 60, // 1 hour // Cache for 2 hours gcTime: 1e3 * 60 * 60 * 2, // 2 hours // Retry once on failure retry: 1, // Don't refetch on window focus to preserve API quota refetchOnWindowFocus: !1 }); Q(() => { u ? d(u) : b?.currency && d(b.currency); }, [u, b?.currency]); const x = a && g?.conversion_rates ? g.conversion_rates[a] : null, j = W(() => a && g && g.conversion_rates && !(a in g.conversion_rates) ? new Error(`Currency '${a}' was detected from your location but is not supported by the exchange rate provider.`) : null, [a, g]), O = T || F && !!a, _ = A || v || j || p, k = typeof r == "number" && x !== null && !j ? r * x : null, D = W(() => ({ convertedPrice: k, localCurrency: a, baseCurrency: l, exchangeRate: x || null, isLoading: O, error: _ || null }), [k, a, l, x, O, _]); return Q(() => { !O && !_ && k !== null && w.current && w.current(D); }, [O, _, k, D]), Q(() => { !O && _ && y.current && y.current(_); }, [O, _]), D; }; var Y = { exports: {} }, M = {}; /** * @license React * react-jsx-runtime.production.js * * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ var re; function pe() { if (re) return M; re = 1; var r = Symbol.for("react.transitional.element"), t = Symbol.for("react.fragment"); function n(s, i, c) { var l = null; if (c !== void 0 && (l = "" + c), i.key !== void 0 && (l = "" + i.key), "key" in i) { c = {}; for (var u in i) u !== "key" && (c[u] = i[u]); } else c = i; return i = c.ref, { $$typeof: r, type: s, key: l, ref: i !== void 0 ? i : null, props: c }; } return M.Fragment = t, M.jsx = n, M.jsxs = n, M; } var $ = {}; /** * @license React * react-jsx-runtime.development.js * * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ var te; function ye() { return te || (te = 1, process.env.NODE_ENV !== "production" && function() { function r(e) { if (e == null) return null; if (typeof e == "function") return e.$$typeof === S ? null : e.displayName || e.name || null; if (typeof e == "string") return e; switch (e) { case T: return "Fragment"; case v: return "Profiler"; case g: return "StrictMode"; case O: return "Suspense"; case _: return "SuspenseList"; case I: return "Activity"; } if (typeof e == "object") switch (typeof e.tag == "number" && console.error( "Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue." ), e.$$typeof) { case A: return "Portal"; case x: return (e.displayName || "Context") + ".Provider"; case F: return (e._context.displayName || "Context") + ".Consumer"; case j: var o = e.render; return e = e.displayName, e || (e = o.displayName || o.name || "", e = e !== "" ? "ForwardRef(" + e + ")" : "ForwardRef"), e; case k: return o = e.displayName || null, o !== null ? o : r(e.type) || "Memo"; case D: o = e._payload, e = e._init; try { return r(e(o)); } catch { } } return null; } function t(e) { return "" + e; } function n(e) { try { t(e); var o = !1; } catch { o = !0; } if (o) { o = console; var f = o.error, m = typeof Symbol == "function" && Symbol.toStringTag && e[Symbol.toStringTag] || e.constructor.name || "Object"; return f.call( o, "The provided key is an unsupported type %s. This value must be coerced to a string before using it here.", m ), t(e); } } function s(e) { if (e === T) return "<>"; if (typeof e == "object" && e !== null && e.$$typeof === D) return "<...>"; try { var o = r(e); return o ? "<" + o + ">" : "<...>"; } catch { return "<...>"; } } function i() { var e = E.A; return e === null ? null : e.getOwner(); } function c() { return Error("react-stack-top-frame"); } function l(e) { if (G.call(e, "key")) { var o = Object.getOwnPropertyDescriptor(e, "key").get; if (o && o.isReactWarning) return !1; } return e.key !== void 0; } function u(e, o) { function f() { V || (V = !0, console.error( "%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://react.dev/link/special-props)", o )); } f.isReactWarning = !0, Object.defineProperty(e, "key", { get: f, configurable: !0 }); } function a() { var e = r(this.type); return H[e] || (H[e] = !0, console.error( "Accessing element.ref was removed in React 19. ref is now a regular prop. It will be removed from the JSX Element type in a future release." )), e = this.props.ref, e !== void 0 ? e : null; } function d(e, o, f, m, P, R, U, q) { return f = R.ref, e = { $$typeof: b, type: e, key: o, props: R, _owner: P }, (f !== void 0 ? f : null) !== null ? Object.defineProperty(e, "ref", { enumerable: !1, get: a }) : Object.defineProperty(e, "ref", { enumerable: !1, value: null }), e._store = {}, Object.defineProperty(e._store, "validated", { configurable: !1, enumerable: !1, writable: !0, value: 0 }), Object.defineProperty(e, "_debugInfo", { configurable: !1, enumerable: !1, writable: !0, value: null }), Object.defineProperty(e, "_debugStack", { configurable: !1, enumerable: !1, writable: !0, value: U }), Object.defineProperty(e, "_debugTask", { configurable: !1, enumerable: !1, writable: !0, value: q }), Object.freeze && (Object.freeze(e.props), Object.freeze(e)), e; } function p(e, o, f, m, P, R, U, q) { var h = o.children; if (h !== void 0) if (m) if (ue(h)) { for (m = 0; m < h.length; m++) w(h[m]); Object.freeze && Object.freeze(h); } else console.error( "React.jsx: Static children should always be an array. You are likely explicitly calling React.jsxs or React.jsxDEV. Use the Babel transform instead." ); else w(h); if (G.call(o, "key")) { h = r(e); var N = Object.keys(o).filter(function(le) { return le !== "key"; }); m = 0 < N.length ? "{key: someKey, " + N.join(": ..., ") + ": ...}" : "{key: someKey}", Z[h + m] || (N = 0 < N.length ? "{" + N.join(": ..., ") + ": ...}" : "{}", console.error( `A props object containing a "key" prop is being spread into JSX: let props = %s; <%s {...props} /> React keys must be passed directly to JSX without using spread: let props = %s; <%s key={someKey} {...props} />`, m, h, N, h ), Z[h + m] = !0); } if (h = null, f !== void 0 && (n(f), h = "" + f), l(o) && (n(o.key), h = "" + o.key), "key" in o) { f = {}; for (var z in o) z !== "key" && (f[z] = o[z]); } else f = o; return h && u( f, typeof e == "function" ? e.displayName || e.name || "Unknown" : e ), d( e, h, R, P, i(), f, U, q ); } function w(e) { typeof e == "object" && e !== null && e.$$typeof === b && e._store && (e._store.validated = 1); } var y = me, b = Symbol.for("react.transitional.element"), A = Symbol.for("react.portal"), T = Symbol.for("react.fragment"), g = Symbol.for("react.strict_mode"), v = Symbol.for("react.profiler"), F = Symbol.for("react.consumer"), x = Symbol.for("react.context"), j = Symbol.for("react.forward_ref"), O = Symbol.for("react.suspense"), _ = Symbol.for("react.suspense_list"), k = Symbol.for("react.memo"), D = Symbol.for("react.lazy"), I = Symbol.for("react.activity"), S = Symbol.for("react.client.reference"), E = y.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE, G = Object.prototype.hasOwnProperty, ue = Array.isArray, L = console.createTask ? console.createTask : function() { return null; }; y = { react_stack_bottom_frame: function(e) { return e(); } }; var V, H = {}, X = y.react_stack_bottom_frame.bind( y, c )(), B = L(s(c)), Z = {}; $.Fragment = T, $.jsx = function(e, o, f, m, P) { var R = 1e4 > E.recentlyCreatedOwnerStacks++; return p( e, o, f, !1, m, P, R ? Error("react-stack-top-frame") : X, R ? L(s(e)) : B ); }, $.jsxs = function(e, o, f, m, P) { var R = 1e4 > E.recentlyCreatedOwnerStacks++; return p( e, o, f, !0, m, P, R ? Error("react-stack-top-frame") : X, R ? L(s(e)) : B ); }; }()), $; } var ne; function ve() { return ne || (ne = 1, process.env.NODE_ENV === "production" ? Y.exports = pe() : Y.exports = ye()), Y.exports; } var C = ve(); const Ie = ({ basePrice: r, baseCurrency: t, apiKey: n, manualCurrency: s, loadingComponent: i = /* @__PURE__ */ C.jsx("span", { children: "..." }), errorComponent: c, formatPrice: l }) => { const { convertedPrice: u, localCurrency: a, isLoading: d, error: p } = he({ basePrice: r, baseCurrency: t, apiKey: n, manualCurrency: s }); if (d) return /* @__PURE__ */ C.jsx(C.Fragment, { children: i }); if (p || u === null && !d) return c ? /* @__PURE__ */ C.jsx(C.Fragment, { children: c(p || new Error("Conversion failed")) }) : /* @__PURE__ */ C.jsx("span", { title: `Conversion failed, showing original price in ${t}`, children: new Intl.NumberFormat(void 0, { style: "currency", currency: t }).format(r) }); if (u === null) return /* @__PURE__ */ C.jsx(C.Fragment, { children: i }); const w = l ? l(u, a || t) : new Intl.NumberFormat(void 0, { style: "currency", currency: a || t }).format(u); return /* @__PURE__ */ C.jsx("span", { title: `Converted from ${r} ${t}`, children: w }); }; function oe() { } function Ee(r) { let t; if (r.then((n) => (t = n, n), oe)?.catch(oe), t !== void 0) return { data: t }; } function ce(r) { return r; } function ge(r) { return { mutationKey: r.options.mutationKey, state: r.state, ...r.options.scope && { scope: r.options.scope }, ...r.meta && { meta: r.meta } }; } function be(r, t, n) { return { dehydratedAt: Date.now(), state: { ...r.state, ...r.state.data !== void 0 && { data: t(r.state.data) } }, queryKey: r.queryKey, queryHash: r.queryHash, ...r.state.status === "pending" && { promise: r.promise?.then(t).catch((s) => n(s) ? (process.env.NODE_ENV !== "production" && console.error( `A query that was dehydrated as pending ended up rejecting. [${r.queryHash}]: ${s}; The error will be redacted in production builds` ), Promise.reject(new Error("redacted"))) : Promise.reject(s)) }, ...r.meta && { meta: r.meta } }; } function _e(r) { return r.state.isPaused; } function we(r) { return r.state.status === "success"; } function Re(r) { return !0; } function Ce(r, t = {}) { const n = t.shouldDehydrateMutation ?? r.getDefaultOptions().dehydrate?.shouldDehydrateMutation ?? _e, s = r.getMutationCache().getAll().flatMap( (a) => n(a) ? [ge(a)] : [] ), i = t.shouldDehydrateQuery ?? r.getDefaultOptions().dehydrate?.shouldDehydrateQuery ?? we, c = t.shouldRedactErrors ?? r.getDefaultOptions().dehydrate?.shouldRedactErrors ?? Re, l = t.serializeData ?? r.getDefaultOptions().dehydrate?.serializeData ?? ce, u = r.getQueryCache().getAll().flatMap( (a) => i(a) ? [be(a, l, c)] : [] ); return { mutations: s, queries: u }; } function Te(r, t, n) { if (typeof t != "object" || t === null) return; const s = r.getMutationCache(), i = r.getQueryCache(), c = n?.defaultOptions?.deserializeData ?? r.getDefaultOptions().hydrate?.deserializeData ?? ce, l = t.mutations || [], u = t.queries || []; l.forEach(({ state: a, ...d }) => { s.build( r, { ...r.getDefaultOptions().hydrate?.mutations, ...n?.defaultOptions?.mutations, ...d }, a ); }), u.forEach( ({ queryKey: a, state: d, queryHash: p, meta: w, promise: y, dehydratedAt: b }) => { const A = y ? Ee(y) : void 0, T = d.data === void 0 ? A?.data : d.data, g = T === void 0 ? T : c(T); let v = i.get(p); const F = v?.state.status === "pending", x = v?.state.fetchStatus === "fetching"; if (v) { const j = A && // We only need this undefined check to handle older dehydration // payloads that might not have dehydratedAt b !== void 0 && b > v.state.dataUpdatedAt; if (d.dataUpdatedAt > v.state.dataUpdatedAt || j) { const { fetchStatus: O, ..._ } = d; v.setState({ ..._, data: g }); } } else v = i.build( r, { ...r.getDefaultOptions().hydrate?.queries, ...n?.defaultOptions?.queries, queryKey: a, queryHash: p, meta: w }, // Reset fetch status to idle to avoid // query being stuck in fetching state upon hydration { ...d, data: g, fetchStatus: "idle", status: g !== void 0 ? "success" : d.status } ); y && !F && !x && // Only hydrate if dehydration is newer than any existing data, // this is always true for new queries (b === void 0 || b > v.state.dataUpdatedAt) && v.fetch(void 0, { // RSC transformed promises are not thenable initialPromise: Promise.resolve(y).then(c) }); } ); } var Oe = ["added", "removed", "updated"]; function ae(r) { return Oe.includes(r); } async function Se({ queryClient: r, persister: t, maxAge: n = 1e3 * 60 * 60 * 24, buster: s = "", hydrateOptions: i }) { try { const c = await t.restoreClient(); if (c) if (c.timestamp) { const l = Date.now() - c.timestamp > n, u = c.buster !== s; if (l || u) return t.removeClient(); Te(r, c.clientState, i); } else return t.removeClient(); } catch (c) { throw process.env.NODE_ENV !== "production" && (console.error(c), console.warn( "Encountered an error attempting to restore client cache from persisted location. As a precaution, the persisted cache will be discarded." )), await t.removeClient(), c; } } async function se({ queryClient: r, persister: t, buster: n = "", dehydrateOptions: s }) { const i = { buster: n, timestamp: Date.now(), clientState: Ce(r, s) }; await t.persistClient(i); } function xe(r) { const t = r.queryClient.getQueryCache().subscribe((s) => { ae(s.type) && se(r); }), n = r.queryClient.getMutationCache().subscribe((s) => { ae(s.type) && se(r); }); return () => { t(), n(); }; } function Pe(r) { let t = !1, n; const s = () => { t = !0, n?.(); }, i = Se(r).then(() => { t || (n = xe(r)); }); return [s, i]; } function J() { } function Ae({ storage: r, key: t = "REACT_QUERY_OFFLINE_CACHE", throttleTime: n = 1e3, serialize: s = JSON.stringify, deserialize: i = JSON.parse, retry: c }) { if (r) { const l = (u) => { try { r.setItem(t, s(u)); return; } catch (a) { return a; } }; return { persistClient: je((u) => { let a = u, d = l(a), p = 0; for (; d && a; ) p++, a = c?.({ persistedClient: a, error: d, errorCount: p }), a && (d = l(a)); }, n), restoreClient: () => { const u = r.getItem(t); if (u) return i(u); }, removeClient: () => { r.removeItem(t); } }; } return { persistClient: J, restoreClient: J, removeClient: J }; } function je(r, t = 100) { let n = null, s; return function(...i) { s = i, n === null && (n = setTimeout(() => { r(...s), n = null; }, t)); }; } const ke = () => new de({ defaultOptions: { queries: { // Default cache time - geolocation queries will override this gcTime: 1e3 * 60 * 60 * 24, // 24 hours // Default stale time - exchange rate queries will have shorter staleTime: 1e3 * 60 * 60, // 1 hour // Disable retries by default to respect API rate limits retry: !1, // Don't refetch on window focus by default for better UX refetchOnWindowFocus: !1 } } }), De = () => typeof window < "u" && window.localStorage ? Ae({ storage: window.localStorage, key: "react-currency-localizer-cache" }) : null, Me = ({ children: r, queryClient: t }) => { const [n] = ie(() => { if (t) return t; const s = ke(), i = De(); return i && Pe({ queryClient: s, persister: i, maxAge: 864e5, // 24 hours buster: "v1" // Change this to invalidate old cache }), s; }); return /* @__PURE__ */ C.jsx(fe, { client: n, children: r }); }; export { Me as CurrencyConverterProvider, Ie as LocalizedPrice, he as useCurrencyConverter }; //# sourceMappingURL=index.es.js.map