mirror of
https://github.com/aljazceru/mutiny-web.git
synced 2025-12-18 14:54:26 +01:00
redesigned send and receive
This commit is contained in:
@@ -4,7 +4,6 @@ module.exports = {
|
|||||||
"es2021": true
|
"es2021": true
|
||||||
},
|
},
|
||||||
"extends": [
|
"extends": [
|
||||||
"prettier",
|
|
||||||
"eslint:recommended",
|
"eslint:recommended",
|
||||||
"plugin:@typescript-eslint/recommended",
|
"plugin:@typescript-eslint/recommended",
|
||||||
"plugin:solid/typescript",
|
"plugin:solid/typescript",
|
||||||
|
|||||||
@@ -48,5 +48,13 @@
|
|||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.8"
|
"node": ">=16.8"
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": false,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"printWidth": 80,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
src/assets/icons/copy.svg
Normal file
3
src/assets/icons/copy.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M19 21H8V7h11m0-2H8c-.53043 0-1.03914.21071-1.41421.58579C6.21071 5.96086 6 6.46957 6 7v14c0 .5304.21071 1.0391.58579 1.4142C6.96086 22.7893 7.46957 23 8 23h11c.5304 0 1.0391-.2107 1.4142-.5858S21 21.5304 21 21V7c0-.53043-.2107-1.03914-.5858-1.41421C20.0391 5.21071 19.5304 5 19 5Zm-3-4H4c-.53043 0-1.03914.21071-1.41421.58579C2.21071 1.96086 2 2.46957 2 3v14h2V3h12V1Z" fill="#fff"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 478 B |
3
src/assets/icons/eye.svg
Normal file
3
src/assets/icons/eye.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5ZM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5Zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3Z" fill="#fff"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 318 B |
3
src/assets/icons/pencil.svg
Normal file
3
src/assets/icons/pencil.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M8.707 19.7069 18 10.4139l-4.414-4.41402-9.293 9.29302c-.12794.1281-.21882.2884-.263.464L3 20.9999l5.242-1.03c.176-.044.337-.135.465-.263ZM21 7.41388c.3749-.37506.5856-.88367.5856-1.414s-.2107-1.03895-.5856-1.414l-1.586-1.586c-.3751-.37494-.8837-.58557-1.414-.58557-.5303 0-1.0389.21063-1.414.58557L15 4.58588l4.414 4.414L21 7.41388Z" fill="#fff"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 442 B |
3
src/assets/icons/share.svg
Normal file
3
src/assets/icons/share.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M6 23c-.55 0-1.021-.196-1.413-.588C4.195 22.02 3.99934 21.5493 4 21V10c0-.55.196-1.021.588-1.413C4.98 8.195 5.45067 7.99933 6 8h3v2H6v11h12V10h-3V8h3c.55 0 1.021.196 1.413.588.392.392.5877.86267.587 1.412v11c0 .55-.196 1.021-.588 1.413-.392.392-.8627.5877-1.412.587H6Zm5-7V4.825l-1.6 1.6L8 5l4-4 4 4-1.4 1.425-1.6-1.6V16h-2Z" fill="#fff"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 433 B |
87
src/components/AmountCard.tsx
Normal file
87
src/components/AmountCard.tsx
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { Match, ParentComponent, Show, Switch, createMemo } from "solid-js";
|
||||||
|
import { Card, VStack } from "~/components/layout";
|
||||||
|
import { useMegaStore } from "~/state/megaStore";
|
||||||
|
import { satsToUsd } from "~/utils/conversions";
|
||||||
|
import { AmountEditable } from "./AmountEditable";
|
||||||
|
|
||||||
|
const KeyValue: ParentComponent<{ key: string, gray?: boolean }> = (props) => {
|
||||||
|
return (
|
||||||
|
<div class="flex justify-between items-center" classList={{ "text-neutral-400": props.gray }}>
|
||||||
|
<div class="font-semibold uppercase">{props.key}</div>
|
||||||
|
<div class="font-light">{props.children}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InlineAmount: ParentComponent<{ amount: string, sign?: string, fiat?: boolean }> = (props) => {
|
||||||
|
const prettyPrint = createMemo(() => {
|
||||||
|
const parsed = Number(props.amount);
|
||||||
|
if (isNaN(parsed)) {
|
||||||
|
return props.amount;
|
||||||
|
} else {
|
||||||
|
return parsed.toLocaleString();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (<div class="inline-block text-lg">{props.sign ? `${props.sign} ` : ""}{props.fiat ? "$" : ""}{prettyPrint()} <span class="text-sm">{props.fiat ? "USD" : "SATS"}</span></div>)
|
||||||
|
}
|
||||||
|
|
||||||
|
function USDShower(props: { amountSats: string, fee?: string }) {
|
||||||
|
const [state, _] = useMegaStore()
|
||||||
|
const amountInUsd = () => satsToUsd(state.price, add(props.amountSats, props.fee), true)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Show when={!(props.amountSats === "0")}>
|
||||||
|
<KeyValue gray key="">
|
||||||
|
<div class="self-end">≈ {amountInUsd()} <span class="text-sm">USD</span></div>
|
||||||
|
</KeyValue>
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function add(a: string, b?: string) {
|
||||||
|
return Number(a || 0) + Number(b || 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AmountCard(props: { amountSats: string, fee?: string, initialOpen?: boolean, isAmountEditable?: boolean, setAmountSats?: (amount: bigint) => void }) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<VStack>
|
||||||
|
<Switch>
|
||||||
|
<Match when={props.fee}>
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<KeyValue key="Amount">
|
||||||
|
<Show when={props.isAmountEditable} fallback={<InlineAmount amount={props.amountSats} />
|
||||||
|
}>
|
||||||
|
<AmountEditable initialOpen={props.initialOpen ?? false} initialAmountSats={props.amountSats.toString()} setAmountSats={props.setAmountSats ? props.setAmountSats : () => { }} />
|
||||||
|
</Show>
|
||||||
|
</KeyValue>
|
||||||
|
<KeyValue gray key="+ Fee">
|
||||||
|
<InlineAmount amount={props.fee || "0"} />
|
||||||
|
</KeyValue>
|
||||||
|
</div>
|
||||||
|
<hr class="border-white/20" />
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<KeyValue key="Total">
|
||||||
|
<InlineAmount amount={add(props.amountSats, props.fee).toString()} />
|
||||||
|
</KeyValue>
|
||||||
|
<USDShower amountSats={props.amountSats} fee={props.fee} />
|
||||||
|
</div>
|
||||||
|
</Match>
|
||||||
|
<Match when={!props.fee}>
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<KeyValue key="Amount">
|
||||||
|
<Show when={props.isAmountEditable} fallback={<InlineAmount amount={props.amountSats} />
|
||||||
|
}>
|
||||||
|
<AmountEditable initialOpen={props.initialOpen ?? false} initialAmountSats={props.amountSats.toString()} setAmountSats={props.setAmountSats ? props.setAmountSats : () => { }} />
|
||||||
|
</Show>
|
||||||
|
</KeyValue>
|
||||||
|
<USDShower amountSats={props.amountSats} />
|
||||||
|
</div>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</VStack>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import { For, Show, createMemo, createSignal } from 'solid-js';
|
import { For, ParentComponent, Show, createMemo, createSignal } from 'solid-js';
|
||||||
import { Button } from '~/components/layout';
|
import { Button } from '~/components/layout';
|
||||||
import { useMegaStore } from '~/state/megaStore';
|
import { useMegaStore } from '~/state/megaStore';
|
||||||
import { satsToUsd } from '~/utils/conversions';
|
import { satsToUsd } from '~/utils/conversions';
|
||||||
import { Amount } from './Amount';
|
|
||||||
import { Dialog } from '@kobalte/core';
|
import { Dialog } from '@kobalte/core';
|
||||||
import close from "~/assets/icons/close.svg";
|
import close from "~/assets/icons/close.svg";
|
||||||
|
import pencil from "~/assets/icons/pencil.svg";
|
||||||
|
import { InlineAmount } from './AmountCard';
|
||||||
|
|
||||||
const CHARACTERS = ["1", "2", "3", "4", "5", "6", "7", "8", "9", ".", "0", "DEL"];
|
const CHARACTERS = ["1", "2", "3", "4", "5", "6", "7", "8", "9", ".", "0", "DEL"];
|
||||||
|
|
||||||
@@ -25,8 +26,8 @@ function SingleDigitButton(props: { character: string, onClick: (c: string) => v
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AmountEditable(props: { initialAmountSats: string, setAmountSats: (s: bigint) => void }) {
|
export const AmountEditable: ParentComponent<{ initialAmountSats: string, initialOpen: boolean, setAmountSats: (s: bigint) => void }> = (props) => {
|
||||||
const [isOpen, setIsOpen] = createSignal(false);
|
const [isOpen, setIsOpen] = createSignal(props.initialOpen);
|
||||||
|
|
||||||
const [displayAmount, setDisplayAmount] = createSignal(props.initialAmountSats || "0");
|
const [displayAmount, setDisplayAmount] = createSignal(props.initialAmountSats || "0");
|
||||||
|
|
||||||
@@ -135,8 +136,13 @@ export function AmountEditable(props: { initialAmountSats: string, setAmountSats
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.Root isOpen={isOpen()}>
|
<Dialog.Root isOpen={isOpen()}>
|
||||||
<button onClick={() => setIsOpen(true)} class="p-4 rounded-xl border-2 border-m-blue">
|
<button onClick={() => setIsOpen(true)} class="px-4 py-2 rounded-xl border-2 border-m-blue flex gap-2 items-center">
|
||||||
<Amount amountSats={Number(displayAmount())} showFiat />
|
{/* <Amount amountSats={Number(displayAmount())} showFiat /><span>✏️</span> */}
|
||||||
|
<Show when={displayAmount() !== "0"} fallback={<div class="inline-block font-semibold">Set amount</div>}>
|
||||||
|
<InlineAmount amount={displayAmount()} />
|
||||||
|
</Show>
|
||||||
|
<img src={pencil} alt="Edit" />
|
||||||
|
{/* {props.children} */}
|
||||||
</button>
|
</button>
|
||||||
<Dialog.Portal>
|
<Dialog.Portal>
|
||||||
{/* <Dialog.Overlay class={OVERLAY} /> */}
|
{/* <Dialog.Overlay class={OVERLAY} /> */}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Show, Suspense } from "solid-js";
|
import { Show, Suspense } from "solid-js";
|
||||||
import { ButtonLink, FancyCard } from "~/components/layout";
|
import { ButtonLink, FancyCard, Indicator } from "~/components/layout";
|
||||||
import { useMegaStore } from "~/state/megaStore";
|
import { useMegaStore } from "~/state/megaStore";
|
||||||
import { Amount } from "./Amount";
|
import { Amount } from "./Amount";
|
||||||
|
|
||||||
@@ -10,12 +10,6 @@ function prettyPrintAmount(n?: number | bigint): string {
|
|||||||
return n.toLocaleString()
|
return n.toLocaleString()
|
||||||
}
|
}
|
||||||
|
|
||||||
function SyncingIndicator() {
|
|
||||||
return (
|
|
||||||
<div class="box-border animate-pulse px-2 py-1 -my-1 bg-white/70 rounded text-xs uppercase text-black">Syncing</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function BalanceBox() {
|
export default function BalanceBox() {
|
||||||
const [state, actions] = useMegaStore();
|
const [state, actions] = useMegaStore();
|
||||||
|
|
||||||
@@ -25,7 +19,7 @@ export default function BalanceBox() {
|
|||||||
<Amount amountSats={state.balance?.lightning || 0} showFiat />
|
<Amount amountSats={state.balance?.lightning || 0} showFiat />
|
||||||
</FancyCard>
|
</FancyCard>
|
||||||
|
|
||||||
<FancyCard title="On-Chain" tag={state.is_syncing && <SyncingIndicator />}>
|
<FancyCard title="On-Chain" tag={state.is_syncing && <Indicator>Syncing</Indicator>}>
|
||||||
<div onClick={actions.sync}>
|
<div onClick={actions.sync}>
|
||||||
<Amount amountSats={state.balance?.confirmed} showFiat />
|
<Amount amountSats={state.balance?.confirmed} showFiat />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Match, Switch, createSignal, createUniqueId } from 'solid-js';
|
import { Match, Switch, createSignal, createUniqueId } from 'solid-js';
|
||||||
import { SmallHeader } from '~/components/layout';
|
import { SmallHeader, TinyButton } from '~/components/layout';
|
||||||
import { Dialog } from '@kobalte/core';
|
import { Dialog } from '@kobalte/core';
|
||||||
import close from "~/assets/icons/close.svg";
|
import close from "~/assets/icons/close.svg";
|
||||||
import { SubmitHandler } from '@modular-forms/solid';
|
import { SubmitHandler } from '@modular-forms/solid';
|
||||||
@@ -39,7 +39,7 @@ export function ContactEditor(props: { createContact: (contact: ContactItem) =>
|
|||||||
</button>
|
</button>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={!props.list}>
|
<Match when={!props.list}>
|
||||||
<button onClick={() => setIsOpen(true)} class="border border-l-white/50 border-r-white/50 border-t-white/75 border-b-white/25 bg-black px-1 py-[0.5] rounded cursor-pointer hover:outline-white hover:outline-1">+ Add Contact</button>
|
<TinyButton onClick={() => setIsOpen(true)}>+ Add Contact</TinyButton>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
<Dialog.Portal>
|
<Dialog.Portal>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useCopy } from "~/utils/useCopy";
|
|||||||
|
|
||||||
const OVERLAY = "fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
|
const OVERLAY = "fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
|
||||||
const DIALOG_POSITIONER = "fixed inset-0 z-50 flex items-center justify-center"
|
const DIALOG_POSITIONER = "fixed inset-0 z-50 flex items-center justify-center"
|
||||||
const DIALOG_CONTENT = "max-w-[600px] max-h-full p-4 bg-gray/50 backdrop-blur-md shadow-xl rounded-xl border border-white/10"
|
const DIALOG_CONTENT = "max-w-[600px] max-h-full mx-4 p-4 bg-neutral-900/50 backdrop-blur-md shadow-xl rounded-xl border border-white/10"
|
||||||
|
|
||||||
export function JsonModal(props: { title: string, open: boolean, data?: unknown, setOpen: (open: boolean) => void, children?: JSX.Element }) {
|
export function JsonModal(props: { title: string, open: boolean, data?: unknown, setOpen: (open: boolean) => void, children?: JSX.Element }) {
|
||||||
const json = createMemo(() => JSON.stringify(props.data, null, 2));
|
const json = createMemo(() => JSON.stringify(props.data, null, 2));
|
||||||
|
|||||||
71
src/components/ShareCard.tsx
Normal file
71
src/components/ShareCard.tsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { Card, VStack } from "~/components/layout";
|
||||||
|
import { useCopy } from "~/utils/useCopy";
|
||||||
|
import copyIcon from "~/assets/icons/copy.svg"
|
||||||
|
import shareIcon from "~/assets/icons/share.svg"
|
||||||
|
import eyeIcon from "~/assets/icons/eye.svg"
|
||||||
|
import { Show, createSignal } from "solid-js";
|
||||||
|
import { JsonModal } from "./JsonModal";
|
||||||
|
|
||||||
|
const STYLE = "px-4 py-2 rounded-xl border-2 border-white flex gap-2 items-center font-semibold"
|
||||||
|
|
||||||
|
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) {
|
||||||
|
console.error("Share not supported")
|
||||||
|
}
|
||||||
|
const shareData: ShareData = {
|
||||||
|
title: "Mutiny Wallet",
|
||||||
|
text: receiveString,
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await navigator.share(shareData)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button class={STYLE} onClick={(_) => share(props.receiveString)}><span>Share</span><img src={shareIcon} alt="share" /></button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StringShower(props: { text: string }) {
|
||||||
|
const [open, setOpen] = createSignal(false);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<JsonModal open={open()} data={props.text} title="Details" setOpen={setOpen} />
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<pre class="truncate text-neutral-400">{props.text}</pre>
|
||||||
|
<button class="w-[16rem]" onClick={() => setOpen(true)}>
|
||||||
|
<img src={eyeIcon} alt="eye" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ShareCard(props: { text?: string }) {
|
||||||
|
const [copy, copied] = useCopy({ copiedTimeout: 1000 });
|
||||||
|
|
||||||
|
function handleCopy() {
|
||||||
|
copy(props.text ?? "")
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<StringShower text={props.text ?? ""} />
|
||||||
|
<VStack>
|
||||||
|
|
||||||
|
<div class="flex gap-4 justify-center">
|
||||||
|
<button class={STYLE} onClick={handleCopy}>{copied() ? "Copied" : "Copy"}<img src={copyIcon} alt="copy" /></button>
|
||||||
|
<Show when={navigator.share}>
|
||||||
|
<ShareButton receiveString={props.text ?? ""} />
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</VStack>
|
||||||
|
</Card >
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,22 +1,21 @@
|
|||||||
import { Select, createOptions } from "@thisbeyond/solid-select";
|
import { Select, createOptions } from "@thisbeyond/solid-select";
|
||||||
import "~/styles/solid-select.css"
|
import "~/styles/solid-select.css"
|
||||||
import { SmallHeader } from "./layout";
|
|
||||||
import { For, createUniqueId } from "solid-js";
|
import { For, createUniqueId } from "solid-js";
|
||||||
import { ContactEditor } from "./ContactEditor";
|
import { ContactEditor } from "./ContactEditor";
|
||||||
import { ContactItem, TagItem, TextItem, addContact } from "~/state/contacts";
|
import { ContactItem, TagItem, TextItem, addContact } from "~/state/contacts";
|
||||||
|
import { TinyButton } from "./layout";
|
||||||
|
|
||||||
// take two arrays, subtract the second from the first, then return the first
|
// take two arrays, subtract the second from the first, then return the first
|
||||||
function subtract<T>(a: T[], b: T[]) {
|
function subtract<T>(a: T[], b: T[]) {
|
||||||
const set = new Set(b);
|
const set = new Set(b);
|
||||||
return a.filter(x => !set.has(x));
|
return a.filter(x => !set.has(x));
|
||||||
};
|
}
|
||||||
|
|
||||||
const createValue = (name: string): TextItem => {
|
const createValue = (name: string): TextItem => {
|
||||||
return { id: createUniqueId(), name, kind: "text" };
|
return { id: createUniqueId(), name, kind: "text" };
|
||||||
};
|
};
|
||||||
|
|
||||||
export function TagEditor(props: { title: string, values: TagItem[], setValues: (values: TagItem[]) => void, selectedValues: TagItem[], setSelectedValues: (values: TagItem[]) => void, placeholder: string }) {
|
export function TagEditor(props: { values: TagItem[], setValues: (values: TagItem[]) => void, selectedValues: TagItem[], setSelectedValues: (values: TagItem[]) => void, placeholder: string }) {
|
||||||
console.log(props.values);
|
|
||||||
const onChange = (selected: TagItem[]) => {
|
const onChange = (selected: TagItem[]) => {
|
||||||
props.setSelectedValues(selected);
|
props.setSelectedValues(selected);
|
||||||
|
|
||||||
@@ -42,7 +41,7 @@ export function TagEditor(props: { title: string, values: TagItem[], setValues:
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col gap-2 flex-grow flex-shrink flex-1" >
|
<div class="flex flex-col gap-2 flex-grow flex-shrink flex-1" >
|
||||||
<SmallHeader>{props.title}</SmallHeader>
|
{/* FIXME this is causing overflow scroll for now good reason */}
|
||||||
<Select
|
<Select
|
||||||
multiple
|
multiple
|
||||||
initialValue={props.selectedValues}
|
initialValue={props.selectedValues}
|
||||||
@@ -53,15 +52,12 @@ export function TagEditor(props: { title: string, values: TagItem[], setValues:
|
|||||||
<div class="flex gap-2 flex-wrap">
|
<div class="flex gap-2 flex-wrap">
|
||||||
<For each={subtract(props.values, props.selectedValues).slice(0, 3)}>
|
<For each={subtract(props.values, props.selectedValues).slice(0, 3)}>
|
||||||
{(tag) => (
|
{(tag) => (
|
||||||
<div onClick={() => onChange([...props.selectedValues, tag])}
|
<TinyButton onClick={() => onChange([...props.selectedValues, tag])}
|
||||||
class="border border-l-white/50 border-r-white/50 border-t-white/75 border-b-white/25 px-1 py-[0.5] rounded cursor-pointer hover:outline-white hover:outline-1"
|
|
||||||
classList={{ "bg-black": tag.kind === "text", "bg-m-blue": tag.kind === "contact" && tag.color === "blue", "bg-m-green": tag.kind === "contact" && tag.color === "green", "bg-m-red": tag.kind === "contact" && tag.color === "red", "bg-[#898989]": tag.kind === "contact" && tag.color === "gray" }}
|
|
||||||
>
|
>
|
||||||
{tag.name}
|
{tag.name}
|
||||||
</div>
|
</TinyButton>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
{/* <button class="border border-l-white/50 border-r-white/50 border-t-white/75 border-b-white/25 bg-black px-1 py-[0.5] rounded cursor-pointer hover:outline-white hover:outline-1">+ Add Contact</button> */}
|
|
||||||
<ContactEditor createContact={newContact} />
|
<ContactEditor createContact={newContact} />
|
||||||
</div>
|
</div>
|
||||||
</div >
|
</div >
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
|
|
||||||
import { Dialog } from "@kobalte/core";
|
import { Dialog } from "@kobalte/core";
|
||||||
import { JSX } from "solid-js";
|
import { JSX } from "solid-js";
|
||||||
import { Button, LargeHeader, SmallHeader } from "~/components/layout";
|
import { Button, LargeHeader } from "~/components/layout";
|
||||||
import close from "~/assets/icons/close.svg";
|
import close from "~/assets/icons/close.svg";
|
||||||
|
|
||||||
const DIALOG_POSITIONER = "fixed inset-0 safe-top safe-bottom z-50"
|
const DIALOG_POSITIONER = "fixed inset-0 safe-top safe-bottom z-50"
|
||||||
const DIALOG_CONTENT = "h-full flex flex-col justify-between p-4 bg-gray/50 backdrop-blur-md bg-black/80"
|
const DIALOG_CONTENT = "h-full flex flex-col justify-between p-4 backdrop-blur-md bg-neutral-900/50"
|
||||||
|
|
||||||
type FullscreenModalProps = {
|
type FullscreenModalProps = {
|
||||||
title: string,
|
title: string,
|
||||||
|
|||||||
@@ -4,13 +4,19 @@ import { For, Show } from "solid-js";
|
|||||||
type Choices = { value: string, label: string, caption: string }[]
|
type Choices = { value: string, label: string, caption: string }[]
|
||||||
|
|
||||||
// TODO: how could would it be if we could just pass the estimated fees in here?
|
// 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, small?: boolean, red?: boolean }) {
|
export function StyledRadioGroup(props: { value: string, choices: Choices, onValueChange: (value: string) => void, small?: boolean, accent?: "red" | "white" }) {
|
||||||
return (
|
return (
|
||||||
// TODO: rewrite this with CVA, props are bad for tailwind
|
// TODO: rewrite this with CVA, props are bad for tailwind
|
||||||
<RadioGroup.Root value={props.value} onValueChange={(e) => props.onValueChange(e)} class={`grid w-full gap-${props.small ? "2" : "4"} grid-cols-${props.choices.length.toString()}`}>
|
<RadioGroup.Root value={props.value} onValueChange={(e) => props.onValueChange(e)}
|
||||||
|
class={"grid w-full gap-4"}
|
||||||
|
classList={{ "grid-cols-2": props.choices.length === 2, "grid-cols-3": props.choices.length === 3, "gap-2": props.small }}
|
||||||
|
>
|
||||||
<For each={props.choices}>
|
<For each={props.choices}>
|
||||||
{choice =>
|
{choice =>
|
||||||
<RadioGroup.Item value={choice.value} class={`ui-checked:bg-neutral-950 bg-white/10 rounded outline outline-black/50 ${props.red ? "ui-checked:outline-m-red" : "ui-checked:outline-m-blue"} ui-checked:outline-2`}>
|
<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`}
|
||||||
|
classList={{ "ui-checked:outline-m-red": props.accent === "red", "ui-checked:outline-white": props.accent === "white" }}
|
||||||
|
>
|
||||||
<div class={props.small ? "py-2 px-2" : "py-3 px-4"}>
|
<div class={props.small ? "py-2 px-2" : "py-3 px-4"}>
|
||||||
<RadioGroup.ItemInput />
|
<RadioGroup.ItemInput />
|
||||||
<RadioGroup.ItemControl >
|
<RadioGroup.ItemControl >
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const SmallHeader: ParentComponent<{ class?: string }> = (props) => {
|
|||||||
|
|
||||||
export const Card: ParentComponent<{ title?: string, titleElement?: JSX.Element }> = (props) => {
|
export const Card: ParentComponent<{ title?: string, titleElement?: JSX.Element }> = (props) => {
|
||||||
return (
|
return (
|
||||||
<div class='rounded-xl p-4 flex flex-col gap-2 bg-neutral-950 overflow-x-hidden w-full'>
|
<div class='rounded-xl p-4 flex flex-col gap-2 bg-neutral-950/50 overflow-x-hidden w-full'>
|
||||||
{props.title && <SmallHeader>{props.title}</SmallHeader>}
|
{props.title && <SmallHeader>{props.title}</SmallHeader>}
|
||||||
{props.titleElement && props.titleElement}
|
{props.titleElement && props.titleElement}
|
||||||
{props.children}
|
{props.children}
|
||||||
@@ -97,7 +97,7 @@ export const Hr = () => <Separator.Root class="my-4 border-white/20" />
|
|||||||
export const LargeHeader: ParentComponent<{ action?: JSX.Element }> = (props) => {
|
export const LargeHeader: ParentComponent<{ action?: JSX.Element }> = (props) => {
|
||||||
return (
|
return (
|
||||||
<header class="w-full flex justify-between items-center mt-4 mb-2">
|
<header class="w-full flex justify-between items-center mt-4 mb-2">
|
||||||
<h1 class="text-4xl font-semibold">{props.children}</h1>
|
<h1 class="text-3xl font-semibold">{props.children}</h1>
|
||||||
<Show when={props.action}>
|
<Show when={props.action}>
|
||||||
{props.action}
|
{props.action}
|
||||||
</Show>
|
</Show>
|
||||||
@@ -120,3 +120,17 @@ export const SmallAmount: ParentComponent<{ amount: number | bigint, sign?: stri
|
|||||||
export const NiceP: ParentComponent = (props) => {
|
export const NiceP: ParentComponent = (props) => {
|
||||||
return (<p class="text-2xl font-light">{props.children}</p>)
|
return (<p class="text-2xl font-light">{props.children}</p>)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const TinyButton: ParentComponent<{ onClick: () => void }> = (props) => {
|
||||||
|
return (
|
||||||
|
<button class="py-1 px-2 rounded-lg bg-white/10" onClick={props.onClick}>
|
||||||
|
{props.children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Indicator: ParentComponent = (props) => {
|
||||||
|
return (
|
||||||
|
<div class="box-border animate-pulse px-2 py-1 -my-1 bg-white/70 rounded text-xs uppercase text-black">{props.children}</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export default function Root() {
|
|||||||
content="width=device-width, initial-scale=1.0 height=device-height viewport-fit=cover user-scalable=no"
|
content="width=device-width, initial-scale=1.0 height=device-height viewport-fit=cover user-scalable=no"
|
||||||
/>
|
/>
|
||||||
<Link rel="manifest" href="/manifest.webmanifest" />
|
<Link rel="manifest" href="/manifest.webmanifest" />
|
||||||
<Meta name="theme-color" content="#000000" />
|
<Meta name="theme-color" content="rgb(23,23,23)" />
|
||||||
<Meta name="description" content="Lightning wallet for the web" />
|
<Meta name="description" content="Lightning wallet for the web" />
|
||||||
<Link rel="icon" href="/favicon.ico" />
|
<Link rel="icon" href="/favicon.ico" />
|
||||||
<Link rel="apple-touch-icon" href="/images/icon.png" sizes="512x512" />
|
<Link rel="apple-touch-icon" href="/images/icon.png" sizes="512x512" />
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Button, DefaultMain, LargeHeader, NiceP, NodeManagerGuard, SafeArea, VStack } from "~/components/layout";
|
import { Button, DefaultMain, LargeHeader, NiceP, NodeManagerGuard, SafeArea, VStack } from "~/components/layout";
|
||||||
import NavBar from "~/components/NavBar";
|
import NavBar from "~/components/NavBar";
|
||||||
import { useNavigate } from 'solid-start';
|
import { useNavigate } from 'solid-start';
|
||||||
import { BackButton } from '~/components/layout/BackButton';
|
|
||||||
import { SeedWords } from '~/components/SeedWords';
|
import { SeedWords } from '~/components/SeedWords';
|
||||||
import { useMegaStore } from '~/state/megaStore';
|
import { useMegaStore } from '~/state/megaStore';
|
||||||
import { Show, createSignal } from 'solid-js';
|
import { Show, createSignal } from 'solid-js';
|
||||||
|
import { BackLink } from "~/components/layout/BackLink";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [store, actions] = useMegaStore();
|
const [store, actions] = useMegaStore();
|
||||||
@@ -21,7 +21,7 @@ export default function App() {
|
|||||||
<NodeManagerGuard>
|
<NodeManagerGuard>
|
||||||
<SafeArea>
|
<SafeArea>
|
||||||
<DefaultMain>
|
<DefaultMain>
|
||||||
<BackButton />
|
<BackLink />
|
||||||
<LargeHeader>Backup</LargeHeader>
|
<LargeHeader>Backup</LargeHeader>
|
||||||
<VStack>
|
<VStack>
|
||||||
<NiceP>Let's get these funds secured.</NiceP>
|
<NiceP>Let's get these funds secured.</NiceP>
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { MutinyBip21RawMaterials, MutinyInvoice } from "@mutinywallet/mutiny-wasm";
|
import { MutinyBip21RawMaterials, MutinyInvoice } from "@mutinywallet/mutiny-wasm";
|
||||||
import { createEffect, createResource, createSignal, For, Match, onCleanup, onMount, Switch } from "solid-js";
|
import { createEffect, createMemo, createResource, createSignal, Match, onCleanup, onMount, Show, Switch } from "solid-js";
|
||||||
import { QRCodeSVG } from "solid-qr-code";
|
import { QRCodeSVG } from "solid-qr-code";
|
||||||
import { AmountEditable } from "~/components/AmountEditable";
|
import { Button, Card, Indicator, LargeHeader, MutinyWalletGuard, SafeArea } from "~/components/layout";
|
||||||
import { Button, Card, LargeHeader, MutinyWalletGuard, SafeArea, SmallHeader } from "~/components/layout";
|
|
||||||
import NavBar from "~/components/NavBar";
|
import NavBar from "~/components/NavBar";
|
||||||
import { useMegaStore } from "~/state/megaStore";
|
import { useMegaStore } from "~/state/megaStore";
|
||||||
import { objectToSearchParams } from "~/utils/objectToSearchParams";
|
import { objectToSearchParams } from "~/utils/objectToSearchParams";
|
||||||
import { useCopy } from "~/utils/useCopy";
|
|
||||||
import mempoolTxUrl from "~/utils/mempoolTxUrl";
|
import mempoolTxUrl from "~/utils/mempoolTxUrl";
|
||||||
import { Amount } from "~/components/Amount";
|
import { Amount } from "~/components/Amount";
|
||||||
import { FullscreenModal } from "~/components/layout/FullscreenModal";
|
import { FullscreenModal } from "~/components/layout/FullscreenModal";
|
||||||
@@ -17,6 +15,9 @@ import { showToast } from "~/components/Toaster";
|
|||||||
import { useNavigate } from "solid-start";
|
import { useNavigate } from "solid-start";
|
||||||
import megacheck from "~/assets/icons/megacheck.png";
|
import megacheck from "~/assets/icons/megacheck.png";
|
||||||
import { TagItem, listTags } from "~/state/contacts";
|
import { TagItem, listTags } from "~/state/contacts";
|
||||||
|
import { AmountCard } from "~/components/AmountCard";
|
||||||
|
import { ShareCard } from "~/components/ShareCard";
|
||||||
|
import { BackButton } from "~/components/layout/BackButton";
|
||||||
|
|
||||||
type OnChainTx = {
|
type OnChainTx = {
|
||||||
transaction: {
|
transaction: {
|
||||||
@@ -44,39 +45,9 @@ type OnChainTx = {
|
|||||||
|
|
||||||
const createUniqueId = () => Math.random().toString(36).substr(2, 9);
|
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" }]
|
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"
|
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
|
|
||||||
if (!navigator.share) {
|
|
||||||
console.error("Share not supported")
|
|
||||||
}
|
|
||||||
const shareData: ShareData = {
|
|
||||||
title: "Mutiny Wallet",
|
|
||||||
text: receiveString,
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await navigator.share(shareData)
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button onClick={(_) => share(props.receiveString)}>Share</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ReceiveState = "edit" | "show" | "paid"
|
type ReceiveState = "edit" | "show" | "paid"
|
||||||
type PaidState = "lightning_paid" | "onchain_paid";
|
type PaidState = "lightning_paid" | "onchain_paid";
|
||||||
|
|
||||||
@@ -88,17 +59,12 @@ export default function Receive() {
|
|||||||
const [receiveState, setReceiveState] = createSignal<ReceiveState>("edit")
|
const [receiveState, setReceiveState] = createSignal<ReceiveState>("edit")
|
||||||
const [bip21Raw, setBip21Raw] = createSignal<MutinyBip21RawMaterials>();
|
const [bip21Raw, setBip21Raw] = createSignal<MutinyBip21RawMaterials>();
|
||||||
const [unified, setUnified] = createSignal("")
|
const [unified, setUnified] = createSignal("")
|
||||||
|
const [shouldShowAmountEditor, setShouldShowAmountEditor] = createSignal(true)
|
||||||
|
|
||||||
// Tagging stuff
|
// Tagging stuff
|
||||||
const [selectedValues, setSelectedValues] = createSignal<TagItem[]>([]);
|
const [selectedValues, setSelectedValues] = createSignal<TagItem[]>([]);
|
||||||
const [values, setValues] = createSignal<TagItem[]>([{ id: createUniqueId(), name: "Unknown", kind: "text" }]);
|
const [values, setValues] = createSignal<TagItem[]>([{ id: createUniqueId(), name: "Unknown", kind: "text" }]);
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
listTags().then((tags) => {
|
|
||||||
setValues(prev => [...prev, ...tags || []])
|
|
||||||
});
|
|
||||||
})
|
|
||||||
|
|
||||||
// The data we get after a payment
|
// The data we get after a payment
|
||||||
const [paymentTx, setPaymentTx] = createSignal<OnChainTx>();
|
const [paymentTx, setPaymentTx] = createSignal<OnChainTx>();
|
||||||
const [paymentInvoice, setPaymentInvoice] = createSignal<MutinyInvoice>();
|
const [paymentInvoice, setPaymentInvoice] = createSignal<MutinyInvoice>();
|
||||||
@@ -106,6 +72,25 @@ export default function Receive() {
|
|||||||
// The flavor of the receive
|
// The flavor of the receive
|
||||||
const [flavor, setFlavor] = createSignal<ReceiveFlavor>("unified");
|
const [flavor, setFlavor] = createSignal<ReceiveFlavor>("unified");
|
||||||
|
|
||||||
|
const receiveString = createMemo(() => {
|
||||||
|
if (unified() && receiveState() === "show") {
|
||||||
|
if (flavor() === "unified") {
|
||||||
|
return unified();
|
||||||
|
} else if (flavor() === "lightning") {
|
||||||
|
return bip21Raw()?.invoice ?? "";
|
||||||
|
} else if (flavor() === "onchain") {
|
||||||
|
return bip21Raw()?.address ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
listTags().then((tags) => {
|
||||||
|
setValues(prev => [...prev, ...tags || []])
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
function clearAll() {
|
function clearAll() {
|
||||||
setAmount("")
|
setAmount("")
|
||||||
setReceiveState("edit")
|
setReceiveState("edit")
|
||||||
@@ -116,33 +101,6 @@ export default function Receive() {
|
|||||||
setSelectedValues([])
|
setSelectedValues([])
|
||||||
}
|
}
|
||||||
|
|
||||||
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 [copy, copied] = useCopy({ copiedTimeout: 1000 });
|
|
||||||
|
|
||||||
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) {
|
async function getUnifiedQr(amount: string) {
|
||||||
const bigAmount = BigInt(amount);
|
const bigAmount = BigInt(amount);
|
||||||
try {
|
try {
|
||||||
@@ -171,6 +129,7 @@ export default function Receive() {
|
|||||||
|
|
||||||
setUnified(unifiedQr || "")
|
setUnified(unifiedQr || "")
|
||||||
setReceiveState("show")
|
setReceiveState("show")
|
||||||
|
setShouldShowAmountEditor(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkIfPaid(bip21?: MutinyBip21RawMaterials): Promise<PaidState | undefined> {
|
async function checkIfPaid(bip21?: MutinyBip21RawMaterials): Promise<PaidState | undefined> {
|
||||||
@@ -211,63 +170,31 @@ export default function Receive() {
|
|||||||
return (
|
return (
|
||||||
<MutinyWalletGuard>
|
<MutinyWalletGuard>
|
||||||
<SafeArea>
|
<SafeArea>
|
||||||
<main class="max-w-[600px] flex flex-col gap-4 mx-auto p-4">
|
<main class="max-w-[600px] flex flex-col flex-1 gap-4 mx-auto p-4 overflow-y-auto">
|
||||||
<BackLink />
|
<Show when={receiveState() === "show"} fallback={<BackLink />}>
|
||||||
<LargeHeader>Receive Bitcoin</LargeHeader>
|
<BackButton onClick={() => setReceiveState("edit")} title="Edit" />
|
||||||
|
</Show>
|
||||||
|
<LargeHeader action={receiveState() === "show" && <Indicator>Checking</Indicator>}>Receive Bitcoin</LargeHeader>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={!unified() || receiveState() === "edit"}>
|
<Match when={!unified() || receiveState() === "edit"}>
|
||||||
<dl>
|
<div class="flex flex-col flex-1 gap-8">
|
||||||
<dd>
|
<AmountCard initialOpen={shouldShowAmountEditor()} amountSats={amount() || "0"} setAmountSats={setAmount} isAmountEditable />
|
||||||
<AmountEditable initialAmountSats={amount() || "0"} setAmountSats={setAmount} />
|
|
||||||
</dd>
|
<Card title="Tag the origin">
|
||||||
<dd>
|
<TagEditor values={values()} setValues={setValues} selectedValues={selectedValues()} setSelectedValues={setSelectedValues} placeholder="Where's it coming from?" />
|
||||||
<TagEditor title="Tag the origin" values={values()} setValues={setValues} selectedValues={selectedValues()} setSelectedValues={setSelectedValues} placeholder="Where's it coming from?" />
|
</Card>
|
||||||
</dd>
|
|
||||||
</dl>
|
<div class="flex-1" />
|
||||||
<Button class="w-full" disabled={!amount() || !selectedValues().length} intent="green" onClick={onSubmit}>Create Invoice</Button>
|
<Button class="w-full flex-grow-0 mb-4" style="" disabled={!amount() || !selectedValues().length} intent="green" onClick={onSubmit}>Create Invoice</Button>
|
||||||
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={unified() && receiveState() === "show"}>
|
<Match when={unified() && receiveState() === "show"}>
|
||||||
<StyledRadioGroup small value={flavor()} onValueChange={setFlavor} choices={RECEIVE_FLAVORS} />
|
<StyledRadioGroup small value={flavor()} onValueChange={setFlavor} choices={RECEIVE_FLAVORS} accent="white" />
|
||||||
<div class="w-full bg-white rounded-xl">
|
<div class="w-full bg-white rounded-xl">
|
||||||
<Switch>
|
<QRCodeSVG value={receiveString() ?? ""} class="w-full h-full p-8 max-h-[400px]" />
|
||||||
<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>
|
||||||
<div class="flex gap-2 w-full">
|
<p class="text-neutral-400 text-center">Show or share this code with the sender</p>
|
||||||
<Button onClick={handleCopy}>{copied() ? "Copied" : "Copy"}</Button>
|
<ShareCard text={receiveString() ?? ""} />
|
||||||
<ShareButton receiveString={unified() ?? ""} />
|
|
||||||
</div>
|
|
||||||
<Card>
|
|
||||||
<SmallHeader>Amount</SmallHeader>
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<Amount amountSats={parseInt(amount()) || 0} showFiat={true} />
|
|
||||||
<button onClick={editAmount}>✏️</button>
|
|
||||||
</div>
|
|
||||||
<SmallHeader>Tags</SmallHeader>
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<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">
|
|
||||||
<code class="break-all">{unified()}</code>
|
|
||||||
</Card>
|
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={receiveState() === "paid" && paidState() === "lightning_paid"}>
|
<Match when={receiveState() === "paid" && paidState() === "lightning_paid"}>
|
||||||
<FullscreenModal
|
<FullscreenModal
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { Match, Show, Switch, createEffect, createMemo, createSignal, onMount } from "solid-js";
|
import { Match, Show, Switch, createEffect, createMemo, createSignal, onMount } from "solid-js";
|
||||||
import { Amount } from "~/components/Amount";
|
import { Amount } from "~/components/Amount";
|
||||||
import NavBar from "~/components/NavBar";
|
import NavBar from "~/components/NavBar";
|
||||||
import { Button, ButtonLink, DefaultMain, HStack, LargeHeader, MutinyWalletGuard, SafeArea, SmallAmount, SmallHeader, VStack } from "~/components/layout";
|
import { Button, ButtonLink, Card, DefaultMain, HStack, LargeHeader, MutinyWalletGuard, SafeArea, SmallHeader, VStack } from "~/components/layout";
|
||||||
import { Paste } from "~/assets/svg/Paste";
|
import { Paste } from "~/assets/svg/Paste";
|
||||||
import { Scan } from "~/assets/svg/Scan";
|
import { Scan } from "~/assets/svg/Scan";
|
||||||
import { useMegaStore } from "~/state/megaStore";
|
import { useMegaStore } from "~/state/megaStore";
|
||||||
import { MutinyInvoice } from "@mutinywallet/mutiny-wasm";
|
import { MutinyInvoice } from "@mutinywallet/mutiny-wasm";
|
||||||
import { AmountEditable } from "~/components/AmountEditable";
|
|
||||||
import { StyledRadioGroup } from "~/components/layout/Radio";
|
import { StyledRadioGroup } from "~/components/layout/Radio";
|
||||||
import { ParsedParams, toParsedParams } from "./Scanner";
|
import { ParsedParams, toParsedParams } from "./Scanner";
|
||||||
import { showToast } from "~/components/Toaster";
|
import { showToast } from "~/components/Toaster";
|
||||||
@@ -19,35 +18,29 @@ import { BackLink } from "~/components/layout/BackLink";
|
|||||||
import { useNavigate } from "solid-start";
|
import { useNavigate } from "solid-start";
|
||||||
import { TagEditor } from "~/components/TagEditor";
|
import { TagEditor } from "~/components/TagEditor";
|
||||||
import { TagItem, createUniqueId, listTags } from "~/state/contacts";
|
import { TagItem, createUniqueId, listTags } from "~/state/contacts";
|
||||||
|
import { StringShower } from "~/components/ShareCard";
|
||||||
|
import { AmountCard } from "~/components/AmountCard";
|
||||||
|
|
||||||
type SendSource = "lightning" | "onchain";
|
type SendSource = "lightning" | "onchain";
|
||||||
|
|
||||||
const PAYMENT_METHODS = [{ value: "lightning", label: "Lightning", caption: "Fast and cool" }, { value: "onchain", label: "On-chain", caption: "Just like Satoshi did it" }]
|
// const TEST_DEST = "bitcoin:tb1pdh43en28jmhnsrhxkusja46aufdlae5qnfrhucw5jvefw9flce3sdxfcwe?amount=0.00001&label=heyo&lightning=lntbs10u1pjrwrdedq8dpjhjmcnp4qd60w268ve0jencwzhz048ruprkxefhj0va2uspgj4q42azdg89uupp5gngy2pqte5q5uvnwcxwl2t8fsdlla5s6xl8aar4xcsvxeus2w2pqsp5n5jp3pz3vpu92p3uswttxmw79a5lc566herwh3f2amwz2sp6f9tq9qyysgqcqpcxqrpwugv5m534ww5ukcf6sdw2m75f2ntjfh3gzeqay649256yvtecgnhjyugf74zakaf56sdh66ec9fqep2kvu6xv09gcwkv36rrkm38ylqsgpw3yfjl"
|
||||||
|
// const TEST_DEST_ADDRESS = "tb1pdh43en28jmhnsrhxkusja46aufdlae5qnfrhucw5jvefw9flce3sdxfcwe"
|
||||||
const TEST_DEST = "bitcoin:tb1pdh43en28jmhnsrhxkusja46aufdlae5qnfrhucw5jvefw9flce3sdxfcwe?amount=0.00001&label=heyo&lightning=lntbs10u1pjrwrdedq8dpjhjmcnp4qd60w268ve0jencwzhz048ruprkxefhj0va2uspgj4q42azdg89uupp5gngy2pqte5q5uvnwcxwl2t8fsdlla5s6xl8aar4xcsvxeus2w2pqsp5n5jp3pz3vpu92p3uswttxmw79a5lc566herwh3f2amwz2sp6f9tq9qyysgqcqpcxqrpwugv5m534ww5ukcf6sdw2m75f2ntjfh3gzeqay649256yvtecgnhjyugf74zakaf56sdh66ec9fqep2kvu6xv09gcwkv36rrkm38ylqsgpw3yfjl"
|
|
||||||
const TEST_DEST_ADDRESS = "tb1pdh43en28jmhnsrhxkusja46aufdlae5qnfrhucw5jvefw9flce3sdxfcwe"
|
|
||||||
|
|
||||||
// TODO: better success / fail type
|
// TODO: better success / fail type
|
||||||
type SentDetails = { amount?: bigint, destination?: string, txid?: string, failure_reason?: string }
|
type SentDetails = { amount?: bigint, destination?: string, txid?: string, failure_reason?: string }
|
||||||
|
|
||||||
function MethodChooser(props: { source: SendSource, setSource: (source: string) => void }) {
|
function MethodChooser(props: { source: SendSource, setSource: (source: string) => void }) {
|
||||||
const [store, actions] = useMegaStore();
|
const [store, _actions] = useMegaStore();
|
||||||
|
|
||||||
|
const methods = createMemo(() => {
|
||||||
|
return [
|
||||||
|
{ value: "lightning", label: "Lightning", caption: store.balance?.lightning ? `${store.balance?.lightning.toLocaleString()} SATS` : "No balance" },
|
||||||
|
{ value: "onchain", label: "On-chain", caption: store.balance?.confirmed ? `${store.balance?.confirmed.toLocaleString()} SATS` : "No balance" }
|
||||||
|
]
|
||||||
|
|
||||||
const amount = createMemo(() => {
|
|
||||||
if (props.source === "lightning") {
|
|
||||||
return store.balance?.lightning
|
|
||||||
} else {
|
|
||||||
return store.balance?.confirmed
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
return (
|
return (
|
||||||
<VStack>
|
<StyledRadioGroup accent="white" value={props.source} onValueChange={props.setSource} choices={methods()} />
|
||||||
<SmallHeader>
|
|
||||||
Payment Method
|
|
||||||
</SmallHeader>
|
|
||||||
<StyledRadioGroup value={props.source} onValueChange={props.setSource} choices={PAYMENT_METHODS} />
|
|
||||||
<SmallHeader class="flex items-center gap-2">Current balance <SmallAmount amount={amount() || 0n} /></SmallHeader>
|
|
||||||
</VStack>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,62 +82,18 @@ function DestinationShower(props: {
|
|||||||
clearAll: () => void,
|
clearAll: () => void,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<VStack>
|
<Switch>
|
||||||
<SmallHeader>Destination</SmallHeader>
|
<Match when={props.address && props.source === "onchain"}>
|
||||||
<div class="flex gap-2 items-center">
|
<StringShower text={props.address || ""} />
|
||||||
<Show when={props.address && props.source === "onchain"}>
|
</Match>
|
||||||
<code class="truncate text-sm break-all">{"Address: "} {props.address}
|
<Match when={props.invoice && props.source === "lightning"}>
|
||||||
<Show when={props.description}>
|
<StringShower text={props.invoice?.bolt11 || ""} />
|
||||||
<br />
|
</Match>
|
||||||
{"Description:"} {props.description}
|
<Match when={props.nodePubkey && props.source === "lightning"}>
|
||||||
</Show>
|
<StringShower text={props.nodePubkey || ""} />
|
||||||
</code>
|
</Match>
|
||||||
</Show>
|
</Switch>
|
||||||
<Show when={props.invoice && props.source === "lightning"}>
|
|
||||||
<code class="truncate text-sm break-all">{"Invoice: "} {props.invoice?.bolt11}
|
|
||||||
<Show when={props.description}>
|
|
||||||
<br />
|
|
||||||
{"Description:"} {props.description}
|
|
||||||
</Show>
|
|
||||||
</code>
|
|
||||||
</Show>
|
|
||||||
<Show when={props.nodePubkey && props.source === "lightning"}>
|
|
||||||
<code class="truncate text-sm break-all">{"Node Pubkey: "} {props.nodePubkey}
|
|
||||||
|
|
||||||
</code>
|
|
||||||
</Show>
|
|
||||||
<Button class="flex-0" intent="glowy" layout="xs" onClick={props.clearAll}>Clear</Button>
|
|
||||||
</div>
|
|
||||||
</VStack>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function AmountThing(props: { invoice?: MutinyInvoice, amountSats: bigint, fakeFee: bigint, setAmountSats: (amount: bigint) => void }) {
|
|
||||||
return (
|
|
||||||
<VStack>
|
|
||||||
<SmallHeader>Amount</SmallHeader>
|
|
||||||
|
|
||||||
<div class="flex gap-2 items-center">
|
|
||||||
{/* if the amount came with the invoice we can't allow setting it */}
|
|
||||||
<Show when={!(props.invoice?.amount_sats)} fallback={<Amount amountSats={props.amountSats} showFiat />}>
|
|
||||||
<AmountEditable initialAmountSats={props.amountSats.toString()} setAmountSats={props.setAmountSats} />
|
|
||||||
</Show>
|
|
||||||
<div class="bg-white/10 px-4 py-2 rounded-xl">
|
|
||||||
<div class="flex gap-2 items-center">
|
|
||||||
<h2 class="text-neutral-400 font-semibold uppercase">+ Fee</h2>
|
|
||||||
<h3 class="text-xl font-light text-neutral-300">
|
|
||||||
{props.fakeFee.toLocaleString()} <span class='text-lg'>SATS</span>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-2 items-center">
|
|
||||||
<h2 class="font-semibold uppercase text-white">Total</h2>
|
|
||||||
<h3 class="text-xl font-light text-white">
|
|
||||||
{(props.amountSats.valueOf() + props.fakeFee.valueOf()).toLocaleString()} <span class='text-lg'>SATS</span>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</VStack>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,7 +109,7 @@ function SendTags() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TagEditor title="Tag the receiver" values={values()} setValues={setValues} selectedValues={selectedValues()} setSelectedValues={setSelectedValues} placeholder="Where's it going to?" />
|
<TagEditor values={values()} setValues={setValues} selectedValues={selectedValues()} setSelectedValues={setSelectedValues} placeholder="Where's it going to?" />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,9 +119,8 @@ export default function Send() {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
// These can only be set by the user
|
// These can only be set by the user
|
||||||
const [fieldDestination, setFieldDestination] = createSignal(TEST_DEST);
|
const [fieldDestination, setFieldDestination] = createSignal("");
|
||||||
const [destination, setDestination] = createSignal<ParsedParams>();
|
const [destination, setDestination] = createSignal<ParsedParams>();
|
||||||
const [privateLabel, setPrivateLabel] = createSignal("");
|
|
||||||
|
|
||||||
// These can be derived from the "destination" signal or set by the user
|
// These can be derived from the "destination" signal or set by the user
|
||||||
const [amountSats, setAmountSats] = createSignal(0n);
|
const [amountSats, setAmountSats] = createSignal(0n);
|
||||||
@@ -190,7 +138,6 @@ export default function Send() {
|
|||||||
|
|
||||||
function clearAll() {
|
function clearAll() {
|
||||||
setDestination(undefined);
|
setDestination(undefined);
|
||||||
setPrivateLabel("");
|
|
||||||
setAmountSats(0n);
|
setAmountSats(0n);
|
||||||
setSource("lightning");
|
setSource("lightning");
|
||||||
setInvoice(undefined);
|
setInvoice(undefined);
|
||||||
@@ -281,7 +228,7 @@ export default function Send() {
|
|||||||
try {
|
try {
|
||||||
setSending(true);
|
setSending(true);
|
||||||
const bolt11 = invoice()?.bolt11;
|
const bolt11 = invoice()?.bolt11;
|
||||||
let sentDetails: Partial<SentDetails> = {};
|
const sentDetails: Partial<SentDetails> = {};
|
||||||
if (source() === "lightning" && invoice() && bolt11) {
|
if (source() === "lightning" && invoice() && bolt11) {
|
||||||
const nodes = await state.mutiny_wallet?.list_nodes();
|
const nodes = await state.mutiny_wallet?.list_nodes();
|
||||||
const firstNode = nodes[0] as string || ""
|
const firstNode = nodes[0] as string || ""
|
||||||
@@ -367,12 +314,16 @@ export default function Send() {
|
|||||||
<VStack biggap>
|
<VStack biggap>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={address() || invoice() || nodePubkey()}>
|
<Match when={address() || invoice() || nodePubkey()}>
|
||||||
<SendTags />
|
|
||||||
<DestinationShower source={source()} description={description()} invoice={invoice()} address={address()} nodePubkey={nodePubkey()} clearAll={clearAll} />
|
|
||||||
<Show when={address() && invoice()}>
|
<Show when={address() && invoice()}>
|
||||||
<MethodChooser source={source()} setSource={setSource} />
|
<MethodChooser source={source()} setSource={setSource} />
|
||||||
</Show>
|
</Show>
|
||||||
<AmountThing amountSats={amountSats()} invoice={invoice()} fakeFee={fakeFee()} setAmountSats={setAmountSats} />
|
<Card>
|
||||||
|
<VStack>
|
||||||
|
<DestinationShower source={source()} description={description()} invoice={invoice()} address={address()} nodePubkey={nodePubkey()} clearAll={clearAll} />
|
||||||
|
<SendTags />
|
||||||
|
</VStack>
|
||||||
|
</Card>
|
||||||
|
<AmountCard amountSats={amountSats().toString()} setAmountSats={setAmountSats} fee={fakeFee().toString()} isAmountEditable={!(invoice()?.amount_sats)} />
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
<DestinationInput fieldDestination={fieldDestination()} setFieldDestination={setFieldDestination} handleDecode={handleDecode} handlePaste={handlePaste} />
|
<DestinationInput fieldDestination={fieldDestination()} setFieldDestination={setFieldDestination} handleDecode={handleDecode} handlePaste={handlePaste} />
|
||||||
|
|||||||
23
src/routes/Storybook.tsx
Normal file
23
src/routes/Storybook.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { AmountCard } from "~/components/AmountCard";
|
||||||
|
import NavBar from "~/components/NavBar";
|
||||||
|
import { ShareCard } from "~/components/ShareCard";
|
||||||
|
import { DefaultMain, LargeHeader, SafeArea, VStack } from "~/components/layout";
|
||||||
|
|
||||||
|
const SAMPLE = "bitcoin:tb1prqm8xtlgme0vmw5s30lgf0a4f5g4mkgsqundwmpu6thrg8zr6uvq2qrhzq?amount=0.001&lightning=lntbs1m1pj9n9xjsp5xgdrmvprtm67p7nq4neparalexlhlmtxx87zx6xeqthsplu842zspp546d6zd2seyaxpapaxx62m88yz3xueqtjmn9v6wj8y56np8weqsxqdqqnp4qdn2hj8tfknpuvdg6tz9yrf3e27ltrx9y58c24jh89lnm43yjwfc5xqrpwjcqpj9qrsgq5sdgh0m3ur5mu5hrmmag4mx9yvy86f83pd0x9ww80kgck6tac3thuzkj0mrtltaxwnlfea95h2re7tj4qsnwzxlvrdmyq2h9mgapnycpppz6k6"
|
||||||
|
export default function Admin() {
|
||||||
|
return (
|
||||||
|
<SafeArea>
|
||||||
|
<DefaultMain>
|
||||||
|
<LargeHeader>Storybook</LargeHeader>
|
||||||
|
<VStack>
|
||||||
|
<AmountCard amountSats={"100000"} fee={"69"} />
|
||||||
|
<AmountCard amountSats={"100000"} />
|
||||||
|
<AmountCard amountSats={"100000"} isAmountEditable />
|
||||||
|
<AmountCard amountSats={"0"} isAmountEditable />
|
||||||
|
<ShareCard text={SAMPLE} />
|
||||||
|
</VStack>
|
||||||
|
</DefaultMain>
|
||||||
|
<NavBar activeTab="none" />
|
||||||
|
</SafeArea>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user