mirror of
https://github.com/aljazceru/mutiny-web.git
synced 2025-12-18 06:44:27 +01:00
option for address / invoice
This commit is contained in:
@@ -18,9 +18,12 @@ function SingleDigitButton(props: { character: string, onClick: (c: string) => v
|
||||
);
|
||||
}
|
||||
|
||||
export function AmountEditable(props: { initialAmountSats: string, setAmountSats: (s: bigint) => void, onSave?: () => void }) {
|
||||
export function AmountEditable(props: { initialAmountSats: string, setAmountSats: (s: bigint) => void }) {
|
||||
const [isOpen, setIsOpen] = createSignal(false);
|
||||
|
||||
const [displayAmount, setDisplayAmount] = createSignal(props.initialAmountSats || "0");
|
||||
|
||||
|
||||
let inputRef!: HTMLInputElement;
|
||||
|
||||
function handleCharacterInput(character: string) {
|
||||
@@ -101,9 +104,12 @@ export function AmountEditable(props: { initialAmountSats: string, setAmountSats
|
||||
const amountInUsd = () => satsToUsd(state.price, Number(displayAmount()) || 0, true)
|
||||
|
||||
// What we're all here for in the first place: returning a value
|
||||
function handleSubmit() {
|
||||
function handleSubmit(e: SubmitEvent | MouseEvent) {
|
||||
e.preventDefault()
|
||||
|
||||
// validate it's a number
|
||||
console.log("handling submit...");
|
||||
console.log(displayAmount());
|
||||
const number = Number(displayAmount());
|
||||
if (isNaN(number) || number < 0) {
|
||||
setDisplayAmount("0");
|
||||
@@ -112,36 +118,36 @@ export function AmountEditable(props: { initialAmountSats: string, setAmountSats
|
||||
} else {
|
||||
const bign = BigInt(displayAmount());
|
||||
props.setAmountSats(bign);
|
||||
// This is so the parent can focus the next input if it wants to
|
||||
if (props.onSave) {
|
||||
props.onSave();
|
||||
}
|
||||
}
|
||||
|
||||
setIsOpen(false);
|
||||
}
|
||||
|
||||
const DIALOG_POSITIONER = "fixed inset-0 safe-top safe-bottom z-50"
|
||||
const DIALOG_CONTENT = "h-screen-safe p-4 bg-gray/50 backdrop-blur-md bg-black/80"
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>
|
||||
<div class="p-4 rounded-xl border-2 border-m-blue">
|
||||
<Amount amountSats={Number(displayAmount())} showFiat />
|
||||
</div>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Root isOpen={isOpen()}>
|
||||
<button onClick={() => setIsOpen(true)} class="p-4 rounded-xl border-2 border-m-blue">
|
||||
<Amount amountSats={Number(displayAmount())} showFiat />
|
||||
</button>
|
||||
<Dialog.Portal>
|
||||
{/* <Dialog.Overlay class={OVERLAY} /> */}
|
||||
<div class={DIALOG_POSITIONER}>
|
||||
<Dialog.Content class={DIALOG_CONTENT}>
|
||||
{/* TODO: figure out how to submit on enter */}
|
||||
<input ref={el => inputRef = el}
|
||||
autofocus
|
||||
inputmode='none'
|
||||
type="text"
|
||||
class="opacity-0 absolute -z-10"
|
||||
value={displayAmount()}
|
||||
onInput={(e) => handleHiddenInput(e)}
|
||||
/>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input ref={el => inputRef = el}
|
||||
autofocus
|
||||
inputmode='none'
|
||||
type="text"
|
||||
class="opacity-0 absolute -z-10"
|
||||
value={displayAmount()}
|
||||
onInput={(e) => handleHiddenInput(e)}
|
||||
/>
|
||||
</form>
|
||||
<div class="flex flex-col gap-4 max-w-[400px] mx-auto">
|
||||
<div class="p-4 bg-black rounded-xl flex flex-col gap-4 items-center justify-center">
|
||||
<h1 class={`font-light text-center transition-transform ease-out duration-300 text-6xl ${scale()}`}>
|
||||
@@ -159,13 +165,9 @@ export function AmountEditable(props: { initialAmountSats: string, setAmountSats
|
||||
</For>
|
||||
</div>
|
||||
{/* TODO: this feels wrong */}
|
||||
<Dialog.CloseButton>
|
||||
<Button intent="inactive" class="w-full flex-none"
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Set Amount
|
||||
</Button>
|
||||
</Dialog.CloseButton>
|
||||
<Button intent="inactive" class="w-full flex-none" onClick={handleSubmit}>
|
||||
Set Amount
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
import { Dialog } from "@kobalte/core";
|
||||
import { JSX } from "solid-js";
|
||||
import { Button, SmallHeader } from "~/components/layout";
|
||||
import { Button, LargeHeader, SmallHeader } from "~/components/layout";
|
||||
import close from "~/assets/icons/close.svg";
|
||||
|
||||
const DIALOG_POSITIONER = "fixed inset-0 safe-top safe-bottom z-50"
|
||||
@@ -24,9 +24,9 @@ export function FullscreenModal(props: FullscreenModalProps) {
|
||||
<Dialog.Content class={DIALOG_CONTENT}>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<Dialog.Title>
|
||||
<SmallHeader>
|
||||
<LargeHeader>
|
||||
{props.title}
|
||||
</SmallHeader>
|
||||
</LargeHeader>
|
||||
</Dialog.Title>
|
||||
<Dialog.CloseButton class="p-2 hover:bg-white/10 rounded-lg active:bg-m-blue">
|
||||
<img src={close} alt="Close" />
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
import { RadioGroup } from "@kobalte/core";
|
||||
import { For } from "solid-js";
|
||||
import { For, Show } from "solid-js";
|
||||
|
||||
type Choices = { value: string, label: string, caption: string }[]
|
||||
|
||||
// TODO: how could would it be if we could just pass the estimated fees in here?
|
||||
export function StyledRadioGroup(props: { value: string, choices: Choices, onValueChange: (value: string) => void }) {
|
||||
export function StyledRadioGroup(props: { value: string, choices: Choices, onValueChange: (value: string) => void, small?: boolean, }) {
|
||||
return (
|
||||
<RadioGroup.Root value={props.value} onValueChange={(e) => props.onValueChange(e)} class="grid w-full gap-4 grid-cols-2">
|
||||
<RadioGroup.Root value={props.value} onValueChange={(e) => props.onValueChange(e)} class={`grid w-full gap-${props.small ? 2 : 4} grid-cols-${props.choices.length}`}>
|
||||
<For each={props.choices}>
|
||||
{choice =>
|
||||
<RadioGroup.Item value={choice.value} class="ui-checked:bg-neutral-950 bg-white/10 rounded outline outline-black/50 ui-checked:outline-m-blue ui-checked:outline-2">
|
||||
<div class="py-3 px-4">
|
||||
<div class={props.small ? "py-2 px-2" : "py-3 px-4"}>
|
||||
<RadioGroup.ItemInput />
|
||||
<RadioGroup.ItemControl >
|
||||
<RadioGroup.ItemIndicator />
|
||||
</RadioGroup.ItemControl>
|
||||
<RadioGroup.ItemLabel class="ui-checked:text-white text-neutral-400">
|
||||
<div class="block">
|
||||
<div class="text-lg font-semibold">{choice.label}</div>
|
||||
<div class="text-sm font-light">{choice.caption}</div>
|
||||
<div class={`text-${props.small ? "base" : "lg"} font-semibold`}>{choice.label}</div>
|
||||
<Show when={!props.small}>
|
||||
<div class="text-sm font-light">{choice.caption}</div>
|
||||
</Show>
|
||||
</div>
|
||||
</RadioGroup.ItemLabel>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { TextField } from "@kobalte/core";
|
||||
import { MutinyBip21RawMaterials, MutinyInvoice } from "@mutinywallet/mutiny-wasm";
|
||||
import { createEffect, createMemo, createResource, createSignal, Match, onCleanup, Switch } from "solid-js";
|
||||
import { createEffect, createResource, createSignal, For, Match, onCleanup, Switch } from "solid-js";
|
||||
import { QRCodeSVG } from "solid-qr-code";
|
||||
import { AmountEditable } from "~/components/AmountEditable";
|
||||
import { Button, Card, DefaultMain, LargeHeader, NodeManagerGuard, SafeArea, SmallHeader } from "~/components/layout";
|
||||
import { Button, Card, LargeHeader, NodeManagerGuard, 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";
|
||||
import mempoolTxUrl from "~/utils/mempoolTxUrl";
|
||||
@@ -15,7 +13,8 @@ import party from '~/assets/party.gif';
|
||||
import { Amount } from "~/components/Amount";
|
||||
import { FullscreenModal } from "~/components/layout/FullscreenModal";
|
||||
import { BackButton } from "~/components/layout/BackButton";
|
||||
import { TagEditor } from "~/components/TagEditor";
|
||||
import { TagEditor, TagItem } from "~/components/TagEditor";
|
||||
import { StyledRadioGroup } from "~/components/layout/Radio";
|
||||
|
||||
type OnChainTx = {
|
||||
transaction: {
|
||||
@@ -41,6 +40,19 @@ type OnChainTx = {
|
||||
}
|
||||
}
|
||||
|
||||
const createUniqueId = () => Math.random().toString(36).substr(2, 9);
|
||||
|
||||
const fakeContacts: TagItem[] = [
|
||||
{ id: createUniqueId(), name: "Unknown", kind: "text" },
|
||||
{ id: createUniqueId(), name: "Alice", kind: "contact" },
|
||||
{ id: createUniqueId(), name: "Bob", kind: "contact" },
|
||||
{ id: createUniqueId(), name: "Carol", kind: "contact" },
|
||||
]
|
||||
|
||||
const RECEIVE_FLAVORS = [{ value: "unified", label: "Unified", caption: "Sender decides" }, { value: "lightning", label: "Lightning", caption: "Fast and cool" }, { value: "onchain", label: "On-chain", caption: "Just like Satoshi did it" }]
|
||||
|
||||
type ReceiveFlavor = "unified" | "lightning" | "onchain"
|
||||
|
||||
function ShareButton(props: { receiveString: string }) {
|
||||
async function share(receiveString: string) {
|
||||
// If the browser doesn't support share we can just copy the address
|
||||
@@ -70,26 +82,29 @@ export default function Receive() {
|
||||
const [state, _] = useMegaStore()
|
||||
|
||||
const [amount, setAmount] = createSignal("")
|
||||
const [label, setLabel] = createSignal("")
|
||||
|
||||
const [receiveState, setReceiveState] = createSignal<ReceiveState>("edit")
|
||||
|
||||
const [bip21Raw, setBip21Raw] = createSignal<MutinyBip21RawMaterials>();
|
||||
|
||||
const [unified, setUnified] = createSignal("")
|
||||
|
||||
// Tagging stuff
|
||||
const [selectedValues, setSelectedValues] = createSignal<TagItem[]>([]);
|
||||
const [values, setValues] = createSignal([...fakeContacts]);
|
||||
|
||||
// The data we get after a payment
|
||||
const [paymentTx, setPaymentTx] = createSignal<OnChainTx>();
|
||||
const [paymentInvoice, setPaymentInvoice] = createSignal<MutinyInvoice>();
|
||||
|
||||
// The flavor of the receive
|
||||
const [flavor, setFlavor] = createSignal<ReceiveFlavor>("unified");
|
||||
|
||||
function clearAll() {
|
||||
setAmount("")
|
||||
setLabel("")
|
||||
setReceiveState("edit")
|
||||
setBip21Raw(undefined)
|
||||
setUnified("")
|
||||
setPaymentTx(undefined)
|
||||
setPaymentInvoice(undefined)
|
||||
setSelectedValues([])
|
||||
}
|
||||
|
||||
let amountInput!: HTMLInputElement;
|
||||
@@ -107,13 +122,21 @@ export default function Receive() {
|
||||
labelInput.focus();
|
||||
}
|
||||
|
||||
|
||||
|
||||
const [copy, copied] = useCopy({ copiedTimeout: 1000 });
|
||||
|
||||
async function getUnifiedQr(amount: string, label: string) {
|
||||
function handleCopy() {
|
||||
if (flavor() === "unified") {
|
||||
copy(unified() ?? "")
|
||||
} else if (flavor() === "lightning") {
|
||||
copy(bip21Raw()?.invoice ?? "")
|
||||
} else if (flavor() === "onchain") {
|
||||
copy(bip21Raw()?.address ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
async function getUnifiedQr(amount: string) {
|
||||
const bigAmount = BigInt(amount);
|
||||
const raw = await state.node_manager?.create_bip21(bigAmount, label);
|
||||
const raw = await state.node_manager?.create_bip21(bigAmount, "TODO DELETE ME");
|
||||
|
||||
// Save the raw info so we can watch the address and invoice
|
||||
setBip21Raw(raw);
|
||||
@@ -130,20 +153,12 @@ export default function Receive() {
|
||||
async function onSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
|
||||
const unifiedQr = await getUnifiedQr(amount(), label())
|
||||
const unifiedQr = await getUnifiedQr(amount())
|
||||
|
||||
setUnified(unifiedQr)
|
||||
setReceiveState("show")
|
||||
}
|
||||
|
||||
const amountInUsd = createMemo(() => satsToUsd(state.price, parseInt(amount()) || 0, true))
|
||||
|
||||
function handleAmountSave() {
|
||||
console.error("focusing label input...")
|
||||
console.error(labelInput)
|
||||
labelInput.focus();
|
||||
}
|
||||
|
||||
async function checkIfPaid(bip21?: MutinyBip21RawMaterials): Promise<PaidState | undefined> {
|
||||
if (bip21) {
|
||||
console.log("checking if paid...")
|
||||
@@ -179,6 +194,9 @@ export default function Receive() {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<NodeManagerGuard>
|
||||
<SafeArea>
|
||||
@@ -189,32 +207,52 @@ export default function Receive() {
|
||||
<Match when={!unified() || receiveState() === "edit"}>
|
||||
<dl>
|
||||
<dd>
|
||||
|
||||
<AmountEditable initialAmountSats={amount() || "0"} setAmountSats={setAmount} onSave={handleAmountSave} />
|
||||
<AmountEditable initialAmountSats={amount() || "0"} setAmountSats={setAmount} />
|
||||
</dd>
|
||||
<dd>
|
||||
<TagEditor />
|
||||
<TagEditor title="Tag the origin" values={values()} setValues={setValues} selectedValues={selectedValues()} setSelectedValues={setSelectedValues} />
|
||||
</dd>
|
||||
|
||||
</dl>
|
||||
<Button class="w-full" disabled={!amount() || !label()} intent="green" onClick={onSubmit}>Create Invoice</Button>
|
||||
<Button class="w-full" disabled={!amount() || !selectedValues().length} intent="green" onClick={onSubmit}>Create Invoice</Button>
|
||||
</Match>
|
||||
<Match when={unified() && receiveState() === "show"}>
|
||||
<StyledRadioGroup small value={flavor()} onValueChange={setFlavor} choices={RECEIVE_FLAVORS} />
|
||||
<div class="w-full bg-white rounded-xl">
|
||||
<QRCodeSVG value={unified() ?? ""} class="w-full h-full p-8 max-h-[400px]" />
|
||||
<Switch>
|
||||
<Match when={flavor() === "unified"}>
|
||||
<QRCodeSVG value={unified() ?? ""} class="w-full h-full p-8 max-h-[400px]" />
|
||||
</Match>
|
||||
<Match when={flavor() === "lightning"}>
|
||||
<QRCodeSVG value={bip21Raw()?.invoice ?? ""} class="w-full h-full p-8 max-h-[400px]" />
|
||||
</Match>
|
||||
<Match when={flavor() === "onchain"}>
|
||||
<QRCodeSVG value={bip21Raw()?.address ?? ""} class="w-full h-full p-8 max-h-[400px]" />
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
<div class="flex gap-2 w-full">
|
||||
<Button onClick={(_) => copy(unified() ?? "")}>{copied() ? "Copied" : "Copy"}</Button>
|
||||
<Button onClick={handleCopy}>{copied() ? "Copied" : "Copy"}</Button>
|
||||
<ShareButton receiveString={unified() ?? ""} />
|
||||
</div>
|
||||
<Card>
|
||||
<SmallHeader>Amount</SmallHeader>
|
||||
<div class="flex justify-between">
|
||||
<p>{amount()} sats</p><button onClick={editAmount}>✏️</button>
|
||||
<Amount amountSats={parseInt(amount()) || 0} showFiat={true} />
|
||||
<button onClick={editAmount}>✏️</button>
|
||||
</div>
|
||||
<pre>({amountInUsd()})</pre>
|
||||
<SmallHeader>Private Label</SmallHeader>
|
||||
<SmallHeader>Tags</SmallHeader>
|
||||
<div class="flex justify-between">
|
||||
<p>{label()} </p><button onClick={editLabel}>✏️</button>
|
||||
<div class="flex flex-wrap">
|
||||
<For each={selectedValues()}>
|
||||
{(tag) => (
|
||||
<div class=" bg-white/20 rounded px-1">
|
||||
{tag.name}
|
||||
</div>)}
|
||||
</For>
|
||||
</div>
|
||||
{/* <pre>{JSON.stringify(selectedValues(), null, 2)}</pre> */}
|
||||
<button onClick={editLabel}>✏️</button>
|
||||
</div>
|
||||
</Card>
|
||||
<Card title="Bip21">
|
||||
@@ -222,7 +260,7 @@ export default function Receive() {
|
||||
</Card>
|
||||
</Match>
|
||||
<Match when={receiveState() === "paid" && paidState() === "lightning_paid"}>
|
||||
<FullscreenModal title="Payment Received!" open={!!paidState()} setOpen={(open: boolean) => { if (!open) clearAll() }}>
|
||||
<FullscreenModal title="Payment Received" open={!!paidState()} setOpen={(open: boolean) => { if (!open) clearAll() }}>
|
||||
<div class="flex flex-col items-center gap-8">
|
||||
<img src={party} alt="party" class="w-1/2 mx-auto max-w-[50vh] aspect-square" />
|
||||
<Amount amountSats={paymentInvoice()?.amount_sats} showFiat />
|
||||
@@ -230,7 +268,7 @@ export default function Receive() {
|
||||
</FullscreenModal>
|
||||
</Match>
|
||||
<Match when={receiveState() === "paid" && paidState() === "onchain_paid"}>
|
||||
<FullscreenModal title="Payment Received!" open={!!paidState()} setOpen={(open: boolean) => { if (!open) clearAll() }}>
|
||||
<FullscreenModal title="Payment Received" open={!!paidState()} setOpen={(open: boolean) => { if (!open) clearAll() }}>
|
||||
<div class="flex flex-col items-center gap-8">
|
||||
<img src={party} alt="party" class="w-1/2 mx-auto max-w-[50vh] aspect-square" />
|
||||
<Amount amountSats={paymentTx()?.received} showFiat />
|
||||
|
||||
@@ -171,7 +171,7 @@ export default function Send() {
|
||||
<DefaultMain>
|
||||
<BackButton />
|
||||
<LargeHeader>Send Bitcoin</LargeHeader>
|
||||
<FullscreenModal title="Sent!" open={!!sentDetails()} setOpen={(open: boolean) => { if (!open) setSentDetails(undefined) }} onConfirm={() => setSentDetails(undefined)}>
|
||||
<FullscreenModal title="Sent" open={!!sentDetails()} setOpen={(open: boolean) => { if (!open) setSentDetails(undefined) }} onConfirm={() => setSentDetails(undefined)}>
|
||||
<div class="flex flex-col items-center gap-8">
|
||||
<img src={handshake} alt="party" class="w-1/2 mx-auto max-w-[50vh] zoom-image" />
|
||||
<Amount amountSats={sentDetails()?.amount} showFiat />
|
||||
@@ -247,8 +247,6 @@ export default function Send() {
|
||||
</div>
|
||||
</ButtonLink>
|
||||
</HStack>
|
||||
|
||||
|
||||
</VStack>
|
||||
</Match>
|
||||
</Switch>
|
||||
|
||||
Reference in New Issue
Block a user