feat: new wallet design
This commit is contained in:
@ -71,7 +71,7 @@ export function SnortDeckLayout() {
|
||||
id="IOu4Xh"
|
||||
values={{
|
||||
app: CONFIG.appNameCapitalized,
|
||||
tier: mapPlanName(CONFIG.deckSubKind),
|
||||
tier: mapPlanName(CONFIG.deckSubKind ?? -1),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@ -122,7 +122,11 @@ export function SnortDeckLayout() {
|
||||
)}
|
||||
{deckState.article && (
|
||||
<>
|
||||
<Modal onClose={() => setDeckState({})} className="long-form" onClick={() => setDeckState({})}>
|
||||
<Modal
|
||||
id="deck-article"
|
||||
onClose={() => setDeckState({})}
|
||||
className="long-form"
|
||||
onClick={() => setDeckState({})}>
|
||||
<div onClick={e => e.stopPropagation()}>
|
||||
<LongFormText ev={deckState.article} isPreview={false} related={[]} />
|
||||
</div>
|
||||
@ -140,11 +144,11 @@ function NotesCol() {
|
||||
return (
|
||||
<div>
|
||||
<div className="deck-col-header flex">
|
||||
<div className="flex f-1 g8">
|
||||
<div className="flex flex-1 g8">
|
||||
<Icon name="rows-01" size={24} />
|
||||
<FormattedMessage defaultMessage="Notes" id="7+Domh" />
|
||||
</div>
|
||||
<div className="f-1">
|
||||
<div className="flex-1">
|
||||
<RootTabs base="/deck" />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,18 +1,21 @@
|
||||
import { LogoHeader } from "./LogoHeader";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import Icon from "@/Icons/Icon";
|
||||
import { ProfileLink } from "../../Element/User/ProfileLink";
|
||||
import Avatar from "../../Element/User/Avatar";
|
||||
import useLogin from "../../Hooks/useLogin";
|
||||
import { useUserProfile } from "@snort/system-react";
|
||||
import { NoteCreatorButton } from "../../Element/Event/Create/NoteCreatorButton";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";
|
||||
import classNames from "classnames";
|
||||
import { getCurrentSubscription } from "@/Subscription";
|
||||
import { HasNotificationsMarker } from "@/Pages/Layout/HasNotificationsMarker";
|
||||
import NavLink from "@/Element/Button/NavLink";
|
||||
import { subscribeToNotifications } from "@/Notifications";
|
||||
import useEventPublisher from "@/Hooks/useEventPublisher";
|
||||
import { Sats, useWallet } from "@/Wallet";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRates } from "@/Hooks/useRates";
|
||||
|
||||
const MENU_ITEMS = [
|
||||
{
|
||||
@ -72,6 +75,44 @@ const getNavLinkClass = (isActive: boolean, narrow: boolean) => {
|
||||
});
|
||||
};
|
||||
|
||||
const WalletBalance = () => {
|
||||
const [balance, setBalance] = useState<Sats>();
|
||||
const wallet = useWallet();
|
||||
const rates = useRates("BTCUSD");
|
||||
|
||||
useEffect(() => {
|
||||
setBalance(undefined);
|
||||
if (wallet.wallet && wallet.wallet.canGetBalance()) {
|
||||
wallet.wallet.getBalance().then(setBalance);
|
||||
}
|
||||
}, [wallet]);
|
||||
|
||||
return (
|
||||
<div className="w-max flex flex-col max-xl:hidden">
|
||||
<div className="grow flex items-center justify-between">
|
||||
<div className="flex gap-1 items-center">
|
||||
<Icon name="sats" size={24} />
|
||||
<FormattedNumber value={balance ?? 0} />
|
||||
</div>
|
||||
<Link to="/wallet">
|
||||
<Icon name="dots" className="text-secondary" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="text-secondary text-sm">
|
||||
<FormattedMessage
|
||||
defaultMessage="~{amount}"
|
||||
id="3QwfJR"
|
||||
values={{
|
||||
amount: (
|
||||
<FormattedNumber style="currency" currency="USD" value={(rates?.ask ?? 0) * (balance ?? 0) * 1e-8} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function NavSidebar({ narrow = false }) {
|
||||
const { publicKey, subscriptions, readonly } = useLogin(s => ({
|
||||
publicKey: s.publicKey,
|
||||
@ -106,6 +147,7 @@ export default function NavSidebar({ narrow = false }) {
|
||||
{ "xl:items-start": !narrow, "xl:gap-2": !narrow },
|
||||
"gap-1 flex flex-col items-center text-lg font-bold",
|
||||
)}>
|
||||
<WalletBalance narrow={narrow} />
|
||||
{MENU_ITEMS.filter(a => {
|
||||
if ((CONFIG.hideFromNavbar ?? []).includes(a.link)) {
|
||||
return false;
|
||||
@ -122,7 +164,7 @@ export default function NavSidebar({ narrow = false }) {
|
||||
return "";
|
||||
}
|
||||
const onClick = () => {
|
||||
if (item.label === "Notifications") {
|
||||
if (item.label === "Notifications" && publisher) {
|
||||
subscribeToNotifications(publisher);
|
||||
}
|
||||
};
|
||||
|
@ -5,31 +5,35 @@ import { useNavigate } from "react-router-dom";
|
||||
import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";
|
||||
|
||||
import NoteTime from "@/Element/Event/NoteTime";
|
||||
import { WalletInvoice, Sats, WalletInfo, WalletInvoiceState, useWallet, LNWallet, Wallets } from "@/Wallet";
|
||||
import { WalletInvoice, Sats, useWallet, LNWallet, Wallets } from "@/Wallet";
|
||||
import AsyncButton from "@/Element/Button/AsyncButton";
|
||||
import { unwrap } from "@/SnortUtils";
|
||||
import Icon from "@/Icons/Icon";
|
||||
import { useRates } from "@/Hooks/useRates";
|
||||
import { AsyncIcon } from "@/Element/Button/AsyncIcon";
|
||||
import classNames from "classnames";
|
||||
|
||||
export default function WalletPage() {
|
||||
export default function WalletPage(props: { showHistory: boolean }) {
|
||||
const navigate = useNavigate();
|
||||
const { formatMessage } = useIntl();
|
||||
const [info, setInfo] = useState<WalletInfo>();
|
||||
const [balance, setBalance] = useState<Sats>();
|
||||
const [history, setHistory] = useState<WalletInvoice[]>();
|
||||
const [walletPassword, setWalletPassword] = useState<string>();
|
||||
const [error, setError] = useState<string>();
|
||||
const walletState = useWallet();
|
||||
const wallet = walletState.wallet;
|
||||
const rates = useRates("BTCUSD");
|
||||
|
||||
async function loadWallet(wallet: LNWallet) {
|
||||
try {
|
||||
const i = await wallet.getInfo();
|
||||
setInfo(i);
|
||||
setError(undefined);
|
||||
setBalance(0);
|
||||
setHistory(undefined);
|
||||
if (wallet.canGetBalance()) {
|
||||
const b = await wallet.getBalance();
|
||||
setBalance(b as Sats);
|
||||
}
|
||||
if (wallet.canGetInvoices()) {
|
||||
if (wallet.canGetInvoices() && (props.showHistory ?? true)) {
|
||||
const h = await wallet.getInvoices();
|
||||
setHistory((h as WalletInvoice[]).sort((a, b) => b.timestamp - a.timestamp));
|
||||
}
|
||||
@ -43,29 +47,11 @@ export default function WalletPage() {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (wallet) {
|
||||
if (wallet.isReady()) {
|
||||
loadWallet(wallet).catch(console.warn);
|
||||
} else if (wallet.canAutoLogin()) {
|
||||
wallet
|
||||
.login()
|
||||
.then(async () => await loadWallet(wallet))
|
||||
.catch(console.warn);
|
||||
}
|
||||
if (wallet && wallet.isReady()) {
|
||||
loadWallet(wallet).catch(console.warn);
|
||||
}
|
||||
}, [wallet]);
|
||||
|
||||
function stateIcon(s: WalletInvoiceState) {
|
||||
switch (s) {
|
||||
case WalletInvoiceState.Pending:
|
||||
return <Icon name="clock" size={15} />;
|
||||
case WalletInvoiceState.Paid:
|
||||
return <Icon name="check" size={15} />;
|
||||
case WalletInvoiceState.Expired:
|
||||
return <Icon name="close" size={15} />;
|
||||
}
|
||||
}
|
||||
|
||||
async function loginWallet(pw: string) {
|
||||
if (wallet) {
|
||||
await wallet.login(pw);
|
||||
@ -112,11 +98,11 @@ export default function WalletPage() {
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="flex w-max">
|
||||
<h4 className="f-1">
|
||||
<div className="flex items-center">
|
||||
<h4 className="grow">
|
||||
<FormattedMessage defaultMessage="Select Wallet" id="G1BGCg" />
|
||||
</h4>
|
||||
<div className="f-1">
|
||||
<div>
|
||||
<select className="w-max" onChange={e => Wallets.switch(e.target.value)} value={walletState.config?.id}>
|
||||
{Wallets.list().map(a => {
|
||||
return <option value={a.id}>{a.info.alias}</option>;
|
||||
@ -128,79 +114,108 @@ export default function WalletPage() {
|
||||
}
|
||||
|
||||
function walletHistory() {
|
||||
if (!wallet?.canGetInvoices()) return;
|
||||
if (!wallet?.canGetInvoices() || !(props.showHistory ?? true)) return;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3>
|
||||
<FormattedMessage defaultMessage="History" id="d6CyG5" description="Wallet transation history" />
|
||||
<FormattedMessage defaultMessage="Payments" id="pukxg/" description="Wallet transation history" />
|
||||
</h3>
|
||||
{history?.map(a => (
|
||||
<div className="card flex wallet-history-item" key={a.timestamp}>
|
||||
<div className="grow">
|
||||
<NoteTime from={a.timestamp * 1000} fallback={formatMessage({ defaultMessage: "now", id: "kaaf1E" })} />
|
||||
<div>{(a.memo ?? "").length === 0 ? <> </> : a.memo}</div>
|
||||
</div>
|
||||
<div
|
||||
className={`flex gap-2 items-center ${(() => {
|
||||
switch (a.state) {
|
||||
case WalletInvoiceState.Paid:
|
||||
return "success";
|
||||
case WalletInvoiceState.Expired:
|
||||
return "expired";
|
||||
case WalletInvoiceState.Failed:
|
||||
return "failed";
|
||||
default:
|
||||
return "pending";
|
||||
}
|
||||
})()}`}>
|
||||
<div>{stateIcon(a.state)}</div>
|
||||
{history?.map(a => {
|
||||
const dirClassname = {
|
||||
"text-[--success]": a.direction === "in",
|
||||
"text-[--error]": a.direction === "out",
|
||||
};
|
||||
return (
|
||||
<div className="flex gap-4 p-2 hover:bg-[--gray-superdark] rounded-xl items-center" key={a.timestamp}>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
defaultMessage="{amount} sats"
|
||||
id="vrTOHJ"
|
||||
values={{
|
||||
amount: <FormattedNumber value={a.amount / 1e3} />,
|
||||
}}
|
||||
/>
|
||||
<div className="rounded-full aspect-square p-2 bg-[--gray-dark]">
|
||||
<Icon
|
||||
name="arrow-up-right"
|
||||
className={classNames(dirClassname, {
|
||||
"rotate-180": a.direction === "in",
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grow flex justify-between">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div>{a.memo?.length === 0 ? CONFIG.appNameCapitalized : a.memo}</div>
|
||||
<div className="text-secondary text-sm">
|
||||
<NoteTime
|
||||
from={a.timestamp * 1000}
|
||||
fallback={formatMessage({ defaultMessage: "now", id: "kaaf1E" })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 text-right">
|
||||
<div className={classNames(dirClassname)}>
|
||||
<FormattedMessage
|
||||
defaultMessage="{sign} {amount} sats"
|
||||
id="tj6kdX"
|
||||
values={{
|
||||
sign: a.direction === "in" ? "+" : "-",
|
||||
amount: <FormattedNumber value={a.amount / 1e3} />,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-secondary text-sm">
|
||||
<FormattedMessage
|
||||
defaultMessage="~{amount}"
|
||||
id="3QwfJR"
|
||||
values={{
|
||||
amount: (
|
||||
<FormattedNumber
|
||||
style="currency"
|
||||
currency="USD"
|
||||
value={(rates?.ask ?? 0) * a.amount * 1e-11}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function walletBalance() {
|
||||
if (!wallet?.canGetBalance()) return;
|
||||
return (
|
||||
<small>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormattedMessage
|
||||
defaultMessage="Balance: {amount} sats"
|
||||
id="VN0+Fz"
|
||||
defaultMessage="<big>{amount}</big> <small>sats</small>"
|
||||
id="E5ZIPD"
|
||||
values={{
|
||||
big: c => <span className="text-3xl font-bold">{c}</span>,
|
||||
small: c => <span className="text-secondary">{c}</span>,
|
||||
amount: <FormattedNumber value={balance ?? 0} />,
|
||||
}}
|
||||
/>
|
||||
</small>
|
||||
<AsyncIcon size={20} className="text-secondary cursor-pointer" iconName="closedeye" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function walletInfo() {
|
||||
if (!wallet?.isReady()) return;
|
||||
return (
|
||||
<>
|
||||
<div className="p br b flex justify-between">
|
||||
<div>
|
||||
<div>{info?.alias}</div>
|
||||
{walletBalance()}
|
||||
</div>
|
||||
<div>
|
||||
{walletState.config?.id && (
|
||||
<AsyncButton onClick={() => Wallets.remove(unwrap(walletState.config?.id))}>
|
||||
<FormattedMessage defaultMessage="Remove" id="G/yZLu" />
|
||||
</AsyncButton>
|
||||
)}
|
||||
<div className="flex flex-col items-center px-6 py-4 bg-[--gray-superdark] rounded-2xl gap-1">
|
||||
{walletBalance()}
|
||||
<div className="text-secondary">
|
||||
<FormattedMessage
|
||||
defaultMessage="~{amount}"
|
||||
id="3QwfJR"
|
||||
values={{
|
||||
amount: (
|
||||
<FormattedNumber style="currency" currency="USD" value={(rates?.ask ?? 0) * (balance ?? 0) * 1e-8} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{walletHistory()}
|
||||
@ -209,7 +224,7 @@ export default function WalletPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="main-content p">
|
||||
<div className="main-content">
|
||||
{walletList()}
|
||||
{error && <b className="warning">{error}</b>}
|
||||
{unlockWallet()}
|
||||
|
@ -22,7 +22,7 @@ export default function AccountsPage() {
|
||||
about: false,
|
||||
}}
|
||||
actions={
|
||||
<div className="f-1">
|
||||
<div className="flex-1">
|
||||
<button className="mb10" onClick={() => LoginStore.switchAccount(a.id)}>
|
||||
<FormattedMessage defaultMessage="Switch" id="n1Whvj" />
|
||||
</button>
|
||||
|
@ -16,7 +16,7 @@ const WalletSettings = () => {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<>
|
||||
<WalletPage />
|
||||
<WalletPage showHistory={false} />
|
||||
<h3>
|
||||
<FormattedMessage defaultMessage="Connect Wallet" id="cg1VJ2" />
|
||||
</h3>
|
||||
|
@ -47,7 +47,7 @@ export default function SubscriptionCard({ sub }: { sub: Subscription }) {
|
||||
{mapPlanName(sub.type)}
|
||||
</div>
|
||||
<div className="flex">
|
||||
<p className="f-1">
|
||||
<p className="flex-1">
|
||||
<FormattedMessage defaultMessage="Created" id="ORGv1Q" />
|
||||
:
|
||||
<time dateTime={created.toISOString()}>
|
||||
@ -55,7 +55,7 @@ export default function SubscriptionCard({ sub }: { sub: Subscription }) {
|
||||
</time>
|
||||
</p>
|
||||
{daysToExpire >= 1 && (
|
||||
<p className="f-1">
|
||||
<p className="flex-1">
|
||||
<FormattedMessage defaultMessage="Expires" id="xhQMeQ" />
|
||||
:
|
||||
<time dateTime={expires.toISOString()}>
|
||||
@ -70,7 +70,7 @@ export default function SubscriptionCard({ sub }: { sub: Subscription }) {
|
||||
</p>
|
||||
)}
|
||||
{daysToExpire >= 0 && daysToExpire < 1 && (
|
||||
<p className="f-1">
|
||||
<p className="flex-1">
|
||||
<FormattedMessage defaultMessage="Expires" id="xhQMeQ" />
|
||||
:
|
||||
<time dateTime={expires.toISOString()}>
|
||||
@ -85,12 +85,12 @@ export default function SubscriptionCard({ sub }: { sub: Subscription }) {
|
||||
</p>
|
||||
)}
|
||||
{isExpired && (
|
||||
<p className="f-1 error">
|
||||
<p className="flex-1 error">
|
||||
<FormattedMessage defaultMessage="Expired" id="RahCRH" />
|
||||
</p>
|
||||
)}
|
||||
{isNew && (
|
||||
<p className="f-1">
|
||||
<p className="flex-1">
|
||||
<FormattedMessage defaultMessage="Unpaid" id="6uMqL1" />
|
||||
</p>
|
||||
)}
|
||||
|
Reference in New Issue
Block a user