qr redesign

This commit is contained in:
Paul Miller
2023-06-26 17:40:39 -05:00
parent 534a1ba00f
commit cb9e6deeca
11 changed files with 257 additions and 47 deletions

View 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="#000"/>
</svg>

After

Width:  |  Height:  |  Size: 478 B

View 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="#000"/>
</svg>

After

Width:  |  Height:  |  Size: 433 B

View File

@@ -0,0 +1,5 @@
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icons">
<path id="Vector" d="M12.4105 1.91098C12.5668 1.75475 12.7787 1.66699 12.9997 1.66699C13.2206 1.66699 13.4326 1.75475 13.5888 1.91098L16.9222 5.24431C17.0784 5.40059 17.1662 5.61251 17.1662 5.83348C17.1662 6.05445 17.0784 6.26637 16.9222 6.42265L13.5888 9.75598C13.4317 9.90778 13.2212 9.99177 13.0027 9.98987C12.7842 9.98798 12.5752 9.90034 12.4207 9.74583C12.2662 9.59132 12.1785 9.38231 12.1766 9.16381C12.1747 8.94532 12.2587 8.73482 12.4105 8.57765L14.3213 6.66681H4.66634C4.44533 6.66681 4.23337 6.57902 4.07709 6.42274C3.92081 6.26646 3.83301 6.05449 3.83301 5.83348C3.83301 5.61247 3.92081 5.40051 4.07709 5.24423C4.23337 5.08794 4.44533 5.00015 4.66634 5.00015H14.3213L12.4105 3.08931C12.2543 2.93304 12.1665 2.72112 12.1665 2.50015C12.1665 2.27918 12.2543 2.06725 12.4105 1.91098ZM8.58884 10.2443C8.74507 10.4006 8.83283 10.6125 8.83283 10.8335C8.83283 11.0545 8.74507 11.2664 8.58884 11.4226L6.67801 13.3335H16.333C16.554 13.3335 16.766 13.4213 16.9223 13.5776C17.0785 13.7338 17.1663 13.9458 17.1663 14.1668C17.1663 14.3878 17.0785 14.5998 16.9223 14.7561C16.766 14.9123 16.554 15.0001 16.333 15.0001H6.67801L8.58884 16.911C8.74064 17.0681 8.82463 17.2786 8.82274 17.4971C8.82084 17.7156 8.7332 17.9247 8.57869 18.0792C8.42418 18.2337 8.21517 18.3213 7.99668 18.3232C7.77818 18.3251 7.56768 18.2411 7.41051 18.0893L4.07717 14.756C3.92095 14.5997 3.83319 14.3878 3.83319 14.1668C3.83319 13.9458 3.92095 13.7339 4.07717 13.5776L7.41051 10.2443C7.56678 10.0881 7.7787 10.0003 7.99967 10.0003C8.22064 10.0003 8.43257 10.0881 8.58884 10.2443Z" fill="#A3A3A3"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -17,6 +17,7 @@ export function Amount(props: {
loading?: boolean;
centered?: boolean;
icon?: "lightning" | "chain";
whiteBg?: boolean;
}) {
const [state, _] = useMegaStore();
@@ -26,7 +27,9 @@ export function Amount(props: {
return (
<div
class="flex flex-col gap-1"
classList={{ "items-center": props.centered }}
classList={{
"items-center": props.centered
}}
>
<div class="flex gap-2 items-center">
<Show when={props.icon === "lightning"}>
@@ -35,7 +38,12 @@ export function Amount(props: {
<Show when={props.icon === "chain"}>
<img src={chain} alt="chain" class="h-[18px]" />
</Show>
<h1 class="text-2xl font-light">
<h1
class="text-2xl font-light"
classList={{
"text-black": props.whiteBg
}}
>
{props.loading
? "..."
: prettyPrintAmount(props.amountSats)}
@@ -44,7 +52,13 @@ export function Amount(props: {
</h1>
</div>
<Show when={props.showFiat}>
<h2 class="text-sm font-light text-white/70">
<h2
class="text-sm font-light"
classList={{
"text-black": props.whiteBg,
"text-white/70": !props.whiteBg
}}
>
&#8776; {props.loading ? "..." : amountInUsd()}&nbsp;
<span class="text-sm">USD</span>
</h2>

View File

@@ -0,0 +1,111 @@
import { Match, Show, Switch } from "solid-js";
import { QRCodeSVG } from "solid-qr-code";
import { ReceiveFlavor } from "~/routes/Receive";
import { useCopy } from "~/utils/useCopy";
import { Amount } from "./Amount";
import { TruncateMiddle } from "./ShareCard";
import copyBlack from "~/assets/icons/copy-black.svg";
import shareBlack from "~/assets/icons/share-black.svg";
import chainBlack from "~/assets/icons/chain-black.svg";
import boltBlack from "~/assets/icons/bolt-black.svg";
function KindIndicator(props: { kind: ReceiveFlavor }) {
return (
<div class="text-black flex flex-col items-end">
<Switch>
<Match when={props.kind === "onchain"}>
<h3 class="font-semibold">On-chain</h3>
<img src={chainBlack} alt="chain" />
</Match>
<Match when={props.kind === "lightning"}>
<h3 class="font-semibold">Lightning</h3>
<img src={boltBlack} alt="bolt" />
</Match>
<Match when={props.kind === "unified"}>
<h3 class="font-semibold">Unified</h3>
<div class="flex gap-1">
<img src={chainBlack} alt="chain" />
<img src={boltBlack} alt="bolt" />
</div>
</Match>
</Switch>
</div>
);
}
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);
}
}
export function IntegratedQr(props: {
value: string;
amountSats: string;
kind: ReceiveFlavor;
}) {
const [copy, copied] = useCopy({ copiedTimeout: 1000 });
return (
<div
id="qr"
class="w-full bg-white rounded-xl relative flex flex-col items-center px-4"
onClick={() => copy(props.value)}
>
<Show when={copied()}>
<div class="absolute w-full h-full bg-neutral-900/60 z-50 rounded-xl flex flex-col items-center justify-center transition-all">
<p class="text-xl font-bold">Copied</p>
</div>
</Show>
<div class="w-full flex justify-between items-center py-4 max-w-[256px]">
<Amount
amountSats={Number(props.amountSats)}
showFiat
whiteBg
/>
<KindIndicator kind={props.kind} />
</div>
<QRCodeSVG
value={props.value}
class="w-full h-full max-h-[256px]"
/>
<div
class="w-full grid gap-1 py-4 max-w-[256px] "
classList={{
"grid-cols-[2rem_minmax(0,1fr)_2rem]": !!navigator.share,
"grid-cols-[minmax(0,1fr)_2rem]": !navigator.share
}}
>
<Show when={!!navigator.share}>
<button
class="justify-self-start"
onClick={(_) => share(props.value)}
>
<img src={shareBlack} alt="share" />
</button>
</Show>
<div class="">
<TruncateMiddle text={props.value} whiteBg />
</div>
<button
class=" justify-self-end"
onClick={() => copy(props.value)}
>
<img src={copyBlack} alt="copy" />
</button>
</div>
</div>
);
}

View File

@@ -1,7 +1,9 @@
import { Card, VStack } from "~/components/layout";
import { useCopy } from "~/utils/useCopy";
import copyIcon from "~/assets/icons/copy.svg";
import copyBlack from "~/assets/icons/copy-black.svg";
import shareIcon from "~/assets/icons/share.svg";
import shareBlack from "~/assets/icons/share-black.svg";
import eyeIcon from "~/assets/icons/eye.svg";
import { Show, createSignal } from "solid-js";
import { JsonModal } from "./JsonModal";
@@ -9,7 +11,10 @@ import { JsonModal } from "./JsonModal";
const STYLE =
"px-4 py-2 rounded-xl border-2 border-white flex gap-2 items-center font-semibold hover:text-m-blue transition-colors";
export function ShareButton(props: { receiveString: string }) {
export function ShareButton(props: {
receiveString: string;
whiteBg?: boolean;
}) {
async function share(receiveString: string) {
// If the browser doesn't support share we can just copy the address
if (!navigator.share) {
@@ -29,14 +34,20 @@ export function ShareButton(props: { receiveString: string }) {
return (
<button class={STYLE} onClick={(_) => share(props.receiveString)}>
<span>Share</span>
<img src={shareIcon} alt="share" />
<img src={props.whiteBg ? shareBlack : shareIcon} alt="share" />
</button>
);
}
export function TruncateMiddle(props: { text: string }) {
export function TruncateMiddle(props: { text: string; whiteBg?: boolean }) {
return (
<div class="flex text-neutral-300 font-mono">
<div
class="flex font-mono"
classList={{
"text-black": props.whiteBg,
"text-neutral-300": !props.whiteBg
}}
>
<span class="truncate">{props.text}</span>
<span class="pr-2">
{props.text.length > 8 ? props.text.slice(-8) : ""}
@@ -65,7 +76,11 @@ export function StringShower(props: { text: string }) {
);
}
export function CopyButton(props: { text?: string; title?: string }) {
export function CopyButton(props: {
text?: string;
title?: string;
whiteBg?: boolean;
}) {
const [copy, copied] = useCopy({ copiedTimeout: 1000 });
function handleCopy() {
@@ -75,7 +90,7 @@ export function CopyButton(props: { text?: string; title?: string }) {
return (
<button class={STYLE} onClick={handleCopy}>
{copied() ? "Copied" : props.title ?? "Copy"}
<img src={copyIcon} alt="copy" />
<img src={props.whiteBg ? copyBlack : copyIcon} alt="copy" />
</button>
);
}

View File

@@ -15,16 +15,20 @@ export function StyledRadioGroup(props: {
onValueChange: (value: string) => void;
small?: boolean;
accent?: "red" | "white";
vertical?: boolean;
}) {
return (
// TODO: rewrite this with CVA, props are bad for tailwind
<RadioGroup.Root
value={props.value}
onChange={props.onValueChange}
class={"grid w-full gap-4"}
class={"w-full gap-4"}
classList={{
"grid-cols-2": props.choices.length === 2,
"grid-cols-3": props.choices.length === 3,
"flex flex-col": props.vertical,
"grid grid-cols-2":
props.choices.length === 2 && !props.vertical,
"grid grid-cols-3":
props.choices.length === 3 && !props.vertical,
"gap-2": props.small
}}
>

View File

@@ -8,7 +8,7 @@ import {
} from "solid-js";
import Linkify from "./Linkify";
import { Button, ButtonLink } from "./Button";
import { Checkbox as KCheckbox, Separator } from "@kobalte/core";
import { Dialog, Checkbox as KCheckbox, Separator } from "@kobalte/core";
import { useMegaStore } from "~/state/megaStore";
import check from "~/assets/icons/check.svg";
import { MutinyTagItem } from "~/utils/tags";
@@ -75,7 +75,6 @@ export const SettingsCard: ParentComponent<{
);
};
export const SafeArea: ParentComponent = (props) => {
return (
<div class="h-[100dvh] safe-left safe-right">
@@ -166,7 +165,7 @@ export const LargeHeader: ParentComponent<{ action?: JSX.Element }> = (
) => {
return (
<header class="w-full flex justify-between items-center mt-4 mb-2">
<h1 class="text-3xl font-semibold">{props.children}</h1>
<h1 class="text-2xl font-semibold">{props.children}</h1>
<Show when={props.action}>{props.action}</Show>
</header>
);
@@ -282,3 +281,38 @@ export function ModalCloseButton() {
</button>
);
}
export const SIMPLE_OVERLAY = "fixed inset-0 z-50 bg-black/70 backdrop-blur-md";
export const SIMPLE_DIALOG_POSITIONER =
"fixed inset-0 z-50 flex items-center justify-center";
export const SIMPLE_DIALOG_CONTENT =
"max-w-[500px] w-[90vw] max-h-[100dvh] overflow-y-scroll disable-scrollbars mx-4 p-4 bg-neutral-800/80 backdrop-blur-md shadow-xl rounded-xl border border-white/10";
export const SimpleDialog: ParentComponent<{
title: string;
open: boolean;
setOpen: (open: boolean) => void;
}> = (props) => {
return (
<Dialog.Root open={props.open} onOpenChange={props.setOpen}>
<Dialog.Portal>
<Dialog.Overlay class={SIMPLE_OVERLAY} />
<div class={SIMPLE_DIALOG_POSITIONER}>
<Dialog.Content class={SIMPLE_DIALOG_CONTENT}>
<div class="flex justify-between mb-2 items-center">
<Dialog.Title>
<SmallHeader>{props.title}</SmallHeader>
</Dialog.Title>
<Dialog.CloseButton>
<ModalCloseButton />
</Dialog.CloseButton>
</div>
<Dialog.Description class="flex flex-col gap-4">
{props.children}
</Dialog.Description>
</Dialog.Content>
</div>
</Dialog.Portal>
</Dialog.Root>
);
};

View File

@@ -20,7 +20,8 @@ import {
Indicator,
LargeHeader,
MutinyWalletGuard,
SafeArea
SafeArea,
SimpleDialog
} from "~/components/layout";
import NavBar from "~/components/NavBar";
import { useMegaStore } from "~/state/megaStore";
@@ -33,16 +34,16 @@ import { StyledRadioGroup } from "~/components/layout/Radio";
import { showToast } from "~/components/Toaster";
import { useNavigate } from "solid-start";
import { AmountCard } from "~/components/AmountCard";
import { ShareCard } from "~/components/ShareCard";
import { BackButton } from "~/components/layout/BackButton";
import { MutinyTagItem } from "~/utils/tags";
import { Network } from "~/logic/mutinyWalletSetup";
import { SuccessModal } from "~/components/successfail/SuccessModal";
import { MegaCheck } from "~/components/successfail/MegaCheck";
import { ExternalLink } from "~/components/layout/ExternalLink";
import { CopyableQR } from "~/components/CopyableQR";
import { InfoBox } from "~/components/InfoBox";
import { FeesModal } from "~/components/MoreInfoModal";
import { IntegratedQr } from "~/components/IntegratedQR";
import side2side from "~/assets/icons/side-to-side.svg";
type OnChainTx = {
transaction: {
@@ -69,12 +70,27 @@ type OnChainTx = {
};
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" }
{
value: "unified",
label: "Unified",
caption:
"Combines a bitcoin address and a lightning invoice. Sender chooses payment method."
},
{
value: "lightning",
label: "Lightning invoice",
caption:
"Ideal for small transactions. Usually lower fees than on-chain."
},
{
value: "onchain",
label: "Bitcoin address",
caption:
"On-chain, just like Satoshi did it. Ideal for very large transactions."
}
];
type ReceiveFlavor = "unified" | "lightning" | "onchain";
export type ReceiveFlavor = "unified" | "lightning" | "onchain";
type ReceiveState = "edit" | "show" | "paid";
type PaidState = "lightning_paid" | "onchain_paid";
@@ -292,6 +308,8 @@ export default function Receive() {
});
});
const [methodChooserOpen, setMethodChooserOpen] = createSignal(false);
return (
<MutinyWalletGuard>
<SafeArea>
@@ -347,31 +365,34 @@ export default function Receive() {
</Match>
<Match when={unified() && receiveState() === "show"}>
<FeeWarning fee={lspFee()} flavor={flavor()} />
<CopyableQR value={receiveString() ?? ""} />
<IntegratedQr
value={receiveString() ?? ""}
amountSats={amount() || "0"}
kind={flavor()}
/>
<p class="text-neutral-400 text-center">
<Switch>
<Match when={flavor() === "lightning"}>
Show or share this invoice with the
sender.
</Match>
<Match when={flavor() === "onchain"}>
Show or share this address with the
sender.
</Match>
<Match when={flavor() === "unified"}>
Show or share this code with the sender.
Sender decides method of payment.
</Match>
</Switch>
Keep Mutiny open to receive the payment.
</p>
<StyledRadioGroup
small
value={flavor()}
onValueChange={setFlavor}
choices={RECEIVE_FLAVORS}
accent="white"
/>{" "}
<ShareCard text={receiveString() ?? ""} />
<button
class="font-bold text-m-grey-400 flex gap-2 p-2 items-center mx-auto"
onClick={() => setMethodChooserOpen(true)}
>
<span>Choose format</span>
<img class="w-4 h-4" src={side2side} />
</button>
<SimpleDialog
title="Choose payment format"
open={methodChooserOpen()}
setOpen={(open) => setMethodChooserOpen(open)}
>
<StyledRadioGroup
value={flavor()}
onValueChange={setFlavor}
choices={RECEIVE_FLAVORS}
accent="white"
vertical
/>
</SimpleDialog>
</Match>
<Match
when={

View File

@@ -1,7 +1,6 @@
import {
DefaultMain,
LargeHeader,
MutinyWalletGuard,
SafeArea,
SettingsCard,
VStack

View File

@@ -30,8 +30,9 @@ module.exports = {
"m-red-dark": "hsla(343, 92%, 44%, 1)",
"sidebar-gray": "hsla(222, 15%, 7%, 1)",
"m-grey-400": "hsla(0, 0%, 64%, 1)",
"m-grey-800": "hsla(0, 0%, 12%, 1)",
"m-grey-700": "hsla(0, 0%, 25%, 1)",
"m-grey-750": "hsla(0, 0%, 17%, 1)",
"m-grey-800": "hsla(0, 0%, 12%, 1)",
"m-grey-900": "hsla(0, 0%, 9%, 1)"
},
backgroundImage: {