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;
|
||||
center?: boolean;
|
||||
}> = (props) => {
|
||||
const [state, _actions] = useMegaStore();
|
||||
return (
|
||||
<div
|
||||
class="flex flex-col gap-1"
|
||||
@@ -38,6 +39,7 @@ export const ActivityAmount: ParentComponent<{
|
||||
<AmountFiat
|
||||
amountSats={Number(props.amount)}
|
||||
denominationSize="sm"
|
||||
loading={state.price === 0}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,13 +4,13 @@ import bolt from "~/assets/icons/bolt.svg";
|
||||
import chain from "~/assets/icons/chain.svg";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import { satsToUsd } from "~/utils";
|
||||
import { satsToFormattedFiat } from "~/utils";
|
||||
|
||||
function prettyPrintAmount(n?: number | bigint): string {
|
||||
if (!n || n.valueOf() === 0) {
|
||||
return "0";
|
||||
}
|
||||
return n.toLocaleString();
|
||||
return n.toLocaleString(navigator.languages[0]);
|
||||
}
|
||||
|
||||
export function AmountSats(props: {
|
||||
@@ -35,7 +35,7 @@ export function AmountSats(props: {
|
||||
<Show when={props.icon === "minus"}>
|
||||
<span>-</span>
|
||||
</Show>
|
||||
{props.loading ? "..." : prettyPrintAmount(props.amountSats)}
|
||||
{props.loading ? "…" : prettyPrintAmount(props.amountSats)}
|
||||
|
||||
<span
|
||||
class="text-base font-light"
|
||||
@@ -72,15 +72,19 @@ export function AmountFiat(props: {
|
||||
loading?: boolean;
|
||||
denominationSize?: "sm" | "lg" | "xl";
|
||||
}) {
|
||||
const i18n = useI18n();
|
||||
const [state, _] = useMegaStore();
|
||||
|
||||
const amountInUsd = () =>
|
||||
satsToUsd(state.price, Number(props.amountSats) || 0, true);
|
||||
const amountInFiat = () =>
|
||||
(state.fiat.value === "BTC" ? "" : "~") +
|
||||
satsToFormattedFiat(
|
||||
state.price,
|
||||
Number(props.amountSats) || 0,
|
||||
state.fiat
|
||||
);
|
||||
|
||||
return (
|
||||
<h2 class="font-light">
|
||||
~{props.loading ? "..." : amountInUsd()}
|
||||
{props.loading ? "…" : amountInFiat()}
|
||||
<span
|
||||
classList={{
|
||||
"text-sm": props.denominationSize === "sm",
|
||||
@@ -88,7 +92,8 @@ export function AmountFiat(props: {
|
||||
"text-xl": props.denominationSize === "xl"
|
||||
}}
|
||||
>
|
||||
{i18n.t("common.usd")}
|
||||
|
||||
{props.loading ? "" : state.fiat.value}
|
||||
</span>
|
||||
</h2>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createMemo, Match, ParentComponent, Show, Switch } from "solid-js";
|
||||
import { AmountEditable, Card, VStack } from "~/components";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import { satsToUsd } from "~/utils";
|
||||
import { satsToFormattedFiat } from "~/utils";
|
||||
|
||||
const noop = () => {
|
||||
// do nothing
|
||||
@@ -24,7 +24,6 @@ const KeyValue: ParentComponent<{ key: string; gray?: boolean }> = (props) => {
|
||||
export const InlineAmount: ParentComponent<{
|
||||
amount: string;
|
||||
sign?: string;
|
||||
fiat?: boolean;
|
||||
}> = (props) => {
|
||||
const i18n = useI18n();
|
||||
const prettyPrint = createMemo(() => {
|
||||
@@ -32,34 +31,34 @@ export const InlineAmount: ParentComponent<{
|
||||
if (isNaN(parsed)) {
|
||||
return props.amount;
|
||||
} else {
|
||||
return parsed.toLocaleString();
|
||||
return parsed.toLocaleString(navigator.languages[0]);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="inline-block text-lg">
|
||||
{props.sign ? `${props.sign} ` : ""}
|
||||
{props.fiat ? "$" : ""}
|
||||
{prettyPrint()}{" "}
|
||||
<span class="text-sm">
|
||||
{props.fiat ? i18n.t("common.usd") : i18n.t("common.sats")}
|
||||
</span>
|
||||
{prettyPrint()} <span class="text-sm">{i18n.t("common.sats")}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function USDShower(props: { amountSats: string; fee?: string }) {
|
||||
const i18n = useI18n();
|
||||
const [state, _] = useMegaStore();
|
||||
const amountInUsd = () =>
|
||||
satsToUsd(state.price, add(props.amountSats, props.fee), true);
|
||||
const amountInFiat = () =>
|
||||
(state.fiat.value === "BTC" ? "" : "~") +
|
||||
satsToFormattedFiat(
|
||||
state.price,
|
||||
add(props.amountSats, props.fee),
|
||||
state.fiat
|
||||
);
|
||||
|
||||
return (
|
||||
<Show when={!(props.amountSats === "0")}>
|
||||
<KeyValue gray key="">
|
||||
<div class="self-end">
|
||||
~{amountInUsd()}
|
||||
<span class="text-sm">{i18n.t("common.usd")}</span>
|
||||
{amountInFiat()}
|
||||
<span class="text-sm">{state.fiat.value}</span>
|
||||
</div>
|
||||
</KeyValue>
|
||||
</Show>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Dialog } from "@kobalte/core";
|
||||
import {
|
||||
createEffect,
|
||||
createResource,
|
||||
createSignal,
|
||||
For,
|
||||
@@ -20,24 +21,52 @@ import { useI18n } from "~/i18n/context";
|
||||
import { Network } from "~/logic/mutinyWalletSetup";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import { DIALOG_CONTENT, DIALOG_POSITIONER } from "~/styles/dialogs";
|
||||
import { satsToUsd, usdToSats } from "~/utils";
|
||||
import { fiatToSats, satsToFiat } from "~/utils";
|
||||
|
||||
function fiatInputSanitizer(input: string): string {
|
||||
// Make sure only numbers and a single decimal point are allowed
|
||||
const numeric = input.replace(/[^0-9.]/g, "").replace(/(\..*)\./g, "$1");
|
||||
import { Currency } from "./ChooseCurrency";
|
||||
|
||||
// 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
|
||||
const cleaned = numeric.replace(/^0([^.]|$)/g, "$1").replace(/^\./g, "0.");
|
||||
|
||||
// If there are three characters after the decimal, shift the decimal
|
||||
const shifted = cleaned.match(/(\.[0-9]{3}).*/g)
|
||||
? (parseFloat(cleaned) * 10).toFixed(2)
|
||||
// If there are more characters after the decimal than allowed, shift the decimal
|
||||
const shiftRegex = new RegExp(
|
||||
"(\\.[0-9]{" + (maxDecimals + 1) + "}).*",
|
||||
"g"
|
||||
);
|
||||
const shifted = cleaned.match(shiftRegex)
|
||||
? (parseFloat(cleaned) * 10).toFixed(maxDecimals)
|
||||
: cleaned;
|
||||
|
||||
// Truncate any numbers two past the decimal
|
||||
const twoDecimals = shifted.replace(/(\.[0-9]{2}).*/g, "$1");
|
||||
// Truncate any numbers past the maxDecimal for the currency
|
||||
const decimalRegex = new RegExp("(\\.[0-9]{" + maxDecimals + "}).*", "g");
|
||||
const decimals = shifted.replace(decimalRegex, "$1");
|
||||
|
||||
return twoDecimals;
|
||||
return decimals;
|
||||
}
|
||||
|
||||
function satsInputSanitizer(input: string): string {
|
||||
@@ -53,9 +82,10 @@ function SingleDigitButton(props: {
|
||||
character: string;
|
||||
onClick: (c: string) => void;
|
||||
onClear: () => void;
|
||||
fiat: boolean;
|
||||
fiat?: Currency;
|
||||
}) {
|
||||
const i18n = useI18n();
|
||||
|
||||
let holdTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
const holdThreshold = 500;
|
||||
|
||||
@@ -85,9 +115,14 @@ function SingleDigitButton(props: {
|
||||
});
|
||||
|
||||
return (
|
||||
// Skip the "." if it's fiat
|
||||
// Skip the "." if it's sats or a fiat with no decimal option
|
||||
<Show
|
||||
when={props.fiat || !(props.character === ".")}
|
||||
when={
|
||||
(props.fiat &&
|
||||
props.fiat?.maxFractionalDigits !== 0 &&
|
||||
props.fiat?.value !== "BTC") ||
|
||||
!(props.character === "." || props.character === ",")
|
||||
}
|
||||
fallback={<div />}
|
||||
>
|
||||
<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 i18n = useI18n();
|
||||
|
||||
@@ -119,21 +159,38 @@ function BigScalingText(props: { text: string; fiat: boolean }) {
|
||||
"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}
|
||||
<span class="text-xl">
|
||||
{props.fiat ? i18n.t("common.usd") : i18n.t("common.sats")}
|
||||
{props.fiat ? props.fiat.value : i18n.t("common.sats")}
|
||||
</span>
|
||||
</Show>
|
||||
</h1>
|
||||
);
|
||||
}
|
||||
|
||||
function SmallSubtleAmount(props: { text: string; fiat: boolean }) {
|
||||
function SmallSubtleAmount(props: {
|
||||
text: string;
|
||||
fiat?: Currency;
|
||||
mode: "fiat" | "sats";
|
||||
loading: boolean;
|
||||
}) {
|
||||
const i18n = useI18n();
|
||||
|
||||
return (
|
||||
<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">
|
||||
{props.fiat ? i18n.t("common.usd") : i18n.t("common.sats")}
|
||||
{props.fiat ? props.fiat.value : i18n.t("common.sats")}
|
||||
</span>
|
||||
<img
|
||||
class={"pb-[4px] pl-[4px] hover:cursor-pointer"}
|
||||
@@ -142,30 +199,62 @@ function SmallSubtleAmount(props: { text: string; fiat: boolean }) {
|
||||
width={24}
|
||||
alt="Swap currencies"
|
||||
/>
|
||||
</Show>
|
||||
</h2>
|
||||
);
|
||||
}
|
||||
|
||||
function toDisplayHandleNaN(input: string, _fiat: boolean): string {
|
||||
function toDisplayHandleNaN(input: string, fiat?: Currency): string {
|
||||
const parsed = Number(input);
|
||||
|
||||
//handle decimals so the user can always see the accurate amount
|
||||
if (isNaN(parsed)) {
|
||||
return "0";
|
||||
} 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")) {
|
||||
return parsed.toFixed(1);
|
||||
return parsed.toLocaleString(navigator.languages[0], {
|
||||
minimumFractionDigits: 1
|
||||
});
|
||||
} 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 (
|
||||
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)
|
||||
) {
|
||||
return parsed.toFixed(2);
|
||||
return parsed.toLocaleString(navigator.languages[0], {
|
||||
minimumFractionDigits: 2
|
||||
});
|
||||
} else {
|
||||
return parsed.toLocaleString();
|
||||
return parsed.toLocaleString(navigator.languages[0], {
|
||||
maximumFractionDigits: 3
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,13 +276,41 @@ export const AmountEditable: ParentComponent<{
|
||||
props.initialAmountSats || "0"
|
||||
);
|
||||
const [localFiat, setLocalFiat] = createSignal(
|
||||
satsToUsd(
|
||||
satsToFiat(
|
||||
state.price,
|
||||
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 = [
|
||||
{
|
||||
label: i18n.t("receive.amount_editable.fix_amounts.ten_k"),
|
||||
@@ -209,11 +326,28 @@ export const AmountEditable: ParentComponent<{
|
||||
}
|
||||
];
|
||||
|
||||
const FIXED_AMOUNTS_USD = [
|
||||
{ label: "$1", amount: "1" },
|
||||
{ label: "$10", amount: "10" },
|
||||
{ label: "$100", amount: "100" }
|
||||
// Wait to set fiat amounts until we have a price when loading the page
|
||||
let FIXED_AMOUNTS_FIAT;
|
||||
|
||||
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 = [
|
||||
"1",
|
||||
@@ -225,13 +359,14 @@ export const AmountEditable: ParentComponent<{
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
".",
|
||||
decimalDigitDivider,
|
||||
"0",
|
||||
i18n.t("receive.amount_editable.del")
|
||||
];
|
||||
|
||||
const displaySats = () => toDisplayHandleNaN(localSats(), false);
|
||||
const displayFiat = () => `$${toDisplayHandleNaN(localFiat(), true)}`;
|
||||
const displaySats = () => toDisplayHandleNaN(localSats());
|
||||
const displayFiat = () =>
|
||||
state.price !== 0 ? toDisplayHandleNaN(localFiat(), state.fiat) : "…";
|
||||
|
||||
let satsInputRef!: 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 inputSanitizer = isFiatMode
|
||||
? fiatInputSanitizer
|
||||
: satsInputSanitizer;
|
||||
const character = characterInput === "," ? "." : characterInput;
|
||||
let inputSanitizer;
|
||||
if (isFiatMode) {
|
||||
inputSanitizer = fiatInputSanitizer;
|
||||
} else {
|
||||
inputSanitizer = satsInputSanitizer;
|
||||
}
|
||||
const localValue = isFiatMode ? localFiat : localSats;
|
||||
|
||||
let sane;
|
||||
@@ -306,27 +463,57 @@ export const AmountEditable: ParentComponent<{
|
||||
character === "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";
|
||||
} 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 {
|
||||
sane = inputSanitizer(localValue().slice(0, -1));
|
||||
sane = inputSanitizer(
|
||||
localValue().slice(0, -1),
|
||||
state.fiat.maxFractionalDigits
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (localValue() === "0") {
|
||||
sane = inputSanitizer(character);
|
||||
if (localValue() === "0" && state.fiat.value !== "BTC") {
|
||||
sane = inputSanitizer(
|
||||
character,
|
||||
state.fiat.maxFractionalDigits
|
||||
);
|
||||
} else if (state.fiat.value === "BTC" && isFiatMode) {
|
||||
sane = inputSanitizer(
|
||||
Number(localValue()).toFixed(8) + character,
|
||||
state.fiat.maxFractionalDigits
|
||||
);
|
||||
} else {
|
||||
sane = inputSanitizer(localValue() + character);
|
||||
sane = inputSanitizer(
|
||||
localValue() + character,
|
||||
state.fiat.maxFractionalDigits
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isFiatMode) {
|
||||
setLocalFiat(sane);
|
||||
setLocalSats(
|
||||
usdToSats(state.price, parseFloat(sane || "0") || 0, false)
|
||||
fiatToSats(state.price, parseFloat(sane || "0") || 0, false)
|
||||
);
|
||||
} else {
|
||||
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
|
||||
@@ -338,10 +525,10 @@ export const AmountEditable: ParentComponent<{
|
||||
|
||||
if (isFiatMode) {
|
||||
setLocalFiat("0");
|
||||
setLocalSats(usdToSats(state.price, parseFloat("0") || 0, false));
|
||||
setLocalSats(fiatToSats(state.price, parseFloat("0") || 0, false));
|
||||
} else {
|
||||
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
|
||||
@@ -352,11 +539,13 @@ export const AmountEditable: ParentComponent<{
|
||||
if (mode() === "fiat") {
|
||||
setLocalFiat(amount);
|
||||
setLocalSats(
|
||||
usdToSats(state.price, parseFloat(amount || "0") || 0, false)
|
||||
fiatToSats(state.price, parseFloat(amount || "0") || 0, false)
|
||||
);
|
||||
} else {
|
||||
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);
|
||||
setLocalSats(props.initialAmountSats);
|
||||
setLocalFiat(
|
||||
satsToUsd(
|
||||
satsToFiat(
|
||||
state.price,
|
||||
parseInt(props.initialAmountSats || "0") || 0,
|
||||
false
|
||||
state.fiat
|
||||
)
|
||||
);
|
||||
props.exitRoute && navigate(props.exitRoute);
|
||||
@@ -378,7 +567,9 @@ export const AmountEditable: ParentComponent<{
|
||||
function handleSubmit(e: SubmitEvent | MouseEvent) {
|
||||
e.preventDefault();
|
||||
props.setAmountSats(BigInt(localSats()));
|
||||
setLocalFiat(satsToUsd(state.price, Number(localSats()) || 0, false));
|
||||
setLocalFiat(
|
||||
satsToFiat(state.price, Number(localSats()) || 0, state.fiat)
|
||||
);
|
||||
setIsOpen(false);
|
||||
}
|
||||
|
||||
@@ -386,20 +577,55 @@ export const AmountEditable: ParentComponent<{
|
||||
const { value } = e.target as HTMLInputElement;
|
||||
const sane = satsInputSanitizer(value);
|
||||
setLocalSats(sane);
|
||||
setLocalFiat(satsToUsd(state.price, Number(sane) || 0, false));
|
||||
setLocalFiat(satsToFiat(state.price, Number(sane) || 0, state.fiat));
|
||||
}
|
||||
|
||||
function handleFiatInput(e: InputEvent) {
|
||||
const { value } = e.target as HTMLInputElement;
|
||||
const sane = fiatInputSanitizer(value);
|
||||
const { value } = e.currentTarget as HTMLInputElement;
|
||||
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);
|
||||
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"));
|
||||
}
|
||||
focus();
|
||||
}
|
||||
|
||||
@@ -428,7 +654,7 @@ export const AmountEditable: ParentComponent<{
|
||||
) {
|
||||
return (
|
||||
Number(props.maxAmountSats) - Number(props.fee)
|
||||
).toLocaleString();
|
||||
).toLocaleString(navigator.languages[0]);
|
||||
} else {
|
||||
return localSats();
|
||||
}
|
||||
@@ -496,7 +722,7 @@ export const AmountEditable: ParentComponent<{
|
||||
<div class="flex justify-center">
|
||||
<div
|
||||
class="flex w-max flex-col items-center justify-center gap-4 p-4"
|
||||
onClick={toggle}
|
||||
onClick={() => toggle(state.price === 0)}
|
||||
>
|
||||
<BigScalingText
|
||||
text={
|
||||
@@ -504,7 +730,13 @@ export const AmountEditable: ParentComponent<{
|
||||
? displayFiat()
|
||||
: displaySats()
|
||||
}
|
||||
fiat={mode() === "fiat"}
|
||||
fiat={
|
||||
mode() === "fiat"
|
||||
? state.fiat
|
||||
: undefined
|
||||
}
|
||||
mode={mode()}
|
||||
loading={state.price === 0}
|
||||
/>
|
||||
<SmallSubtleAmount
|
||||
text={
|
||||
@@ -512,7 +744,13 @@ export const AmountEditable: ParentComponent<{
|
||||
? displayFiat()
|
||||
: displaySats()
|
||||
}
|
||||
fiat={mode() !== "fiat"}
|
||||
fiat={
|
||||
mode() !== "fiat"
|
||||
? state.fiat
|
||||
: undefined
|
||||
}
|
||||
mode={mode()}
|
||||
loading={state.price === 0}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -534,7 +772,7 @@ export const AmountEditable: ParentComponent<{
|
||||
<For
|
||||
each={
|
||||
mode() === "fiat"
|
||||
? FIXED_AMOUNTS_USD
|
||||
? FIXED_AMOUNTS_FIAT
|
||||
: FIXED_AMOUNTS_SATS
|
||||
}
|
||||
>
|
||||
@@ -550,7 +788,11 @@ export const AmountEditable: ParentComponent<{
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
<Show when={props.maxAmountSats}>
|
||||
<Show
|
||||
when={
|
||||
mode() === "sats" && props.maxAmountSats
|
||||
}
|
||||
>
|
||||
<button
|
||||
onClick={() => {
|
||||
setFixedAmount(
|
||||
@@ -568,7 +810,11 @@ export const AmountEditable: ParentComponent<{
|
||||
<For each={CHARACTERS}>
|
||||
{(character) => (
|
||||
<SingleDigitButton
|
||||
fiat={mode() === "fiat"}
|
||||
fiat={
|
||||
mode() === "fiat"
|
||||
? state.fiat
|
||||
: undefined
|
||||
}
|
||||
character={character}
|
||||
onClick={handleCharacterInput}
|
||||
onClear={handleClear}
|
||||
|
||||
@@ -75,6 +75,7 @@ export function BalanceBox(props: { loading?: boolean }) {
|
||||
state.balance?.lightning || 0
|
||||
}
|
||||
denominationSize="sm"
|
||||
loading={state.price === 0}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -97,6 +98,7 @@ export function BalanceBox(props: { loading?: boolean }) {
|
||||
<AmountFiat
|
||||
amountSats={totalOnchain()}
|
||||
denominationSize="sm"
|
||||
loading={state.price === 0}
|
||||
/>
|
||||
</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 "./BalanceBox";
|
||||
export * from "./BetaWarningModal";
|
||||
export * from "./ChooseCurrency";
|
||||
export * from "./ContactEditor";
|
||||
export * from "./ContactForm";
|
||||
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 "./ProgressBar";
|
||||
export * from "./Radio";
|
||||
export * from "./SelectField";
|
||||
export * from "./TextField";
|
||||
export * from "./ExternalLink";
|
||||
export * from "./LoadingSpinner";
|
||||
|
||||
@@ -5,7 +5,6 @@ export default {
|
||||
home: "Home",
|
||||
sats: "SATS",
|
||||
sat: "SAT",
|
||||
usd: "USD",
|
||||
fee: "Fee",
|
||||
send: "Send",
|
||||
receive: "Receive",
|
||||
@@ -185,10 +184,10 @@ export default {
|
||||
settings: {
|
||||
header: "Settings",
|
||||
support: "Learn how to support Mutiny",
|
||||
general: "GENERAL",
|
||||
beta_features: "BETA FEATURES",
|
||||
debug_tools: "DEBUG TOOLS",
|
||||
danger_zone: "Danger zone",
|
||||
general: "General",
|
||||
admin: {
|
||||
title: "Admin Page",
|
||||
caption: "Our internal debug tools. Use wisely!",
|
||||
@@ -345,6 +344,17 @@ export default {
|
||||
forgot_password_link: "Forgot 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: {
|
||||
title: "LNURL 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
|
||||
header={i18n.t("settings.general")}
|
||||
links={[
|
||||
{
|
||||
href: "/settings/currency",
|
||||
text: i18n.t("settings.currency.title"),
|
||||
caption: i18n.t("settings.currency.caption")
|
||||
},
|
||||
{
|
||||
href: "/settings/channels",
|
||||
text: i18n.t("settings.channels.title")
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { createStore } from "solid-js/store";
|
||||
import { useSearchParams } from "solid-start";
|
||||
|
||||
import { Currency, FIAT_OPTIONS } from "~/components/ChooseCurrency";
|
||||
import { checkBrowserCompatibility } from "~/logic/browserCompatibility";
|
||||
import {
|
||||
doubleInitDefense,
|
||||
@@ -41,6 +42,7 @@ export type MegaStore = [
|
||||
is_syncing?: boolean;
|
||||
last_sync?: number;
|
||||
price: number;
|
||||
fiat: Currency;
|
||||
has_backed_up: boolean;
|
||||
wallet_loading: boolean;
|
||||
setup_error?: Error;
|
||||
@@ -74,6 +76,9 @@ export const Provider: ParentComponent = (props) => {
|
||||
deleting: false,
|
||||
scan_result: undefined as ParsedParams | undefined,
|
||||
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",
|
||||
balance: undefined as MutinyBalance | undefined,
|
||||
last_sync: undefined as number | undefined,
|
||||
@@ -209,14 +214,37 @@ export const Provider: ParentComponent = (props) => {
|
||||
try {
|
||||
if (state.mutiny_wallet && !state.is_syncing) {
|
||||
setState({ is_syncing: true });
|
||||
let price: number;
|
||||
const newBalance = await state.mutiny_wallet?.get_balance();
|
||||
const price =
|
||||
await state.mutiny_wallet?.get_bitcoin_price();
|
||||
if (state.fiat.value === "BTC") {
|
||||
setState({
|
||||
balance: newBalance,
|
||||
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) {
|
||||
console.error(e);
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
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,
|
||||
price: number,
|
||||
formatted: boolean
|
||||
fiat: Currency
|
||||
): string {
|
||||
if (typeof amount !== "number" || isNaN(amount)) {
|
||||
return "";
|
||||
@@ -12,21 +21,16 @@ export function satsToUsd(
|
||||
const btc = MutinyWallet.convert_sats_to_btc(
|
||||
BigInt(Math.floor(amount))
|
||||
);
|
||||
const usd = btc * price;
|
||||
|
||||
if (formatted) {
|
||||
return usd.toLocaleString("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD"
|
||||
});
|
||||
const fiatPrice = btc * price;
|
||||
const roundedFiat = Math.round(fiatPrice);
|
||||
if (
|
||||
(fiat.value !== "BTC" &&
|
||||
roundedFiat * 100 === Math.round(fiatPrice * 100)) ||
|
||||
fiatPrice === 0
|
||||
) {
|
||||
return fiatPrice.toFixed(0);
|
||||
} else {
|
||||
// Some float fighting shenaningans
|
||||
const roundedUsd = Math.round(usd);
|
||||
if (roundedUsd * 100 === Math.round(usd * 100)) {
|
||||
return usd.toFixed(0);
|
||||
} else {
|
||||
return usd.toFixed(2);
|
||||
}
|
||||
return fiatPrice.toFixed(fiat.maxFractionalDigits);
|
||||
}
|
||||
} catch (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,
|
||||
price: number,
|
||||
formatted: boolean
|
||||
|
||||
Reference in New Issue
Block a user