From d0dc6c4157f57fa52e0aa6bc58ad971e4fc1e4a6 Mon Sep 17 00:00:00 2001 From: Paul Miller Date: Tue, 11 Apr 2023 18:13:12 -0500 Subject: [PATCH] better receive --- src/components/AmountInput.tsx | 82 ++++++++++++++ src/components/ReceiveQrShower.tsx | 0 src/components/layout/Button.tsx | 2 +- src/components/layout/index.tsx | 10 +- src/routes/Receive.tsx | 170 +++++++++++++++++++++-------- src/utils/conversions.ts | 39 +++++++ 6 files changed, 253 insertions(+), 50 deletions(-) create mode 100644 src/components/AmountInput.tsx create mode 100644 src/components/ReceiveQrShower.tsx create mode 100644 src/utils/conversions.ts diff --git a/src/components/AmountInput.tsx b/src/components/AmountInput.tsx new file mode 100644 index 0000000..b7e5ce6 --- /dev/null +++ b/src/components/AmountInput.tsx @@ -0,0 +1,82 @@ +import { TextField } from "@kobalte/core"; +import { Match, Suspense, Switch, createEffect, createMemo, createResource, createSignal } from "solid-js"; +import { useMegaStore } from "~/state/megaStore"; +import { satsToUsd, usdToSats } from "~/utils/conversions"; + +export type AmountInputProps = { + initialAmountSats: string; + setAmountSats: (amount: string) => void; + refSetter: (el: HTMLInputElement) => void; +} + +type ActiveCurrency = "usd" | "sats" + +export function AmountInput(props: AmountInputProps) { + // We need to keep a local amount state because we need to convert between sats and USD + // But we should keep the parent state in sats + const [localAmount, setLocalAmount] = createSignal(props.initialAmountSats || "0"); + + const [state, _] = useMegaStore() + + async function getPrice() { + return await state.node_manager?.get_bitcoin_price() + } + + const [activeCurrency, setActiveCurrency] = createSignal("sats") + + const [price] = createResource(getPrice) + + const amountInUsd = createMemo(() => satsToUsd(price(), parseInt(localAmount()) || 0, true)) + const amountInSats = createMemo(() => usdToSats(price(), parseFloat(localAmount() || "0.00") || 0, true)) + + createEffect(() => { + // When the local amount changes, update the parent state if we're in sats + if (activeCurrency() === "sats") { + props.setAmountSats(localAmount()) + } else { + // If we're in USD, convert the amount to sats + props.setAmountSats(usdToSats(price(), parseFloat(localAmount() || "0.00") || 0, false)) + } + }) + + function toggleActiveCurrency() { + if (activeCurrency() === "sats") { + setActiveCurrency("usd") + // Convert the current amount of sats to USD + const usd = satsToUsd(price() || 0, parseInt(localAmount()) || 0, false) + console.log(`converted ${localAmount()} sats to ${usd} USD`) + setLocalAmount(usd); + } else { + setActiveCurrency("sats") + // Convert the current amount of USD to sats + const sats = usdToSats(price() || 0, parseInt(localAmount()) || 0, false) + console.log(`converted ${localAmount()} usd to ${sats} sats`) + setLocalAmount(sats) + } + } + + return ( +
+ +
{`Bitcoin is ${price()?.toLocaleString('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 })}`}
+ Amount {activeCurrency() === "sats" ? "(sats)" : "(USD)"} + props.refSetter(el)} inputmode={"decimal"} class="w-full p-2 rounded-lg text-black" /> + + Loading...
}> + +
{`~${amountInUsd()}`}
+
+ +
{`${amountInSats()} sats`}
+
+ + + + + + ) +} \ No newline at end of file diff --git a/src/components/ReceiveQrShower.tsx b/src/components/ReceiveQrShower.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/components/layout/Button.tsx b/src/components/layout/Button.tsx index fb1a8d9..692c3fe 100644 --- a/src/components/layout/Button.tsx +++ b/src/components/layout/Button.tsx @@ -7,7 +7,7 @@ const button = cva(["p-4", "rounded-xl", "text-xl", "font-semibold"], { variants: { intent: { active: "bg-white text-black", - inactive: "bg-black text-white border border-white", + inactive: "bg-black text-white border border-white disabled:opacity-50", blue: "bg-[#3B6CCC] text-white", red: "bg-[#F61D5B] text-white", green: "bg-[#1EA67F] text-white", diff --git a/src/components/layout/index.tsx b/src/components/layout/index.tsx index b6644d4..c74c0dc 100644 --- a/src/components/layout/index.tsx +++ b/src/components/layout/index.tsx @@ -1,4 +1,4 @@ -import { ParentComponent } from "solid-js" +import { ParentComponent, Show } from "solid-js" import Linkify from "./Linkify" import { Button, ButtonLink } from "./Button" import { Separator } from "@kobalte/core" @@ -15,11 +15,15 @@ const Card: ParentComponent<{ title?: string }> = (props) => { ) } -const SafeArea: ParentComponent = (props) => { +const SafeArea: ParentComponent<{ main?: boolean }> = (props) => { return (
- {props.children} + +
+ {props.children} +
+
) diff --git a/src/routes/Receive.tsx b/src/routes/Receive.tsx index f1bc189..c5e7382 100644 --- a/src/routes/Receive.tsx +++ b/src/routes/Receive.tsx @@ -1,40 +1,24 @@ -import { createResource, Show } from "solid-js"; +import { TextField } from "@kobalte/core"; +import { createMemo, createResource, createSignal, Match, Show, Suspense, Switch } from "solid-js"; import { QRCodeSVG } from "solid-qr-code"; -import { Button, SafeArea } from "~/components/layout"; +import { AmountInput } from "~/components/AmountInput"; +import { Button, Card, SafeArea, SmallHeader } from "~/components/layout"; import NavBar from "~/components/NavBar"; import { useMegaStore } from "~/state/megaStore"; +import { satsToUsd } from "~/utils/conversions"; +import { objectToSearchParams } from "~/utils/objectToSearchParams"; import { useCopy } from "~/utils/useCopy"; -export default function Receive() { - const [state, _] = useMegaStore() - - // TODO: would be nice if this was just newest unused address - const getNewAddress = async () => { - if (state.node_manager) { - console.log("Getting new address"); - const address = await state.node_manager?.get_new_address(); - return address - } else { - return undefined - } - }; - - const [address, { refetch: refetchAddress }] = createResource(getNewAddress); - - const [copy, copied] = useCopy({ copiedTimeout: 1000 }); - - async function share() { +function ShareButton(props: { receiveString: string }) { + async function share(receiveString: string) { // If the browser doesn't support share we can just copy the address if (!navigator.share) { - copy(address() ?? ""); - copied(); - + console.error("Share not supported") } const shareData: ShareData = { title: "Mutiny Wallet", - text: address(), + text: receiveString, } - try { await navigator.share(shareData) } catch (e) { @@ -43,27 +27,121 @@ export default function Receive() { } return ( - -
-
- -
- -
-
- - -
-
-
- Address / Invoice -
- {address()} - -
+ + ) +} + +type ReceiveState = "edit" | "show" + +export default function Receive() { + const [state, _] = useMegaStore() + + const [amount, setAmount] = createSignal("") + const [label, setLabel] = createSignal("") + + const [receiveState, setReceiveState] = createSignal("edit") + + let amountInput!: HTMLInputElement; + let labelInput!: HTMLInputElement; + + function editAmount(e: Event) { + e.preventDefault(); + setReceiveState("edit") + amountInput.focus(); + } + + function editLabel(e: Event) { + e.preventDefault(); + setReceiveState("edit") + labelInput.focus(); + } + + const [unified, setUnified] = createSignal("") + + const [copy, copied] = useCopy({ copiedTimeout: 1000 }); + + async function getUnifiedQr(amount: string, label: string) { + const bigAmount = BigInt(amount); + const bip21Raw = await state.node_manager?.create_bip21(bigAmount, label); + + const params = objectToSearchParams({ + amount: bip21Raw?.btc_amount, + label: bip21Raw?.description, + lightning: bip21Raw?.invoice + }) + + return `bitcoin:${bip21Raw?.address}?${params}` + } + + async function onSubmit(e: Event) { + e.preventDefault(); + + const unifiedQr = await getUnifiedQr(amount(), label()) + + setUnified(unifiedQr) + setReceiveState("show") + } + + async function getPrice() { + return await state.node_manager?.get_bitcoin_price() + } + + const [price] = createResource(getPrice) + + const amountInUsd = createMemo(() => satsToUsd(price(), parseInt(amount()) || 0, true)) + + return ( + +
+ + {/* If I don't have this guard then the node manager only half-works */} + + + +
+ {/* TODO this initial amount is not reactive, hope that's okay? */} + amountInput = el} /> + + Label (private) + labelInput = el} + class="w-full p-2 rounded-lg text-black" /> + + + +
+ +
+ +
+
+ + +
+ + Amount +
+

{amount()} sats

+
+
({amountInUsd()})
+ Private Label +
+

{label()}

+
+
+ + {unified()} + +
+
-
-
+ +
diff --git a/src/utils/conversions.ts b/src/utils/conversions.ts new file mode 100644 index 0000000..ed6fbdd --- /dev/null +++ b/src/utils/conversions.ts @@ -0,0 +1,39 @@ +import { NodeManager } from "@mutinywallet/node-manager"; + +export function satsToUsd(amount: number | undefined, price: number, formatted: boolean): string { + if (typeof amount !== "number" || isNaN(amount)) { + return "" + } + try { + const btc = NodeManager.convert_sats_to_btc(BigInt(Math.floor(amount))); + const usd = btc * price; + + if (formatted) { + return usd.toLocaleString('en-US', { style: 'currency', currency: 'USD' }); + } else { + return usd.toFixed(2); + } + + } catch (e) { + console.error(e); + return "" + } +} + +export function usdToSats(amount: number | undefined, price: number, formatted: boolean): string { + if (typeof amount !== "number" || isNaN(amount)) { + return "" + } + try { + const btc = price / amount; + const sats = NodeManager.convert_btc_to_sats(btc); + if (formatted) { + return parseInt(sats.toString()).toLocaleString(); + } else { + return sats.toString(); + } + } catch (e) { + console.error(e); + return "" + } +}