@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
JavaScript
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