mirror of
https://github.com/aljazceru/mutiny-web.git
synced 2025-12-22 16:54:27 +01:00
feat: Add option to select desired fiat currency
This commit is contained in:
@@ -17,6 +17,7 @@ export const ActivityAmount: ParentComponent<{
|
|||||||
positive?: boolean;
|
positive?: boolean;
|
||||||
center?: boolean;
|
center?: boolean;
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
|
const [state, _actions] = useMegaStore();
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class="flex flex-col gap-1"
|
class="flex flex-col gap-1"
|
||||||
@@ -38,6 +39,7 @@ export const ActivityAmount: ParentComponent<{
|
|||||||
<AmountFiat
|
<AmountFiat
|
||||||
amountSats={Number(props.amount)}
|
amountSats={Number(props.amount)}
|
||||||
denominationSize="sm"
|
denominationSize="sm"
|
||||||
|
loading={state.price === 0}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import bolt from "~/assets/icons/bolt.svg";
|
|||||||
import chain from "~/assets/icons/chain.svg";
|
import chain from "~/assets/icons/chain.svg";
|
||||||
import { useI18n } from "~/i18n/context";
|
import { useI18n } from "~/i18n/context";
|
||||||
import { useMegaStore } from "~/state/megaStore";
|
import { useMegaStore } from "~/state/megaStore";
|
||||||
import { satsToUsd } from "~/utils";
|
import { satsToFormattedFiat } from "~/utils";
|
||||||
|
|
||||||
function prettyPrintAmount(n?: number | bigint): string {
|
function prettyPrintAmount(n?: number | bigint): string {
|
||||||
if (!n || n.valueOf() === 0) {
|
if (!n || n.valueOf() === 0) {
|
||||||
return "0";
|
return "0";
|
||||||
}
|
}
|
||||||
return n.toLocaleString();
|
return n.toLocaleString(navigator.languages[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AmountSats(props: {
|
export function AmountSats(props: {
|
||||||
@@ -35,7 +35,7 @@ export function AmountSats(props: {
|
|||||||
<Show when={props.icon === "minus"}>
|
<Show when={props.icon === "minus"}>
|
||||||
<span>-</span>
|
<span>-</span>
|
||||||
</Show>
|
</Show>
|
||||||
{props.loading ? "..." : prettyPrintAmount(props.amountSats)}
|
{props.loading ? "…" : prettyPrintAmount(props.amountSats)}
|
||||||
|
|
||||||
<span
|
<span
|
||||||
class="text-base font-light"
|
class="text-base font-light"
|
||||||
@@ -72,15 +72,19 @@ export function AmountFiat(props: {
|
|||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
denominationSize?: "sm" | "lg" | "xl";
|
denominationSize?: "sm" | "lg" | "xl";
|
||||||
}) {
|
}) {
|
||||||
const i18n = useI18n();
|
|
||||||
const [state, _] = useMegaStore();
|
const [state, _] = useMegaStore();
|
||||||
|
|
||||||
const amountInUsd = () =>
|
const amountInFiat = () =>
|
||||||
satsToUsd(state.price, Number(props.amountSats) || 0, true);
|
(state.fiat.value === "BTC" ? "" : "~") +
|
||||||
|
satsToFormattedFiat(
|
||||||
|
state.price,
|
||||||
|
Number(props.amountSats) || 0,
|
||||||
|
state.fiat
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<h2 class="font-light">
|
<h2 class="font-light">
|
||||||
~{props.loading ? "..." : amountInUsd()}
|
{props.loading ? "…" : amountInFiat()}
|
||||||
<span
|
<span
|
||||||
classList={{
|
classList={{
|
||||||
"text-sm": props.denominationSize === "sm",
|
"text-sm": props.denominationSize === "sm",
|
||||||
@@ -88,7 +92,8 @@ export function AmountFiat(props: {
|
|||||||
"text-xl": props.denominationSize === "xl"
|
"text-xl": props.denominationSize === "xl"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{i18n.t("common.usd")}
|
|
||||||
|
{props.loading ? "" : state.fiat.value}
|
||||||
</span>
|
</span>
|
||||||
</h2>
|
</h2>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { createMemo, Match, ParentComponent, Show, Switch } from "solid-js";
|
|||||||
import { AmountEditable, Card, VStack } from "~/components";
|
import { AmountEditable, Card, VStack } from "~/components";
|
||||||
import { useI18n } from "~/i18n/context";
|
import { useI18n } from "~/i18n/context";
|
||||||
import { useMegaStore } from "~/state/megaStore";
|
import { useMegaStore } from "~/state/megaStore";
|
||||||
import { satsToUsd } from "~/utils";
|
import { satsToFormattedFiat } from "~/utils";
|
||||||
|
|
||||||
const noop = () => {
|
const noop = () => {
|
||||||
// do nothing
|
// do nothing
|
||||||
@@ -24,7 +24,6 @@ const KeyValue: ParentComponent<{ key: string; gray?: boolean }> = (props) => {
|
|||||||
export const InlineAmount: ParentComponent<{
|
export const InlineAmount: ParentComponent<{
|
||||||
amount: string;
|
amount: string;
|
||||||
sign?: string;
|
sign?: string;
|
||||||
fiat?: boolean;
|
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const prettyPrint = createMemo(() => {
|
const prettyPrint = createMemo(() => {
|
||||||
@@ -32,34 +31,34 @@ export const InlineAmount: ParentComponent<{
|
|||||||
if (isNaN(parsed)) {
|
if (isNaN(parsed)) {
|
||||||
return props.amount;
|
return props.amount;
|
||||||
} else {
|
} else {
|
||||||
return parsed.toLocaleString();
|
return parsed.toLocaleString(navigator.languages[0]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="inline-block text-lg">
|
<div class="inline-block text-lg">
|
||||||
{props.sign ? `${props.sign} ` : ""}
|
{props.sign ? `${props.sign} ` : ""}
|
||||||
{props.fiat ? "$" : ""}
|
{prettyPrint()} <span class="text-sm">{i18n.t("common.sats")}</span>
|
||||||
{prettyPrint()}{" "}
|
|
||||||
<span class="text-sm">
|
|
||||||
{props.fiat ? i18n.t("common.usd") : i18n.t("common.sats")}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function USDShower(props: { amountSats: string; fee?: string }) {
|
function USDShower(props: { amountSats: string; fee?: string }) {
|
||||||
const i18n = useI18n();
|
|
||||||
const [state, _] = useMegaStore();
|
const [state, _] = useMegaStore();
|
||||||
const amountInUsd = () =>
|
const amountInFiat = () =>
|
||||||
satsToUsd(state.price, add(props.amountSats, props.fee), true);
|
(state.fiat.value === "BTC" ? "" : "~") +
|
||||||
|
satsToFormattedFiat(
|
||||||
|
state.price,
|
||||||
|
add(props.amountSats, props.fee),
|
||||||
|
state.fiat
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={!(props.amountSats === "0")}>
|
<Show when={!(props.amountSats === "0")}>
|
||||||
<KeyValue gray key="">
|
<KeyValue gray key="">
|
||||||
<div class="self-end">
|
<div class="self-end">
|
||||||
~{amountInUsd()}
|
{amountInFiat()}
|
||||||
<span class="text-sm">{i18n.t("common.usd")}</span>
|
<span class="text-sm">{state.fiat.value}</span>
|
||||||
</div>
|
</div>
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Dialog } from "@kobalte/core";
|
import { Dialog } from "@kobalte/core";
|
||||||
import {
|
import {
|
||||||
|
createEffect,
|
||||||
createResource,
|
createResource,
|
||||||
createSignal,
|
createSignal,
|
||||||
For,
|
For,
|
||||||
@@ -20,24 +21,52 @@ import { useI18n } from "~/i18n/context";
|
|||||||
import { Network } from "~/logic/mutinyWalletSetup";
|
import { Network } from "~/logic/mutinyWalletSetup";
|
||||||
import { useMegaStore } from "~/state/megaStore";
|
import { useMegaStore } from "~/state/megaStore";
|
||||||
import { DIALOG_CONTENT, DIALOG_POSITIONER } from "~/styles/dialogs";
|
import { DIALOG_CONTENT, DIALOG_POSITIONER } from "~/styles/dialogs";
|
||||||
import { satsToUsd, usdToSats } from "~/utils";
|
import { fiatToSats, satsToFiat } from "~/utils";
|
||||||
|
|
||||||
function fiatInputSanitizer(input: string): string {
|
import { Currency } from "./ChooseCurrency";
|
||||||
// Make sure only numbers and a single decimal point are allowed
|
|
||||||
const numeric = input.replace(/[^0-9.]/g, "").replace(/(\..*)\./g, "$1");
|
// Checks the users locale to determine if decimals should be a "." or a ","
|
||||||
|
const decimalDigitDivider = Number(1.0)
|
||||||
|
.toLocaleString(navigator.languages[0], { minimumFractionDigits: 1 })
|
||||||
|
.substring(1, 2);
|
||||||
|
|
||||||
|
function btcFloatRounding(localValue: string): string {
|
||||||
|
return (
|
||||||
|
(parseFloat(localValue) -
|
||||||
|
parseFloat(localValue.charAt(localValue.length - 1)) / 100000000) /
|
||||||
|
10
|
||||||
|
).toFixed(8);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fiatInputSanitizer(input: string, maxDecimals: number): string {
|
||||||
|
// Make sure only numbers and a single decimal point are allowed if decimals are allowed
|
||||||
|
let allowDecimalRegex;
|
||||||
|
if (maxDecimals !== 0) {
|
||||||
|
allowDecimalRegex = new RegExp("[^0-9.]", "g");
|
||||||
|
} else {
|
||||||
|
allowDecimalRegex = new RegExp("[^0-9]", "g");
|
||||||
|
}
|
||||||
|
const numeric = input
|
||||||
|
.replace(allowDecimalRegex, "")
|
||||||
|
.replace(/(\..*)\./g, "$1");
|
||||||
|
|
||||||
// Remove leading zeros if not a decimal, add 0 if starts with a decimal
|
// Remove leading zeros if not a decimal, add 0 if starts with a decimal
|
||||||
const cleaned = numeric.replace(/^0([^.]|$)/g, "$1").replace(/^\./g, "0.");
|
const cleaned = numeric.replace(/^0([^.]|$)/g, "$1").replace(/^\./g, "0.");
|
||||||
|
|
||||||
// If there are three characters after the decimal, shift the decimal
|
// If there are more characters after the decimal than allowed, shift the decimal
|
||||||
const shifted = cleaned.match(/(\.[0-9]{3}).*/g)
|
const shiftRegex = new RegExp(
|
||||||
? (parseFloat(cleaned) * 10).toFixed(2)
|
"(\\.[0-9]{" + (maxDecimals + 1) + "}).*",
|
||||||
|
"g"
|
||||||
|
);
|
||||||
|
const shifted = cleaned.match(shiftRegex)
|
||||||
|
? (parseFloat(cleaned) * 10).toFixed(maxDecimals)
|
||||||
: cleaned;
|
: cleaned;
|
||||||
|
|
||||||
// Truncate any numbers two past the decimal
|
// Truncate any numbers past the maxDecimal for the currency
|
||||||
const twoDecimals = shifted.replace(/(\.[0-9]{2}).*/g, "$1");
|
const decimalRegex = new RegExp("(\\.[0-9]{" + maxDecimals + "}).*", "g");
|
||||||
|
const decimals = shifted.replace(decimalRegex, "$1");
|
||||||
|
|
||||||
return twoDecimals;
|
return decimals;
|
||||||
}
|
}
|
||||||
|
|
||||||
function satsInputSanitizer(input: string): string {
|
function satsInputSanitizer(input: string): string {
|
||||||
@@ -53,9 +82,10 @@ function SingleDigitButton(props: {
|
|||||||
character: string;
|
character: string;
|
||||||
onClick: (c: string) => void;
|
onClick: (c: string) => void;
|
||||||
onClear: () => void;
|
onClear: () => void;
|
||||||
fiat: boolean;
|
fiat?: Currency;
|
||||||
}) {
|
}) {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
let holdTimer: ReturnType<typeof setTimeout> | undefined;
|
let holdTimer: ReturnType<typeof setTimeout> | undefined;
|
||||||
const holdThreshold = 500;
|
const holdThreshold = 500;
|
||||||
|
|
||||||
@@ -85,9 +115,14 @@ function SingleDigitButton(props: {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// Skip the "." if it's fiat
|
// Skip the "." if it's sats or a fiat with no decimal option
|
||||||
<Show
|
<Show
|
||||||
when={props.fiat || !(props.character === ".")}
|
when={
|
||||||
|
(props.fiat &&
|
||||||
|
props.fiat?.maxFractionalDigits !== 0 &&
|
||||||
|
props.fiat?.value !== "BTC") ||
|
||||||
|
!(props.character === "." || props.character === ",")
|
||||||
|
}
|
||||||
fallback={<div />}
|
fallback={<div />}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@@ -102,7 +137,12 @@ function SingleDigitButton(props: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BigScalingText(props: { text: string; fiat: boolean }) {
|
function BigScalingText(props: {
|
||||||
|
text: string;
|
||||||
|
fiat?: Currency;
|
||||||
|
mode: "fiat" | "sats";
|
||||||
|
loading: boolean;
|
||||||
|
}) {
|
||||||
const chars = () => props.text.length;
|
const chars = () => props.text.length;
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
@@ -119,21 +159,38 @@ function BigScalingText(props: { text: string; fiat: boolean }) {
|
|||||||
"scale-150": chars() <= 4
|
"scale-150": chars() <= 4
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Show when={!props.loading || props.mode === "sats"} fallback="…">
|
||||||
|
{!props.loading && props.mode === "sats"}
|
||||||
|
{props.mode === "fiat" &&
|
||||||
|
//adds only the symbol
|
||||||
|
props.fiat?.hasSymbol}
|
||||||
{props.text}
|
{props.text}
|
||||||
<span class="text-xl">
|
<span class="text-xl">
|
||||||
{props.fiat ? i18n.t("common.usd") : i18n.t("common.sats")}
|
{props.fiat ? props.fiat.value : i18n.t("common.sats")}
|
||||||
</span>
|
</span>
|
||||||
|
</Show>
|
||||||
</h1>
|
</h1>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SmallSubtleAmount(props: { text: string; fiat: boolean }) {
|
function SmallSubtleAmount(props: {
|
||||||
|
text: string;
|
||||||
|
fiat?: Currency;
|
||||||
|
mode: "fiat" | "sats";
|
||||||
|
loading: boolean;
|
||||||
|
}) {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<h2 class="flex flex-row items-end text-xl font-light text-neutral-400">
|
<h2 class="flex flex-row items-end text-xl font-light text-neutral-400">
|
||||||
~{props.text}
|
<Show when={!props.loading || props.mode === "fiat"} fallback="…">
|
||||||
|
{props.fiat?.value !== "BTC" && props.mode === "sats" && "~"}
|
||||||
|
{props.mode === "sats" &&
|
||||||
|
//adds only the symbol
|
||||||
|
props.fiat?.hasSymbol}
|
||||||
|
{props.text}
|
||||||
<span class="text-base">
|
<span class="text-base">
|
||||||
{props.fiat ? i18n.t("common.usd") : i18n.t("common.sats")}
|
{props.fiat ? props.fiat.value : i18n.t("common.sats")}
|
||||||
</span>
|
</span>
|
||||||
<img
|
<img
|
||||||
class={"pb-[4px] pl-[4px] hover:cursor-pointer"}
|
class={"pb-[4px] pl-[4px] hover:cursor-pointer"}
|
||||||
@@ -142,30 +199,62 @@ function SmallSubtleAmount(props: { text: string; fiat: boolean }) {
|
|||||||
width={24}
|
width={24}
|
||||||
alt="Swap currencies"
|
alt="Swap currencies"
|
||||||
/>
|
/>
|
||||||
|
</Show>
|
||||||
</h2>
|
</h2>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toDisplayHandleNaN(input: string, _fiat: boolean): string {
|
function toDisplayHandleNaN(input: string, fiat?: Currency): string {
|
||||||
const parsed = Number(input);
|
const parsed = Number(input);
|
||||||
|
|
||||||
//handle decimals so the user can always see the accurate amount
|
//handle decimals so the user can always see the accurate amount
|
||||||
if (isNaN(parsed)) {
|
if (isNaN(parsed)) {
|
||||||
return "0";
|
return "0";
|
||||||
} else if (parsed === Math.trunc(parsed) && input.endsWith(".")) {
|
} else if (parsed === Math.trunc(parsed) && input.endsWith(".")) {
|
||||||
return parsed.toLocaleString() + ".";
|
return (
|
||||||
|
parsed.toLocaleString(navigator.languages[0]) + decimalDigitDivider
|
||||||
|
);
|
||||||
|
/* To avoid having logic to handle every number up to 8 decimals
|
||||||
|
any custom currency pair that has more than 3 decimals will always show all decimals*/
|
||||||
|
} else if (fiat?.maxFractionalDigits && fiat.maxFractionalDigits > 3) {
|
||||||
|
return parsed.toLocaleString(navigator.languages[0], {
|
||||||
|
minimumFractionDigits: parsed === 0 ? 0 : fiat.maxFractionalDigits,
|
||||||
|
maximumFractionDigits: fiat.maxFractionalDigits
|
||||||
|
});
|
||||||
} else if (parsed === Math.trunc(parsed) && input.endsWith(".0")) {
|
} else if (parsed === Math.trunc(parsed) && input.endsWith(".0")) {
|
||||||
return parsed.toFixed(1);
|
return parsed.toLocaleString(navigator.languages[0], {
|
||||||
|
minimumFractionDigits: 1
|
||||||
|
});
|
||||||
} else if (parsed === Math.trunc(parsed) && input.endsWith(".00")) {
|
} else if (parsed === Math.trunc(parsed) && input.endsWith(".00")) {
|
||||||
return parsed.toFixed(2);
|
return parsed.toLocaleString(navigator.languages[0], {
|
||||||
|
minimumFractionDigits: 2
|
||||||
|
});
|
||||||
|
} else if (parsed === Math.trunc(parsed) && input.endsWith(".000")) {
|
||||||
|
return parsed.toLocaleString(navigator.languages[0], {
|
||||||
|
minimumFractionDigits: 3
|
||||||
|
});
|
||||||
} else if (
|
} else if (
|
||||||
parsed !== Math.trunc(parsed) &&
|
parsed !== Math.trunc(parsed) &&
|
||||||
input.endsWith("0") &&
|
// matches strings that have 3 total digits after the decimal and ends with 0
|
||||||
|
input.match(/\.\d{2}0$/) &&
|
||||||
|
input.includes(".", input.length - 4)
|
||||||
|
) {
|
||||||
|
return parsed.toLocaleString(navigator.languages[0], {
|
||||||
|
minimumFractionDigits: 3
|
||||||
|
});
|
||||||
|
} else if (
|
||||||
|
parsed !== Math.trunc(parsed) &&
|
||||||
|
// matches strings that have 2 total digits after the decimal and ends with 0
|
||||||
|
input.match(/\.\d{1}0$/) &&
|
||||||
input.includes(".", input.length - 3)
|
input.includes(".", input.length - 3)
|
||||||
) {
|
) {
|
||||||
return parsed.toFixed(2);
|
return parsed.toLocaleString(navigator.languages[0], {
|
||||||
|
minimumFractionDigits: 2
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
return parsed.toLocaleString();
|
return parsed.toLocaleString(navigator.languages[0], {
|
||||||
|
maximumFractionDigits: 3
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,13 +276,41 @@ export const AmountEditable: ParentComponent<{
|
|||||||
props.initialAmountSats || "0"
|
props.initialAmountSats || "0"
|
||||||
);
|
);
|
||||||
const [localFiat, setLocalFiat] = createSignal(
|
const [localFiat, setLocalFiat] = createSignal(
|
||||||
satsToUsd(
|
satsToFiat(
|
||||||
state.price,
|
state.price,
|
||||||
parseInt(props.initialAmountSats || "0") || 0,
|
parseInt(props.initialAmountSats || "0") || 0,
|
||||||
false
|
state.fiat
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** FixedAmounts allows for the user to choose 3 amount options approximately equal to ~$1, ~$10, ~$100
|
||||||
|
* This is done by fetching the price and reducing it such that the amounts all end up around the same value
|
||||||
|
*
|
||||||
|
* price = ~261,508.89
|
||||||
|
* roundedPrice = "261508"
|
||||||
|
* priceLength = 6
|
||||||
|
*
|
||||||
|
* input - {@link multipler}: 1, 10, 100
|
||||||
|
* fixedAmount - (10 ** (6 - 5)) * {@link multiplier}
|
||||||
|
* result - 10, 100, 1000
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fixedAmount = (multiplier: number, label: boolean) => {
|
||||||
|
const roundedPrice = Math.round(state.price);
|
||||||
|
const priceLength = roundedPrice.toString().length;
|
||||||
|
//This returns a stringified number based on the price range of the chosen currency as compared to BTC
|
||||||
|
if (!label) {
|
||||||
|
return Number(10 ** (priceLength - 5) * multiplier).toString();
|
||||||
|
// Handle labels with a currency identifier inserted in front/back
|
||||||
|
} else {
|
||||||
|
return `${state.fiat?.hasSymbol ?? ""}${Number(
|
||||||
|
10 ** (priceLength - 5) * multiplier
|
||||||
|
).toLocaleString(navigator.languages[0], {
|
||||||
|
maximumFractionDigits: state.fiat.maxFractionalDigits
|
||||||
|
})} ${!state.fiat?.hasSymbol ? state.fiat?.value : ""}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const FIXED_AMOUNTS_SATS = [
|
const FIXED_AMOUNTS_SATS = [
|
||||||
{
|
{
|
||||||
label: i18n.t("receive.amount_editable.fix_amounts.ten_k"),
|
label: i18n.t("receive.amount_editable.fix_amounts.ten_k"),
|
||||||
@@ -209,11 +326,28 @@ export const AmountEditable: ParentComponent<{
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const FIXED_AMOUNTS_USD = [
|
// Wait to set fiat amounts until we have a price when loading the page
|
||||||
{ label: "$1", amount: "1" },
|
let FIXED_AMOUNTS_FIAT;
|
||||||
{ label: "$10", amount: "10" },
|
|
||||||
{ label: "$100", amount: "100" }
|
createEffect(() => {
|
||||||
|
// set FIXED_AMOUNTS_FIAT once we have a price
|
||||||
|
if (state.price !== 0) {
|
||||||
|
FIXED_AMOUNTS_FIAT = [
|
||||||
|
{
|
||||||
|
label: fixedAmount(1, true),
|
||||||
|
amount: fixedAmount(1, false)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: fixedAmount(10, true),
|
||||||
|
amount: fixedAmount(10, false)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: fixedAmount(100, true),
|
||||||
|
amount: fixedAmount(100, false)
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const CHARACTERS = [
|
const CHARACTERS = [
|
||||||
"1",
|
"1",
|
||||||
@@ -225,13 +359,14 @@ export const AmountEditable: ParentComponent<{
|
|||||||
"7",
|
"7",
|
||||||
"8",
|
"8",
|
||||||
"9",
|
"9",
|
||||||
".",
|
decimalDigitDivider,
|
||||||
"0",
|
"0",
|
||||||
i18n.t("receive.amount_editable.del")
|
i18n.t("receive.amount_editable.del")
|
||||||
];
|
];
|
||||||
|
|
||||||
const displaySats = () => toDisplayHandleNaN(localSats(), false);
|
const displaySats = () => toDisplayHandleNaN(localSats());
|
||||||
const displayFiat = () => `$${toDisplayHandleNaN(localFiat(), true)}`;
|
const displayFiat = () =>
|
||||||
|
state.price !== 0 ? toDisplayHandleNaN(localFiat(), state.fiat) : "…";
|
||||||
|
|
||||||
let satsInputRef!: HTMLInputElement;
|
let satsInputRef!: HTMLInputElement;
|
||||||
let fiatInputRef!: HTMLInputElement;
|
let fiatInputRef!: HTMLInputElement;
|
||||||
@@ -293,11 +428,33 @@ export const AmountEditable: ParentComponent<{
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function handleCharacterInput(character: string) {
|
/** Handling character inputs gives our virtual keyboard full functionality to add and remove digits in a UX friendly way
|
||||||
|
* When the input is dealing with sats there is no allowed decimals
|
||||||
|
*
|
||||||
|
* Special logic is required for BTC as we want to start from the 8th decimal
|
||||||
|
* if state.fiat.value === "BTC"
|
||||||
|
* input - 000123
|
||||||
|
* result - 0.00000123
|
||||||
|
*
|
||||||
|
* input - 11"DEL"11
|
||||||
|
* result - 0.00000111
|
||||||
|
*
|
||||||
|
* for other currencies the inputSanitizer seeks to limit the maximum decimal digits
|
||||||
|
*
|
||||||
|
* if state.fiat.value === "KWD"
|
||||||
|
* input - 123.456666
|
||||||
|
* result - 123456.666
|
||||||
|
*/
|
||||||
|
|
||||||
|
function handleCharacterInput(characterInput: string) {
|
||||||
const isFiatMode = mode() === "fiat";
|
const isFiatMode = mode() === "fiat";
|
||||||
const inputSanitizer = isFiatMode
|
const character = characterInput === "," ? "." : characterInput;
|
||||||
? fiatInputSanitizer
|
let inputSanitizer;
|
||||||
: satsInputSanitizer;
|
if (isFiatMode) {
|
||||||
|
inputSanitizer = fiatInputSanitizer;
|
||||||
|
} else {
|
||||||
|
inputSanitizer = satsInputSanitizer;
|
||||||
|
}
|
||||||
const localValue = isFiatMode ? localFiat : localSats;
|
const localValue = isFiatMode ? localFiat : localSats;
|
||||||
|
|
||||||
let sane;
|
let sane;
|
||||||
@@ -306,27 +463,57 @@ export const AmountEditable: ParentComponent<{
|
|||||||
character === "DEL" ||
|
character === "DEL" ||
|
||||||
character === i18n.t("receive.amount_editable.del")
|
character === i18n.t("receive.amount_editable.del")
|
||||||
) {
|
) {
|
||||||
if (localValue().length <= 1) {
|
if (
|
||||||
|
localValue().length === 1 ||
|
||||||
|
(state.fiat.maxFractionalDigits === 0 &&
|
||||||
|
localValue().startsWith("0"))
|
||||||
|
) {
|
||||||
sane = "0";
|
sane = "0";
|
||||||
|
} else if (
|
||||||
|
state.fiat.value === "BTC" &&
|
||||||
|
isFiatMode &&
|
||||||
|
localValue() !== "0"
|
||||||
|
) {
|
||||||
|
// This allows us to handle the backspace key and fight float rounding
|
||||||
|
sane = inputSanitizer(
|
||||||
|
btcFloatRounding(localValue()),
|
||||||
|
state.fiat.maxFractionalDigits
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
sane = inputSanitizer(localValue().slice(0, -1));
|
sane = inputSanitizer(
|
||||||
|
localValue().slice(0, -1),
|
||||||
|
state.fiat.maxFractionalDigits
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (localValue() === "0") {
|
if (localValue() === "0" && state.fiat.value !== "BTC") {
|
||||||
sane = inputSanitizer(character);
|
sane = inputSanitizer(
|
||||||
|
character,
|
||||||
|
state.fiat.maxFractionalDigits
|
||||||
|
);
|
||||||
|
} else if (state.fiat.value === "BTC" && isFiatMode) {
|
||||||
|
sane = inputSanitizer(
|
||||||
|
Number(localValue()).toFixed(8) + character,
|
||||||
|
state.fiat.maxFractionalDigits
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
sane = inputSanitizer(localValue() + character);
|
sane = inputSanitizer(
|
||||||
|
localValue() + character,
|
||||||
|
state.fiat.maxFractionalDigits
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFiatMode) {
|
if (isFiatMode) {
|
||||||
setLocalFiat(sane);
|
setLocalFiat(sane);
|
||||||
setLocalSats(
|
setLocalSats(
|
||||||
usdToSats(state.price, parseFloat(sane || "0") || 0, false)
|
fiatToSats(state.price, parseFloat(sane || "0") || 0, false)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
setLocalSats(sane);
|
setLocalSats(sane);
|
||||||
setLocalFiat(satsToUsd(state.price, Number(sane) || 0, false));
|
setLocalFiat(
|
||||||
|
satsToFiat(state.price, Number(sane) || 0, state.fiat)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// After a button press make sure we re-focus the input
|
// After a button press make sure we re-focus the input
|
||||||
@@ -338,10 +525,10 @@ export const AmountEditable: ParentComponent<{
|
|||||||
|
|
||||||
if (isFiatMode) {
|
if (isFiatMode) {
|
||||||
setLocalFiat("0");
|
setLocalFiat("0");
|
||||||
setLocalSats(usdToSats(state.price, parseFloat("0") || 0, false));
|
setLocalSats(fiatToSats(state.price, parseFloat("0") || 0, false));
|
||||||
} else {
|
} else {
|
||||||
setLocalSats("0");
|
setLocalSats("0");
|
||||||
setLocalFiat(satsToUsd(state.price, Number("0") || 0, false));
|
setLocalFiat(satsToFiat(state.price, Number("0") || 0, state.fiat));
|
||||||
}
|
}
|
||||||
|
|
||||||
// After a button press make sure we re-focus the input
|
// After a button press make sure we re-focus the input
|
||||||
@@ -352,11 +539,13 @@ export const AmountEditable: ParentComponent<{
|
|||||||
if (mode() === "fiat") {
|
if (mode() === "fiat") {
|
||||||
setLocalFiat(amount);
|
setLocalFiat(amount);
|
||||||
setLocalSats(
|
setLocalSats(
|
||||||
usdToSats(state.price, parseFloat(amount || "0") || 0, false)
|
fiatToSats(state.price, parseFloat(amount || "0") || 0, false)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
setLocalSats(amount);
|
setLocalSats(amount);
|
||||||
setLocalFiat(satsToUsd(state.price, Number(amount) || 0, false));
|
setLocalFiat(
|
||||||
|
satsToFiat(state.price, Number(amount) || 0, state.fiat)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,10 +554,10 @@ export const AmountEditable: ParentComponent<{
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
setLocalSats(props.initialAmountSats);
|
setLocalSats(props.initialAmountSats);
|
||||||
setLocalFiat(
|
setLocalFiat(
|
||||||
satsToUsd(
|
satsToFiat(
|
||||||
state.price,
|
state.price,
|
||||||
parseInt(props.initialAmountSats || "0") || 0,
|
parseInt(props.initialAmountSats || "0") || 0,
|
||||||
false
|
state.fiat
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
props.exitRoute && navigate(props.exitRoute);
|
props.exitRoute && navigate(props.exitRoute);
|
||||||
@@ -378,7 +567,9 @@ export const AmountEditable: ParentComponent<{
|
|||||||
function handleSubmit(e: SubmitEvent | MouseEvent) {
|
function handleSubmit(e: SubmitEvent | MouseEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
props.setAmountSats(BigInt(localSats()));
|
props.setAmountSats(BigInt(localSats()));
|
||||||
setLocalFiat(satsToUsd(state.price, Number(localSats()) || 0, false));
|
setLocalFiat(
|
||||||
|
satsToFiat(state.price, Number(localSats()) || 0, state.fiat)
|
||||||
|
);
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,20 +577,55 @@ export const AmountEditable: ParentComponent<{
|
|||||||
const { value } = e.target as HTMLInputElement;
|
const { value } = e.target as HTMLInputElement;
|
||||||
const sane = satsInputSanitizer(value);
|
const sane = satsInputSanitizer(value);
|
||||||
setLocalSats(sane);
|
setLocalSats(sane);
|
||||||
setLocalFiat(satsToUsd(state.price, Number(sane) || 0, false));
|
setLocalFiat(satsToFiat(state.price, Number(sane) || 0, state.fiat));
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFiatInput(e: InputEvent) {
|
function handleFiatInput(e: InputEvent) {
|
||||||
const { value } = e.target as HTMLInputElement;
|
const { value } = e.currentTarget as HTMLInputElement;
|
||||||
const sane = fiatInputSanitizer(value);
|
let sane;
|
||||||
|
|
||||||
|
/** This behaves the same as handleCharacterInput but allows for the keyboard to be used instead of the virtual keypad
|
||||||
|
*
|
||||||
|
* if state.fiat.value === "BTC"
|
||||||
|
* tracking e.data is required as the string is not created from just normal sequencing numbers
|
||||||
|
* input - 12345
|
||||||
|
* result - 0.00012345
|
||||||
|
*
|
||||||
|
* if state.fiat.value !== "BTC"
|
||||||
|
* Otherwise we need to account for the user inputting decimals
|
||||||
|
* input - 123,45
|
||||||
|
* result - 123.45
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (state.fiat.value === "BTC") {
|
||||||
|
if (e.data !== null) {
|
||||||
|
sane = fiatInputSanitizer(
|
||||||
|
Number(localFiat()).toFixed(8) + e.data,
|
||||||
|
state.fiat.maxFractionalDigits
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
sane = fiatInputSanitizer(
|
||||||
|
// This allows us to handle the backspace key and fight float rounding
|
||||||
|
btcFloatRounding(localFiat()),
|
||||||
|
state.fiat.maxFractionalDigits
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sane = fiatInputSanitizer(
|
||||||
|
value.replace(",", "."),
|
||||||
|
state.fiat.maxFractionalDigits
|
||||||
|
);
|
||||||
|
}
|
||||||
setLocalFiat(sane);
|
setLocalFiat(sane);
|
||||||
setLocalSats(
|
setLocalSats(
|
||||||
usdToSats(state.price, parseFloat(sane || "0") || 0, false)
|
fiatToSats(state.price, parseFloat(sane || "0") || 0, false)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggle() {
|
function toggle(disabled: boolean) {
|
||||||
|
if (!disabled) {
|
||||||
setMode((m) => (m === "sats" ? "fiat" : "sats"));
|
setMode((m) => (m === "sats" ? "fiat" : "sats"));
|
||||||
|
}
|
||||||
focus();
|
focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,7 +654,7 @@ export const AmountEditable: ParentComponent<{
|
|||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
Number(props.maxAmountSats) - Number(props.fee)
|
Number(props.maxAmountSats) - Number(props.fee)
|
||||||
).toLocaleString();
|
).toLocaleString(navigator.languages[0]);
|
||||||
} else {
|
} else {
|
||||||
return localSats();
|
return localSats();
|
||||||
}
|
}
|
||||||
@@ -496,7 +722,7 @@ export const AmountEditable: ParentComponent<{
|
|||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<div
|
<div
|
||||||
class="flex w-max flex-col items-center justify-center gap-4 p-4"
|
class="flex w-max flex-col items-center justify-center gap-4 p-4"
|
||||||
onClick={toggle}
|
onClick={() => toggle(state.price === 0)}
|
||||||
>
|
>
|
||||||
<BigScalingText
|
<BigScalingText
|
||||||
text={
|
text={
|
||||||
@@ -504,7 +730,13 @@ export const AmountEditable: ParentComponent<{
|
|||||||
? displayFiat()
|
? displayFiat()
|
||||||
: displaySats()
|
: displaySats()
|
||||||
}
|
}
|
||||||
fiat={mode() === "fiat"}
|
fiat={
|
||||||
|
mode() === "fiat"
|
||||||
|
? state.fiat
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
mode={mode()}
|
||||||
|
loading={state.price === 0}
|
||||||
/>
|
/>
|
||||||
<SmallSubtleAmount
|
<SmallSubtleAmount
|
||||||
text={
|
text={
|
||||||
@@ -512,7 +744,13 @@ export const AmountEditable: ParentComponent<{
|
|||||||
? displayFiat()
|
? displayFiat()
|
||||||
: displaySats()
|
: displaySats()
|
||||||
}
|
}
|
||||||
fiat={mode() !== "fiat"}
|
fiat={
|
||||||
|
mode() !== "fiat"
|
||||||
|
? state.fiat
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
mode={mode()}
|
||||||
|
loading={state.price === 0}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -534,7 +772,7 @@ export const AmountEditable: ParentComponent<{
|
|||||||
<For
|
<For
|
||||||
each={
|
each={
|
||||||
mode() === "fiat"
|
mode() === "fiat"
|
||||||
? FIXED_AMOUNTS_USD
|
? FIXED_AMOUNTS_FIAT
|
||||||
: FIXED_AMOUNTS_SATS
|
: FIXED_AMOUNTS_SATS
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -550,7 +788,11 @@ export const AmountEditable: ParentComponent<{
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
<Show when={props.maxAmountSats}>
|
<Show
|
||||||
|
when={
|
||||||
|
mode() === "sats" && props.maxAmountSats
|
||||||
|
}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setFixedAmount(
|
setFixedAmount(
|
||||||
@@ -568,7 +810,11 @@ export const AmountEditable: ParentComponent<{
|
|||||||
<For each={CHARACTERS}>
|
<For each={CHARACTERS}>
|
||||||
{(character) => (
|
{(character) => (
|
||||||
<SingleDigitButton
|
<SingleDigitButton
|
||||||
fiat={mode() === "fiat"}
|
fiat={
|
||||||
|
mode() === "fiat"
|
||||||
|
? state.fiat
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
character={character}
|
character={character}
|
||||||
onClick={handleCharacterInput}
|
onClick={handleCharacterInput}
|
||||||
onClear={handleClear}
|
onClear={handleClear}
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ export function BalanceBox(props: { loading?: boolean }) {
|
|||||||
state.balance?.lightning || 0
|
state.balance?.lightning || 0
|
||||||
}
|
}
|
||||||
denominationSize="sm"
|
denominationSize="sm"
|
||||||
|
loading={state.price === 0}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -97,6 +98,7 @@ export function BalanceBox(props: { loading?: boolean }) {
|
|||||||
<AmountFiat
|
<AmountFiat
|
||||||
amountSats={totalOnchain()}
|
amountSats={totalOnchain()}
|
||||||
denominationSize="sm"
|
denominationSize="sm"
|
||||||
|
loading={state.price === 0}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
173
src/components/ChooseCurrency.tsx
Normal file
173
src/components/ChooseCurrency.tsx
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
import { createForm } from "@modular-forms/solid";
|
||||||
|
import { createSignal, Show } from "solid-js";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
ExternalLink,
|
||||||
|
InfoBox,
|
||||||
|
NiceP,
|
||||||
|
SelectField,
|
||||||
|
VStack
|
||||||
|
} from "~/components";
|
||||||
|
import { useI18n } from "~/i18n/context";
|
||||||
|
import { useMegaStore } from "~/state/megaStore";
|
||||||
|
import { eify, timeout } from "~/utils";
|
||||||
|
|
||||||
|
export interface Currency {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
hasSymbol?: string;
|
||||||
|
maxFractionalDigits: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChooseCurrencyForm = {
|
||||||
|
fiatCurrency: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FIAT_OPTIONS is an array of possible currencies
|
||||||
|
* All available currencies can be found here https://api.coingecko.com/api/v3/simple/supported_vs_currencies
|
||||||
|
* @Currency
|
||||||
|
* @param {string} label - should be in the format {Name} {ISO code}
|
||||||
|
* @param {string} values - are uppercase ISO 4217 currency code
|
||||||
|
* @param {string?} hasSymbol - if the currency has a symbol it should be represented as a string
|
||||||
|
* @param {number} maxFractionalDigits - the standard fractional units used by the currency should be set with maxFractionalDigits
|
||||||
|
*
|
||||||
|
* Bitcoin is represented as:
|
||||||
|
* {
|
||||||
|
* label: "bitcoin BTC",
|
||||||
|
* value: "BTC",
|
||||||
|
* hasSymbol: "₿",
|
||||||
|
* maxFractionalDigits: 8
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
//Bitcoin and USD need to be FIAT_OPTIONS[0] and FIAT_OPTIONS[1] respectively, they are called in megaStore.tsx
|
||||||
|
export const FIAT_OPTIONS: Currency[] = [
|
||||||
|
{
|
||||||
|
label: "Bitcoin BTC",
|
||||||
|
value: "BTC",
|
||||||
|
hasSymbol: "₿",
|
||||||
|
maxFractionalDigits: 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "United States Dollar USD",
|
||||||
|
value: "USD",
|
||||||
|
hasSymbol: "$",
|
||||||
|
maxFractionalDigits: 2
|
||||||
|
},
|
||||||
|
{ label: "Swiss Franc CHF", value: "CHF", maxFractionalDigits: 2 },
|
||||||
|
{
|
||||||
|
label: "Chinese Yuan CNY",
|
||||||
|
value: "CNY",
|
||||||
|
hasSymbol: "¥",
|
||||||
|
maxFractionalDigits: 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Euro EUR",
|
||||||
|
value: "EUR",
|
||||||
|
hasSymbol: "€",
|
||||||
|
maxFractionalDigits: 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "British Pound GBP",
|
||||||
|
value: "GBP",
|
||||||
|
hasSymbol: "₤",
|
||||||
|
maxFractionalDigits: 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Japanese Yen JPY",
|
||||||
|
value: "JPY",
|
||||||
|
hasSymbol: "¥",
|
||||||
|
maxFractionalDigits: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Korean Won KRW",
|
||||||
|
value: "KRW",
|
||||||
|
hasSymbol: "₩",
|
||||||
|
maxFractionalDigits: 0
|
||||||
|
},
|
||||||
|
{ label: "Kuwaiti Dinar KWD", value: "KWD", maxFractionalDigits: 3 }
|
||||||
|
];
|
||||||
|
|
||||||
|
export function ChooseCurrency() {
|
||||||
|
const i18n = useI18n();
|
||||||
|
const [error, setError] = createSignal<Error>();
|
||||||
|
const [state, _actions] = useMegaStore();
|
||||||
|
const [loading, setLoading] = createSignal(false);
|
||||||
|
|
||||||
|
function findCurrencyByValue(value: string) {
|
||||||
|
return FIAT_OPTIONS.find((currency) => currency.value === value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [_chooseCurrencyForm, { Form, Field }] =
|
||||||
|
createForm<ChooseCurrencyForm>({
|
||||||
|
initialValues: {
|
||||||
|
fiatCurrency: ""
|
||||||
|
},
|
||||||
|
validate: (values) => {
|
||||||
|
const errors: Record<string, string> = {};
|
||||||
|
if (values.fiatCurrency === undefined) {
|
||||||
|
errors.fiatCurrency = i18n.t(
|
||||||
|
"settings.currency.error_unsupported_currency"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleFormSubmit = async (f: ChooseCurrencyForm) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
localStorage.setItem(
|
||||||
|
"fiat_currency",
|
||||||
|
JSON.stringify(findCurrencyByValue(f.fiatCurrency))
|
||||||
|
);
|
||||||
|
|
||||||
|
await timeout(1000);
|
||||||
|
window.location.href = "/";
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
setError(eify(e));
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VStack>
|
||||||
|
<Form onSubmit={handleFormSubmit} class="flex flex-col gap-4">
|
||||||
|
<NiceP>{i18n.t("settings.currency.caption")}</NiceP>
|
||||||
|
<ExternalLink href="https://github.com/MutinyWallet/mutiny-web/issues/new">
|
||||||
|
{i18n.t("settings.currency.request_currency_support_link")}
|
||||||
|
</ExternalLink>
|
||||||
|
<div />
|
||||||
|
<VStack>
|
||||||
|
<Field name="fiatCurrency">
|
||||||
|
{(field, props) => (
|
||||||
|
<SelectField
|
||||||
|
{...props}
|
||||||
|
value={field.value}
|
||||||
|
error={field.error}
|
||||||
|
placeholder={state.fiat.label}
|
||||||
|
options={FIAT_OPTIONS}
|
||||||
|
label={i18n.t(
|
||||||
|
"settings.currency.select_currency_label"
|
||||||
|
)}
|
||||||
|
caption={i18n.t(
|
||||||
|
"settings.currency.select_currency_caption"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
<Show when={error()}>
|
||||||
|
<InfoBox accent="red">{error()?.message}</InfoBox>
|
||||||
|
</Show>
|
||||||
|
<div />
|
||||||
|
<Button intent="blue" loading={loading()}>
|
||||||
|
{i18n.t("settings.currency.select_currency")}
|
||||||
|
</Button>
|
||||||
|
</VStack>
|
||||||
|
</Form>
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ export * from "./AmountEditable";
|
|||||||
export * from "./App";
|
export * from "./App";
|
||||||
export * from "./BalanceBox";
|
export * from "./BalanceBox";
|
||||||
export * from "./BetaWarningModal";
|
export * from "./BetaWarningModal";
|
||||||
|
export * from "./ChooseCurrency";
|
||||||
export * from "./ContactEditor";
|
export * from "./ContactEditor";
|
||||||
export * from "./ContactForm";
|
export * from "./ContactForm";
|
||||||
export * from "./ContactViewer";
|
export * from "./ContactViewer";
|
||||||
|
|||||||
95
src/components/layout/SelectField.tsx
Normal file
95
src/components/layout/SelectField.tsx
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import { Select as KSelect } from "@kobalte/core";
|
||||||
|
import { JSX, Show, splitProps } from "solid-js";
|
||||||
|
|
||||||
|
import check from "~/assets/icons/check.svg";
|
||||||
|
|
||||||
|
import { Currency } from "../ChooseCurrency";
|
||||||
|
|
||||||
|
type SelectProps = {
|
||||||
|
options: Currency[];
|
||||||
|
multiple?: boolean;
|
||||||
|
size?: string | number;
|
||||||
|
caption?: string;
|
||||||
|
name: string;
|
||||||
|
label?: string | undefined;
|
||||||
|
placeholder?: string | undefined;
|
||||||
|
value: string | undefined;
|
||||||
|
error: string;
|
||||||
|
required?: boolean | undefined;
|
||||||
|
disabled?: boolean | undefined;
|
||||||
|
ref: (element: HTMLSelectElement) => void;
|
||||||
|
onInput: JSX.EventHandler<HTMLSelectElement, InputEvent>;
|
||||||
|
onChange: JSX.EventHandler<HTMLSelectElement, Event>;
|
||||||
|
onBlur: JSX.EventHandler<HTMLSelectElement, FocusEvent>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SelectField(props: SelectProps) {
|
||||||
|
// Split select element props
|
||||||
|
const [rootProps, selectProps] = splitProps(
|
||||||
|
props,
|
||||||
|
["name", "placeholder", "options", "required", "disabled"],
|
||||||
|
["placeholder", "ref", "onInput", "onChange", "onBlur"]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="relative flex items-center">
|
||||||
|
<KSelect.Root
|
||||||
|
{...rootProps}
|
||||||
|
class="flex w-full flex-col gap-2 text-sm font-semibold"
|
||||||
|
optionValue="value"
|
||||||
|
optionTextValue="label"
|
||||||
|
sameWidth
|
||||||
|
validationState={props.error ? "invalid" : "valid"}
|
||||||
|
itemComponent={(props) => (
|
||||||
|
<KSelect.Item
|
||||||
|
item={props.item}
|
||||||
|
class="flex w-full justify-between rounded-lg p-1 hover:bg-m-grey-800"
|
||||||
|
>
|
||||||
|
<Show when={props.item.rawValue.label}>
|
||||||
|
<KSelect.ItemLabel>
|
||||||
|
{props.item.rawValue.label}
|
||||||
|
</KSelect.ItemLabel>
|
||||||
|
</Show>
|
||||||
|
<KSelect.ItemIndicator>
|
||||||
|
<img
|
||||||
|
src={check}
|
||||||
|
alt="check"
|
||||||
|
height={20}
|
||||||
|
width={20}
|
||||||
|
/>
|
||||||
|
</KSelect.ItemIndicator>
|
||||||
|
</KSelect.Item>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Show when={props.label}>
|
||||||
|
<KSelect.Label class="uppercase">
|
||||||
|
{props.label}
|
||||||
|
</KSelect.Label>
|
||||||
|
</Show>
|
||||||
|
<KSelect.HiddenSelect {...selectProps} />
|
||||||
|
<KSelect.Trigger
|
||||||
|
class="flex w-full justify-between rounded-lg bg-white/10 p-2 text-base font-normal text-neutral-400"
|
||||||
|
aria-label="selectField"
|
||||||
|
>
|
||||||
|
<KSelect.Value<Currency>>
|
||||||
|
{(state) => state.selectedOption().label}
|
||||||
|
</KSelect.Value>
|
||||||
|
<KSelect.Icon class="text-white" />
|
||||||
|
</KSelect.Trigger>
|
||||||
|
<Show when={props.caption}>
|
||||||
|
<KSelect.Description class="text-sm font-normal text-neutral-400">
|
||||||
|
{props.caption}
|
||||||
|
</KSelect.Description>
|
||||||
|
</Show>
|
||||||
|
<KSelect.Portal>
|
||||||
|
<KSelect.Content>
|
||||||
|
<KSelect.Listbox class="w-full cursor-default rounded-lg bg-[#0a0a0a] p-2" />
|
||||||
|
</KSelect.Content>
|
||||||
|
</KSelect.Portal>
|
||||||
|
<KSelect.ErrorMessage class="text-m-red">
|
||||||
|
{props.error}
|
||||||
|
</KSelect.ErrorMessage>
|
||||||
|
</KSelect.Root>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ export * from "./Linkify";
|
|||||||
export * from "./Misc";
|
export * from "./Misc";
|
||||||
export * from "./ProgressBar";
|
export * from "./ProgressBar";
|
||||||
export * from "./Radio";
|
export * from "./Radio";
|
||||||
|
export * from "./SelectField";
|
||||||
export * from "./TextField";
|
export * from "./TextField";
|
||||||
export * from "./ExternalLink";
|
export * from "./ExternalLink";
|
||||||
export * from "./LoadingSpinner";
|
export * from "./LoadingSpinner";
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ export default {
|
|||||||
home: "Home",
|
home: "Home",
|
||||||
sats: "SATS",
|
sats: "SATS",
|
||||||
sat: "SAT",
|
sat: "SAT",
|
||||||
usd: "USD",
|
|
||||||
fee: "Fee",
|
fee: "Fee",
|
||||||
send: "Send",
|
send: "Send",
|
||||||
receive: "Receive",
|
receive: "Receive",
|
||||||
@@ -185,10 +184,10 @@ export default {
|
|||||||
settings: {
|
settings: {
|
||||||
header: "Settings",
|
header: "Settings",
|
||||||
support: "Learn how to support Mutiny",
|
support: "Learn how to support Mutiny",
|
||||||
general: "GENERAL",
|
|
||||||
beta_features: "BETA FEATURES",
|
beta_features: "BETA FEATURES",
|
||||||
debug_tools: "DEBUG TOOLS",
|
debug_tools: "DEBUG TOOLS",
|
||||||
danger_zone: "Danger zone",
|
danger_zone: "Danger zone",
|
||||||
|
general: "General",
|
||||||
admin: {
|
admin: {
|
||||||
title: "Admin Page",
|
title: "Admin Page",
|
||||||
caption: "Our internal debug tools. Use wisely!",
|
caption: "Our internal debug tools. Use wisely!",
|
||||||
@@ -345,6 +344,17 @@ export default {
|
|||||||
forgot_password_link: "Forgot Password?",
|
forgot_password_link: "Forgot Password?",
|
||||||
error_wrong_password: "Invalid Password"
|
error_wrong_password: "Invalid Password"
|
||||||
},
|
},
|
||||||
|
currency: {
|
||||||
|
title: "Currency",
|
||||||
|
caption: "Choose your preferred currency pair",
|
||||||
|
select_currency: "Select Currency",
|
||||||
|
select_currency_label: "Currency Pair",
|
||||||
|
select_currency_caption:
|
||||||
|
"Choosing a new currency will resync the wallet to fetch a price update",
|
||||||
|
request_currency_support_link:
|
||||||
|
"Request support for more currencies",
|
||||||
|
error_unsupported_currency: "Please Select a supported currency."
|
||||||
|
},
|
||||||
lnurl_auth: {
|
lnurl_auth: {
|
||||||
title: "LNURL Auth",
|
title: "LNURL Auth",
|
||||||
auth: "Auth",
|
auth: "Auth",
|
||||||
|
|||||||
35
src/routes/settings/Currency.tsx
Normal file
35
src/routes/settings/Currency.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import {
|
||||||
|
BackLink,
|
||||||
|
Card,
|
||||||
|
ChooseCurrency,
|
||||||
|
DefaultMain,
|
||||||
|
LargeHeader,
|
||||||
|
MutinyWalletGuard,
|
||||||
|
NavBar,
|
||||||
|
SafeArea
|
||||||
|
} from "~/components";
|
||||||
|
import { useI18n } from "~/i18n/context";
|
||||||
|
|
||||||
|
export default function Currency() {
|
||||||
|
const i18n = useI18n();
|
||||||
|
return (
|
||||||
|
<MutinyWalletGuard>
|
||||||
|
<SafeArea>
|
||||||
|
<DefaultMain>
|
||||||
|
<BackLink
|
||||||
|
href="/settings"
|
||||||
|
title={i18n.t("settings.header")}
|
||||||
|
/>
|
||||||
|
<LargeHeader>
|
||||||
|
{i18n.t("settings.currency.title")}
|
||||||
|
</LargeHeader>
|
||||||
|
<Card title={i18n.t("settings.currency.select_currency")}>
|
||||||
|
<ChooseCurrency />
|
||||||
|
</Card>
|
||||||
|
<div class="h-full" />
|
||||||
|
</DefaultMain>
|
||||||
|
<NavBar activeTab="settings" />
|
||||||
|
</SafeArea>
|
||||||
|
</MutinyWalletGuard>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -81,6 +81,11 @@ export default function Settings() {
|
|||||||
<SettingsLinkList
|
<SettingsLinkList
|
||||||
header={i18n.t("settings.general")}
|
header={i18n.t("settings.general")}
|
||||||
links={[
|
links={[
|
||||||
|
{
|
||||||
|
href: "/settings/currency",
|
||||||
|
text: i18n.t("settings.currency.title"),
|
||||||
|
caption: i18n.t("settings.currency.caption")
|
||||||
|
},
|
||||||
{
|
{
|
||||||
href: "/settings/channels",
|
href: "/settings/channels",
|
||||||
text: i18n.t("settings.channels.title")
|
text: i18n.t("settings.channels.title")
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import { createStore } from "solid-js/store";
|
import { createStore } from "solid-js/store";
|
||||||
import { useSearchParams } from "solid-start";
|
import { useSearchParams } from "solid-start";
|
||||||
|
|
||||||
|
import { Currency, FIAT_OPTIONS } from "~/components/ChooseCurrency";
|
||||||
import { checkBrowserCompatibility } from "~/logic/browserCompatibility";
|
import { checkBrowserCompatibility } from "~/logic/browserCompatibility";
|
||||||
import {
|
import {
|
||||||
doubleInitDefense,
|
doubleInitDefense,
|
||||||
@@ -41,6 +42,7 @@ export type MegaStore = [
|
|||||||
is_syncing?: boolean;
|
is_syncing?: boolean;
|
||||||
last_sync?: number;
|
last_sync?: number;
|
||||||
price: number;
|
price: number;
|
||||||
|
fiat: Currency;
|
||||||
has_backed_up: boolean;
|
has_backed_up: boolean;
|
||||||
wallet_loading: boolean;
|
wallet_loading: boolean;
|
||||||
setup_error?: Error;
|
setup_error?: Error;
|
||||||
@@ -74,6 +76,9 @@ export const Provider: ParentComponent = (props) => {
|
|||||||
deleting: false,
|
deleting: false,
|
||||||
scan_result: undefined as ParsedParams | undefined,
|
scan_result: undefined as ParsedParams | undefined,
|
||||||
price: 0,
|
price: 0,
|
||||||
|
fiat: localStorage.getItem("fiat_currency")
|
||||||
|
? (JSON.parse(localStorage.getItem("fiat_currency")!) as Currency)
|
||||||
|
: FIAT_OPTIONS[1],
|
||||||
has_backed_up: localStorage.getItem("has_backed_up") === "true",
|
has_backed_up: localStorage.getItem("has_backed_up") === "true",
|
||||||
balance: undefined as MutinyBalance | undefined,
|
balance: undefined as MutinyBalance | undefined,
|
||||||
last_sync: undefined as number | undefined,
|
last_sync: undefined as number | undefined,
|
||||||
@@ -209,14 +214,37 @@ export const Provider: ParentComponent = (props) => {
|
|||||||
try {
|
try {
|
||||||
if (state.mutiny_wallet && !state.is_syncing) {
|
if (state.mutiny_wallet && !state.is_syncing) {
|
||||||
setState({ is_syncing: true });
|
setState({ is_syncing: true });
|
||||||
|
let price: number;
|
||||||
const newBalance = await state.mutiny_wallet?.get_balance();
|
const newBalance = await state.mutiny_wallet?.get_balance();
|
||||||
const price =
|
if (state.fiat.value === "BTC") {
|
||||||
await state.mutiny_wallet?.get_bitcoin_price();
|
|
||||||
setState({
|
setState({
|
||||||
balance: newBalance,
|
balance: newBalance,
|
||||||
last_sync: Date.now(),
|
last_sync: Date.now(),
|
||||||
price: price || 0
|
price: 1,
|
||||||
|
fiat: state.fiat
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
price =
|
||||||
|
await state.mutiny_wallet?.get_bitcoin_price(
|
||||||
|
state.fiat.value.toLowerCase() || "usd"
|
||||||
|
);
|
||||||
|
setState({
|
||||||
|
balance: newBalance,
|
||||||
|
last_sync: Date.now(),
|
||||||
|
price: price || 0,
|
||||||
|
fiat: state.fiat
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
price = 1;
|
||||||
|
setState({
|
||||||
|
balance: newBalance,
|
||||||
|
last_sync: Date.now(),
|
||||||
|
price: price,
|
||||||
|
fiat: FIAT_OPTIONS[0]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
import { MutinyWallet } from "@mutinywallet/mutiny-wasm";
|
import { MutinyWallet } from "@mutinywallet/mutiny-wasm";
|
||||||
|
|
||||||
export function satsToUsd(
|
import { Currency } from "~/components/ChooseCurrency";
|
||||||
|
|
||||||
|
/** satsToFiat
|
||||||
|
* returns a toLocaleString() based on the bitcoin price in the chosen currency
|
||||||
|
* @param {number} amount - Takes a number as a string to parse the formatted value
|
||||||
|
* @param {number} price - Finds the Price from the megaStore state
|
||||||
|
* @param {Currency} fiat - Takes {@link Currency} object options to determine how to format the amount input
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function satsToFiat(
|
||||||
amount: number | undefined,
|
amount: number | undefined,
|
||||||
price: number,
|
price: number,
|
||||||
formatted: boolean
|
fiat: Currency
|
||||||
): string {
|
): string {
|
||||||
if (typeof amount !== "number" || isNaN(amount)) {
|
if (typeof amount !== "number" || isNaN(amount)) {
|
||||||
return "";
|
return "";
|
||||||
@@ -12,21 +21,16 @@ export function satsToUsd(
|
|||||||
const btc = MutinyWallet.convert_sats_to_btc(
|
const btc = MutinyWallet.convert_sats_to_btc(
|
||||||
BigInt(Math.floor(amount))
|
BigInt(Math.floor(amount))
|
||||||
);
|
);
|
||||||
const usd = btc * price;
|
const fiatPrice = btc * price;
|
||||||
|
const roundedFiat = Math.round(fiatPrice);
|
||||||
if (formatted) {
|
if (
|
||||||
return usd.toLocaleString("en-US", {
|
(fiat.value !== "BTC" &&
|
||||||
style: "currency",
|
roundedFiat * 100 === Math.round(fiatPrice * 100)) ||
|
||||||
currency: "USD"
|
fiatPrice === 0
|
||||||
});
|
) {
|
||||||
|
return fiatPrice.toFixed(0);
|
||||||
} else {
|
} else {
|
||||||
// Some float fighting shenaningans
|
return fiatPrice.toFixed(fiat.maxFractionalDigits);
|
||||||
const roundedUsd = Math.round(usd);
|
|
||||||
if (roundedUsd * 100 === Math.round(usd * 100)) {
|
|
||||||
return usd.toFixed(0);
|
|
||||||
} else {
|
|
||||||
return usd.toFixed(2);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@@ -34,7 +38,51 @@ export function satsToUsd(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usdToSats(
|
/** satsToFormattedFiat
|
||||||
|
* returns a toLocaleString() based on the bitcoin price in the chosen currency with its appropriate currency prefix
|
||||||
|
* @param {number} amount - Takes a number as a string to parse the formatted value
|
||||||
|
* @param {number} price - Finds the Price from the megaStore state
|
||||||
|
* @param {Currency} fiat - Takes {@link Currency} object options to determine how to format the amount input
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function satsToFormattedFiat(
|
||||||
|
amount: number | undefined,
|
||||||
|
price: number,
|
||||||
|
fiat: Currency
|
||||||
|
): string {
|
||||||
|
if (typeof amount !== "number" || isNaN(amount)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const btc = MutinyWallet.convert_sats_to_btc(
|
||||||
|
BigInt(Math.floor(amount))
|
||||||
|
);
|
||||||
|
const fiatPrice = btc * price;
|
||||||
|
//Handles currencies not supported by .toLocaleString() like BTC
|
||||||
|
//Returns a string with a currency symbol and a number with decimals equal to the maxFractionalDigits
|
||||||
|
if (fiat.hasSymbol) {
|
||||||
|
return (
|
||||||
|
fiat.hasSymbol +
|
||||||
|
fiatPrice.toLocaleString(navigator.languages[0] || "en-US", {
|
||||||
|
minimumFractionDigits:
|
||||||
|
fiatPrice === 0 ? 0 : fiat.maxFractionalDigits,
|
||||||
|
maximumFractionDigits: fiat.maxFractionalDigits
|
||||||
|
})
|
||||||
|
);
|
||||||
|
//Handles currencies with no symbol only an ISO code
|
||||||
|
} else {
|
||||||
|
return fiatPrice.toLocaleString(navigator.languages[0], {
|
||||||
|
minimumFractionDigits:
|
||||||
|
fiatPrice === 0 ? 0 : fiat.maxFractionalDigits
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fiatToSats(
|
||||||
amount: number | undefined,
|
amount: number | undefined,
|
||||||
price: number,
|
price: number,
|
||||||
formatted: boolean
|
formatted: boolean
|
||||||
|
|||||||
Reference in New Issue
Block a user