mirror of
https://github.com/aljazceru/mutiny-web.git
synced 2025-12-18 06:44:27 +01:00
fancy amount editable
This commit is contained in:
@@ -9,8 +9,7 @@ function prettyPrintAmount(n?: number | bigint): string {
|
|||||||
return n.toLocaleString()
|
return n.toLocaleString()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Amount(props: { amountSats: bigint | number | undefined, showFiat?: boolean }) {
|
export function Amount(props: { amountSats: bigint | number | undefined, showFiat?: boolean, loading?: boolean }) {
|
||||||
|
|
||||||
const [state, _] = useMegaStore()
|
const [state, _] = useMegaStore()
|
||||||
|
|
||||||
async function getPrice() {
|
async function getPrice() {
|
||||||
@@ -23,11 +22,11 @@ export function Amount(props: { amountSats: bigint | number | undefined, showFia
|
|||||||
return (
|
return (
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<h1 class="text-4xl font-light">
|
<h1 class="text-4xl font-light">
|
||||||
{prettyPrintAmount(props.amountSats)} <span class='text-xl'>SAT</span>
|
{props.loading ? "..." : prettyPrintAmount(props.amountSats)} <span class='text-xl'>SATS</span>
|
||||||
</h1>
|
</h1>
|
||||||
<Show when={props.showFiat}>
|
<Show when={props.showFiat}>
|
||||||
<h2 class="text-xl font-light text-white/70" >
|
<h2 class="text-xl font-light text-white/70" >
|
||||||
≈ {amountInUsd()} <span class="text-sm">USD</span>
|
≈ {props.loading ? "..." : amountInUsd()} <span class="text-sm">USD</span>
|
||||||
</h2>
|
</h2>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
179
src/components/AmountEditable.tsx
Normal file
179
src/components/AmountEditable.tsx
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
import { For, Show, createEffect, createMemo, createResource, createSignal } from 'solid-js';
|
||||||
|
import { Button } from '~/components/layout';
|
||||||
|
import { useMegaStore } from '~/state/megaStore';
|
||||||
|
import { satsToUsd } from '~/utils/conversions';
|
||||||
|
import { Amount } from './Amount';
|
||||||
|
|
||||||
|
const CHARACTERS = ["1", "2", "3", "4", "5", "6", "7", "8", "9", ".", "0", "⌫"];
|
||||||
|
const FULLSCREEN_STYLE = 'fixed top-0 left-0 w-screen h-screen z-50 bg-sidebar-gray p-4';
|
||||||
|
|
||||||
|
function SingleDigitButton(props: { character: string, onClick: (c: string) => void }) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
class="p-2 rounded-lg hover:bg-white/10 active:bg-m-blue text-white text-4xl font-semi font-mono"
|
||||||
|
onClick={() => props.onClick(props.character)}
|
||||||
|
>
|
||||||
|
{props.character}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AmountEditable(props: { amountSats: number | bigint, setAmountSats: (s: string) => void }) {
|
||||||
|
const [isFullscreen, setIsFullscreen] = createSignal(false);
|
||||||
|
|
||||||
|
function toggleFullscreen() {
|
||||||
|
setIsFullscreen(!isFullscreen());
|
||||||
|
}
|
||||||
|
|
||||||
|
const [displayAmount, setDisplayAmount] = createSignal(props.amountSats.toString() || "0");
|
||||||
|
|
||||||
|
let inputRef!: HTMLInputElement;
|
||||||
|
|
||||||
|
function handleCharacterInput(character: string) {
|
||||||
|
if (character === "⌫") {
|
||||||
|
setDisplayAmount(displayAmount().slice(0, -1));
|
||||||
|
} else {
|
||||||
|
if (displayAmount() === "0") {
|
||||||
|
setDisplayAmount(character);
|
||||||
|
} else {
|
||||||
|
setDisplayAmount(displayAmount() + character);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// After a button press make sure we re-focus the input
|
||||||
|
inputRef.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (isFullscreen()) {
|
||||||
|
inputRef.focus();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// making a "controlled" input is a known hard problem
|
||||||
|
// https://github.com/solidjs/solid/discussions/416
|
||||||
|
function handleHiddenInput(e: Event & {
|
||||||
|
currentTarget: HTMLInputElement;
|
||||||
|
target: HTMLInputElement;
|
||||||
|
}) {
|
||||||
|
// if the input is empty, set the display amount to 0
|
||||||
|
if (e.target.value === "") {
|
||||||
|
setDisplayAmount("0");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the input starts with one or more 0s, remove them, unless the input is just 0
|
||||||
|
if (e.target.value.startsWith("0") && e.target.value !== "0") {
|
||||||
|
setDisplayAmount(e.target.value.replace(/^0+/, ""));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there's already a decimal point, don't allow another one
|
||||||
|
if (e.target.value.includes(".") && e.target.value.endsWith(".")) {
|
||||||
|
setDisplayAmount(e.target.value.slice(0, -1));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDisplayAmount(e.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// I tried to do this with cooler math but I think it gets confused between decimal and percent
|
||||||
|
const scale = createMemo(() => {
|
||||||
|
const chars = displayAmount().length;
|
||||||
|
|
||||||
|
if (chars > 9) {
|
||||||
|
return "scale-90"
|
||||||
|
} else if (chars > 8) {
|
||||||
|
return "scale-95"
|
||||||
|
} else if (chars > 7) {
|
||||||
|
return "scale-100"
|
||||||
|
} else if (chars > 6) {
|
||||||
|
return "scale-105"
|
||||||
|
} else if (chars > 5) {
|
||||||
|
return "scale-110"
|
||||||
|
} else if (chars > 4) {
|
||||||
|
return "scale-125"
|
||||||
|
} else {
|
||||||
|
return "scale-150"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const prettyPrint = createMemo(() => {
|
||||||
|
let parsed = Number(displayAmount());
|
||||||
|
if (isNaN(parsed)) {
|
||||||
|
return displayAmount();
|
||||||
|
} else {
|
||||||
|
return parsed.toLocaleString();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Fiat conversion
|
||||||
|
const [state, _] = useMegaStore()
|
||||||
|
|
||||||
|
async function getPrice() {
|
||||||
|
return await state.node_manager?.get_bitcoin_price()
|
||||||
|
}
|
||||||
|
|
||||||
|
const [price] = createResource(getPrice)
|
||||||
|
const amountInUsd = () => satsToUsd(price(), Number(displayAmount()) || 0, true)
|
||||||
|
|
||||||
|
// What we're all here for in the first place: returning a value
|
||||||
|
function handleSubmit() {
|
||||||
|
// validate it's a number
|
||||||
|
const number = Number(displayAmount());
|
||||||
|
if (isNaN(number) || number < 0) {
|
||||||
|
setDisplayAmount("0");
|
||||||
|
inputRef.focus();
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
props.setAmountSats(displayAmount());
|
||||||
|
toggleFullscreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* TODO: a better transition than this */}
|
||||||
|
<div class={`cursor-pointer transition-all ${isFullscreen() && FULLSCREEN_STYLE}`}>
|
||||||
|
<Show when={isFullscreen()} fallback={<div class="p-4 rounded-xl border-2 border-m-blue" onClick={toggleFullscreen}>
|
||||||
|
<Amount amountSats={Number(displayAmount())} showFiat />
|
||||||
|
</div>}>
|
||||||
|
<input ref={el => inputRef = el}
|
||||||
|
type="text"
|
||||||
|
class="opacity-0 absolute -z-10"
|
||||||
|
value={displayAmount()}
|
||||||
|
onInput={(e) => handleHiddenInput(e)}
|
||||||
|
/>
|
||||||
|
<div class="w-full h-full max-w-[600px] mx-auto">
|
||||||
|
<div class="flex flex-col gap-4 h-full">
|
||||||
|
<div class="p-4 bg-black rounded-xl flex-1 flex flex-col gap-4 items-center justify-center">
|
||||||
|
<h1 class={`font-light text-center transition-transform ease-out duration-300 text-6xl ${scale()}`}>
|
||||||
|
{prettyPrint()} <span class='text-xl'>SATS</span>
|
||||||
|
</h1>
|
||||||
|
<h2 class="text-xl font-light text-white/70" >
|
||||||
|
≈ {amountInUsd()} <span class="text-sm">USD</span>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-3 w-full flex-none">
|
||||||
|
<For each={CHARACTERS}>
|
||||||
|
{(character) => (
|
||||||
|
<SingleDigitButton character={character} onClick={handleCharacterInput} />
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
<div class="flex-none">
|
||||||
|
<Button intent="inactive" class="w-full flex-none"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
>
|
||||||
|
Set Amount
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Motion, Presence } from "@motionone/solid";
|
import { Motion, Presence } from "@motionone/solid";
|
||||||
import { createResource, Show, Suspense } from "solid-js";
|
import { createResource, Show, Suspense } from "solid-js";
|
||||||
|
|
||||||
import { ButtonLink, SmallHeader } from "~/components/layout";
|
import { Button, ButtonLink, FancyCard, LoadingSpinner, SmallHeader } from "~/components/layout";
|
||||||
import { useMegaStore } from "~/state/megaStore";
|
import { useMegaStore } from "~/state/megaStore";
|
||||||
import { Amount } from "./Amount";
|
import { Amount } from "./Amount";
|
||||||
|
|
||||||
@@ -25,46 +25,34 @@ export default function BalanceBox() {
|
|||||||
const [balance, { refetch: refetchBalance }] = createResource(fetchBalance);
|
const [balance, { refetch: refetchBalance }] = createResource(fetchBalance);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Presence>
|
<>
|
||||||
<Motion
|
<FancyCard title="Lightning">
|
||||||
initial={{ opacity: 0 }}
|
<Suspense fallback={<Amount amountSats={0} showFiat loading={true} />}>
|
||||||
animate={{ opacity: 1 }}
|
<Amount amountSats={balance()?.lightning} showFiat loading={balance.loading} />
|
||||||
exit={{ opacity: 0 }}
|
</Suspense>
|
||||||
transition={{ duration: 0.5, easing: [0.87, 0, 0.13, 1] }}
|
</FancyCard>
|
||||||
>
|
<div class="flex gap-2 py-4">
|
||||||
<div class='border border-white rounded-xl border-b-4 p-4 flex flex-col gap-2'>
|
<ButtonLink href="/send" intent="green">Send</ButtonLink>
|
||||||
<SmallHeader>
|
<ButtonLink href="/receive" intent="blue">Receive</ButtonLink>
|
||||||
Lightning Balance
|
</div>
|
||||||
</SmallHeader>
|
<FancyCard title="On-Chain">
|
||||||
<div onClick={refetchBalance}>
|
<Suspense fallback={<Amount amountSats={0} showFiat loading={true} />}>
|
||||||
<Suspense fallback={"..."}>
|
<Amount amountSats={balance()?.confirmed} showFiat loading={balance.loading} />
|
||||||
<Show when={balance()}>
|
</Suspense>
|
||||||
<div class="flex flex-col gap-4">
|
<Suspense>
|
||||||
<Amount amountSats={balance()?.lightning} showFiat />
|
<Show when={balance()?.unconfirmed}>
|
||||||
<SmallHeader>
|
<div class="flex flex-col gap-2">
|
||||||
On-Chain Balance
|
<header class='text-sm font-semibold uppercase text-white/50'>
|
||||||
</SmallHeader>
|
Unconfirmed Balance
|
||||||
<Amount amountSats={balance()?.confirmed} showFiat />
|
</header>
|
||||||
<Show when={balance()?.unconfirmed}>
|
<div class="text-white/50">
|
||||||
<div class="flex flex-col gap-2">
|
{prettyPrintAmount(balance()?.unconfirmed)} <span class='text-sm'>SATS</span>
|
||||||
<header class='text-sm font-semibold uppercase text-white/50'>
|
</div>
|
||||||
Unconfirmed Balance
|
</div>
|
||||||
</header>
|
</Show>
|
||||||
<div class="text-white/50">
|
</Suspense>
|
||||||
{prettyPrintAmount(balance()?.unconfirmed)} <span class='text-xl'>SAT</span>
|
<Button onClick={() => refetchBalance()}>Sync</Button>
|
||||||
</div>
|
</FancyCard>
|
||||||
</div>
|
</>
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</Suspense>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-2 py-4">
|
|
||||||
<ButtonLink href="/send" intent="green">Send</ButtonLink>
|
|
||||||
<ButtonLink href="/receive" intent="blue">Receive</ButtonLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Motion>
|
|
||||||
</Presence>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,15 @@ const InnerCard: ParentComponent<{ title?: string }> = (props) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FancyCard: ParentComponent<{ title?: string }> = (props) => {
|
||||||
|
return (
|
||||||
|
<div class='border border-white rounded-xl border-b-4 p-4 flex flex-col gap-2'>
|
||||||
|
{props.title && <SmallHeader>{props.title}</SmallHeader>}
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const SafeArea: ParentComponent<{ main?: boolean }> = (props) => {
|
const SafeArea: ParentComponent<{ main?: boolean }> = (props) => {
|
||||||
return (
|
return (
|
||||||
<div class="safe-top safe-left safe-right safe-bottom">
|
<div class="safe-top safe-left safe-right safe-bottom">
|
||||||
@@ -69,4 +78,4 @@ const LoadingSpinner = () => {
|
|||||||
|
|
||||||
const Hr = () => <Separator.Root class="my-4 border-white/20" />
|
const Hr = () => <Separator.Root class="my-4 border-white/20" />
|
||||||
|
|
||||||
export { SmallHeader, Card, SafeArea, LoadingSpinner, Button, ButtonLink, Linkify, Hr, NodeManagerGuard, FullscreenLoader, InnerCard }
|
export { SmallHeader, Card, SafeArea, LoadingSpinner, Button, ButtonLink, Linkify, Hr, NodeManagerGuard, FullscreenLoader, InnerCard, FancyCard }
|
||||||
|
|||||||
@@ -43,5 +43,5 @@ a {
|
|||||||
|
|
||||||
/* Missing you sveltekit */
|
/* Missing you sveltekit */
|
||||||
dd {
|
dd {
|
||||||
@apply mb-8 mt-4;
|
@apply mb-8 mt-2;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { Motion, Presence } from "@motionone/solid";
|
|||||||
import { useMegaStore } from "~/state/megaStore";
|
import { useMegaStore } from "~/state/megaStore";
|
||||||
import { MutinyInvoice, NodeManager } from "@mutinywallet/node-manager";
|
import { MutinyInvoice, NodeManager } from "@mutinywallet/node-manager";
|
||||||
import { bip21decode } from "~/utils/TEMPbip21";
|
import { bip21decode } from "~/utils/TEMPbip21";
|
||||||
|
import { AmountEditable } from "~/components/AmountEditable";
|
||||||
|
|
||||||
type SendSource = "lightning" | "onchain";
|
type SendSource = "lightning" | "onchain";
|
||||||
|
|
||||||
@@ -29,6 +30,16 @@ export default function Send() {
|
|||||||
const [address, setAddress] = createSignal<string>();
|
const [address, setAddress] = createSignal<string>();
|
||||||
const [description, setDescription] = createSignal<string>();
|
const [description, setDescription] = createSignal<string>();
|
||||||
|
|
||||||
|
function clearAll() {
|
||||||
|
setDestination("");
|
||||||
|
setPrivateLabel("");
|
||||||
|
setAmountSats(0n);
|
||||||
|
setSource("lightning");
|
||||||
|
setInvoice(undefined);
|
||||||
|
setAddress(undefined);
|
||||||
|
setDescription(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
async function decode(source: string) {
|
async function decode(source: string) {
|
||||||
if (!source) return;
|
if (!source) return;
|
||||||
try {
|
try {
|
||||||
@@ -59,6 +70,7 @@ export default function Send() {
|
|||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("error", e)
|
console.error("error", e)
|
||||||
|
clearAll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,26 +112,6 @@ export default function Send() {
|
|||||||
<h1 class="text-2xl font-semibold uppercase border-b-2 border-b-white">Send Bitcoin</h1>
|
<h1 class="text-2xl font-semibold uppercase border-b-2 border-b-white">Send Bitcoin</h1>
|
||||||
<dl>
|
<dl>
|
||||||
<dt>
|
<dt>
|
||||||
<SmallHeader>
|
|
||||||
Source
|
|
||||||
</SmallHeader>
|
|
||||||
</dt>
|
|
||||||
<dd>
|
|
||||||
<div class="flex gap-4 items-start">
|
|
||||||
<Button onClick={() => setSource("lightning")} intent={source() === "lightning" ? "active" : "inactive"} layout="small">Lightning</Button>
|
|
||||||
<Button onClick={() => setSource("onchain")} intent={source() === "onchain" ? "active" : "inactive"} layout="small">On-Chain</Button>
|
|
||||||
</div>
|
|
||||||
</dd>
|
|
||||||
<dt>
|
|
||||||
<SmallHeader>
|
|
||||||
How Much
|
|
||||||
</SmallHeader>
|
|
||||||
</dt>
|
|
||||||
<dd>
|
|
||||||
<Amount amountSats={amountSats() || 0} showFiat />
|
|
||||||
</dd>
|
|
||||||
<dt>
|
|
||||||
|
|
||||||
<SmallHeader>Destination</SmallHeader>
|
<SmallHeader>Destination</SmallHeader>
|
||||||
</dt>
|
</dt>
|
||||||
<dd>
|
<dd>
|
||||||
@@ -140,31 +132,53 @@ export default function Send() {
|
|||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</div>}>
|
</div>}>
|
||||||
<Presence>
|
<div class="flex flex-col gap-2">
|
||||||
<Motion
|
<Show when={address()}>
|
||||||
initial={{ opacity: 0 }}
|
<code class="line-clamp-3 text-sm break-all">{source() === "onchain" && "→"} {address()}</code>
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
>
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<Show when={address()}>
|
|
||||||
<code class="line-clamp-3 text-sm break-all">{source() === "onchain" && "→"} {address()}</code>
|
|
||||||
|
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={invoice()}>
|
<Show when={invoice()}>
|
||||||
<code class="line-clamp-3 text-sm break-all">{source() === "lightning" && "→"} {invoice()?.bolt11}</code>
|
<code class="line-clamp-3 text-sm break-all">{source() === "lightning" && "→"} {invoice()?.bolt11}</code>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{/* <code class="line-clamp-3 text-sm break-all mb-2">{destination()}</code> */}
|
||||||
|
<Show when={destination()}>
|
||||||
|
<Button intent="inactive" onClick={clearAll}>Clear</Button>
|
||||||
|
</Show>
|
||||||
|
|
||||||
</div>
|
|
||||||
{/* <code class="line-clamp-3 text-sm break-all mb-2">{destination()}</code> */}
|
|
||||||
</Motion>
|
|
||||||
</Presence>
|
|
||||||
</Show>
|
</Show>
|
||||||
</InnerCard>
|
</InnerCard>
|
||||||
|
|
||||||
|
|
||||||
</dd>
|
</dd>
|
||||||
|
|
||||||
|
<Show when={address() && invoice()}>
|
||||||
|
<dt>
|
||||||
|
<SmallHeader>
|
||||||
|
Payment Method
|
||||||
|
</SmallHeader>
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
<div class="flex gap-4 items-start">
|
||||||
|
<Button onClick={() => setSource("lightning")} intent={source() === "lightning" ? "active" : "inactive"} layout="small">Lightning</Button>
|
||||||
|
<Button onClick={() => setSource("onchain")} intent={source() === "onchain" ? "active" : "inactive"} layout="small">On-Chain</Button>
|
||||||
|
</div>
|
||||||
|
</dd>
|
||||||
|
</Show>
|
||||||
|
<Show when={destination()}>
|
||||||
|
<dt>
|
||||||
|
<SmallHeader>
|
||||||
|
Amount
|
||||||
|
</SmallHeader>
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{/* if the amount came with the invoice we can't allow setting it */}
|
||||||
|
<Show when={!(invoice()?.amount_sats)} fallback={<Amount amountSats={amountSats() || 0} showFiat />}>
|
||||||
|
<AmountEditable amountSats={amountSats() || 0} setAmountSats={setAmountSats} />
|
||||||
|
</Show>
|
||||||
|
</dd>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
|
||||||
<Show when={description()}>
|
<Show when={description()}>
|
||||||
<dt>
|
<dt>
|
||||||
|
|
||||||
@@ -176,31 +190,32 @@ export default function Send() {
|
|||||||
</dd>
|
</dd>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<TextField.Root
|
<Show when={destination()}>
|
||||||
value={privateLabel()}
|
<TextField.Root
|
||||||
onValueChange={setPrivateLabel}
|
value={privateLabel()}
|
||||||
class="flex flex-col gap-2"
|
onValueChange={setPrivateLabel}
|
||||||
>
|
class="flex flex-col gap-2"
|
||||||
<dt>
|
>
|
||||||
<SmallHeader>
|
<dt>
|
||||||
<TextField.Label>Label (private)</TextField.Label>
|
<SmallHeader>
|
||||||
</SmallHeader>
|
<TextField.Label>Label (private)</TextField.Label>
|
||||||
</dt>
|
</SmallHeader>
|
||||||
<dd>
|
</dt>
|
||||||
<TextField.Input
|
<dd>
|
||||||
autofocus
|
<TextField.Input
|
||||||
ref={el => labelInput = el}
|
autofocus
|
||||||
class="w-full p-2 rounded-lg text-black"
|
ref={el => labelInput = el}
|
||||||
placeholder="A helpful reminder of why you spent bitcoin"
|
class="w-full p-2 rounded-lg text-black"
|
||||||
/>
|
placeholder="A helpful reminder of why you spent bitcoin"
|
||||||
</dd>
|
/>
|
||||||
</TextField.Root>
|
</dd>
|
||||||
|
</TextField.Root>
|
||||||
|
</Show>
|
||||||
</dl>
|
</dl>
|
||||||
<Button disabled={!destination()} intent="blue" onClick={handleSend}>Confirm Send</Button>
|
<Button disabled={!destination()} intent="blue" onClick={handleSend}>Confirm Send</Button>
|
||||||
<Show when={destination()}>
|
|
||||||
<Button intent="inactive" onClick={() => setDestination("")}>Clear</Button>
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
|
{/* safety div */}
|
||||||
|
<div class="h-32" />
|
||||||
<NavBar activeTab="send" />
|
<NavBar activeTab="send" />
|
||||||
</SafeArea >
|
</SafeArea >
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { ParentComponent, createContext, createEffect, onMount, useContext } from "solid-js";
|
import { ParentComponent, createContext, createEffect, onMount, useContext } from "solid-js";
|
||||||
import { createStore } from "solid-js/store";
|
import { createStore } from "solid-js/store";
|
||||||
import { setupNodeManager } from "~/logic/nodeManagerSetup";
|
import { setupNodeManager } from "~/logic/nodeManagerSetup";
|
||||||
import { NodeManager } from "@mutinywallet/node-manager";
|
import { MutinyBalance, NodeManager } from "@mutinywallet/node-manager";
|
||||||
|
|
||||||
const MegaStoreContext = createContext<MegaStore>();
|
const MegaStoreContext = createContext<MegaStore>();
|
||||||
|
|
||||||
@@ -14,6 +14,8 @@ export type MegaStore = [{
|
|||||||
node_manager?: NodeManager;
|
node_manager?: NodeManager;
|
||||||
user_status: UserStatus;
|
user_status: UserStatus;
|
||||||
scan_result?: string;
|
scan_result?: string;
|
||||||
|
balance?: MutinyBalance;
|
||||||
|
last_sync?: number;
|
||||||
}, {
|
}, {
|
||||||
fetchUserStatus(): Promise<UserStatus>;
|
fetchUserStatus(): Promise<UserStatus>;
|
||||||
setupNodeManager(): Promise<void>;
|
setupNodeManager(): Promise<void>;
|
||||||
@@ -42,12 +44,9 @@ export const Provider: ParentComponent = (props) => {
|
|||||||
return "waitlisted"
|
return "waitlisted"
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: handle paid status
|
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return "new_here"
|
return "new_here"
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
async setupNodeManager(): Promise<void> {
|
async setupNodeManager(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
@@ -69,7 +68,7 @@ export const Provider: ParentComponent = (props) => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Only node manager when status is approved
|
// Only load node manager when status is approved
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (state.user_status === "approved" && !state.node_manager) {
|
if (state.user_status === "approved" && !state.node_manager) {
|
||||||
console.log("running setup node manager...")
|
console.log("running setup node manager...")
|
||||||
|
|||||||
@@ -2,10 +2,17 @@
|
|||||||
// bitcoin:tb1pdh43en28jmhnsrhxkusja46aufdlae5qnfrhucw5jvefw9flce3sdxfcwe?amount=0.00001&label=heyo&lightning=lntbs10u1pjrwrdedq8dpjhjmcnp4qd60w268ve0jencwzhz048ruprkxefhj0va2uspgj4q42azdg89uupp5gngy2pqte5q5uvnwcxwl2t8fsdlla5s6xl8aar4xcsvxeus2w2pqsp5n5jp3pz3vpu92p3uswttxmw79a5lc566herwh3f2amwz2sp6f9tq9qyysgqcqpcxqrpwugv5m534ww5ukcf6sdw2m75f2ntjfh3gzeqay649256yvtecgnhjyugf74zakaf56sdh66ec9fqep2kvu6xv09gcwkv36rrkm38ylqsgpw3yfjl
|
// bitcoin:tb1pdh43en28jmhnsrhxkusja46aufdlae5qnfrhucw5jvefw9flce3sdxfcwe?amount=0.00001&label=heyo&lightning=lntbs10u1pjrwrdedq8dpjhjmcnp4qd60w268ve0jencwzhz048ruprkxefhj0va2uspgj4q42azdg89uupp5gngy2pqte5q5uvnwcxwl2t8fsdlla5s6xl8aar4xcsvxeus2w2pqsp5n5jp3pz3vpu92p3uswttxmw79a5lc566herwh3f2amwz2sp6f9tq9qyysgqcqpcxqrpwugv5m534ww5ukcf6sdw2m75f2ntjfh3gzeqay649256yvtecgnhjyugf74zakaf56sdh66ec9fqep2kvu6xv09gcwkv36rrkm38ylqsgpw3yfjl
|
||||||
// and return an object with this shape: { address: string, amount: number, label: string, lightning: string }
|
// and return an object with this shape: { address: string, amount: number, label: string, lightning: string }
|
||||||
// using typescript type annotations
|
// using typescript type annotations
|
||||||
export function bip21decode(bip21: string): { address: string, amount?: number, label?: string, lightning?: string } {
|
export function bip21decode(bip21: string): { address?: string, amount?: number, label?: string, lightning?: string } {
|
||||||
const [scheme, data] = bip21.split(':')
|
const [scheme, data] = bip21.split(':')
|
||||||
if (scheme !== 'bitcoin') {
|
if (scheme !== 'bitcoin') {
|
||||||
throw new Error('Not a bitcoin URI')
|
// TODO: this is a WAILA job I just want to debug more of the send flow
|
||||||
|
if (bip21.startsWith('lnt')) {
|
||||||
|
return { lightning: bip21 }
|
||||||
|
} else if (bip21.startsWith('tb1')) {
|
||||||
|
return { address: bip21 }
|
||||||
|
} else {
|
||||||
|
throw new Error('Not a bitcoin URI')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const [address, query] = data.split('?')
|
const [address, query] = data.split('?')
|
||||||
const params = new URLSearchParams(query)
|
const params = new URLSearchParams(query)
|
||||||
|
|||||||
Reference in New Issue
Block a user