split SendSats
This commit is contained in:
@ -20,7 +20,16 @@ module.exports = {
|
|||||||
"simple-import-sort/imports": "error",
|
"simple-import-sort/imports": "error",
|
||||||
"simple-import-sort/exports": "error",
|
"simple-import-sort/exports": "error",
|
||||||
"@typescript-eslint/no-unused-vars": "error",
|
"@typescript-eslint/no-unused-vars": "error",
|
||||||
|
"max-lines": ["warn", { max: 300, skipBlankLines: true, skipComments: true }],
|
||||||
},
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ["*.tsx"],
|
||||||
|
rules: {
|
||||||
|
"max-lines": ["warn", { max: 200, skipBlankLines: true, skipComments: true }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
root: true,
|
root: true,
|
||||||
ignorePatterns: ["build/", "*.test.ts", "*.js"],
|
ignorePatterns: ["build/", "*.test.ts", "*.js"],
|
||||||
env: {
|
env: {
|
||||||
|
@ -3,30 +3,18 @@ import "./SendSats.css";
|
|||||||
import { LNURLSuccessAction } from "@snort/shared";
|
import { LNURLSuccessAction } from "@snort/shared";
|
||||||
import { HexKey } from "@snort/system";
|
import { HexKey } from "@snort/system";
|
||||||
import React, { ReactNode, useEffect, useState } from "react";
|
import React, { ReactNode, useEffect, useState } from "react";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
|
||||||
|
|
||||||
import AsyncButton from "@/Components/Button/AsyncButton";
|
|
||||||
import CloseButton from "@/Components/Button/CloseButton";
|
import CloseButton from "@/Components/Button/CloseButton";
|
||||||
import Copy from "@/Components/Copy/Copy";
|
|
||||||
import Icon from "@/Components/Icons/Icon";
|
|
||||||
import Modal from "@/Components/Modal/Modal";
|
import Modal from "@/Components/Modal/Modal";
|
||||||
import QrCode from "@/Components/QrCode";
|
import { SendSatsInput, SendSatsInputSelection } from "@/Components/SendSats/SendSatsInput";
|
||||||
import ProfileImage from "@/Components/User/ProfileImage";
|
import { SendSatsInvoice } from "@/Components/SendSats/SendSatsInvoice";
|
||||||
|
import { SendSatsTitle } from "@/Components/SendSats/SendSatsTitle";
|
||||||
|
import { SuccessAction } from "@/Components/SendSats/SuccessAction";
|
||||||
|
import { ZapType } from "@/Components/SendSats/ZapType";
|
||||||
import useEventPublisher from "@/Hooks/useEventPublisher";
|
import useEventPublisher from "@/Hooks/useEventPublisher";
|
||||||
import useLogin from "@/Hooks/useLogin";
|
|
||||||
import { debounce } from "@/Utils";
|
import { debounce } from "@/Utils";
|
||||||
import { formatShort } from "@/Utils/Number";
|
|
||||||
import { Zapper, ZapTarget, ZapTargetResult } from "@/Utils/Zapper";
|
import { Zapper, ZapTarget, ZapTargetResult } from "@/Utils/Zapper";
|
||||||
import { LNWallet, useWallet } from "@/Wallet";
|
import { useWallet } from "@/Wallet";
|
||||||
|
|
||||||
import messages from "../messages";
|
|
||||||
|
|
||||||
enum ZapType {
|
|
||||||
PublicZap = 1,
|
|
||||||
AnonZap = 2,
|
|
||||||
PrivateZap = 3,
|
|
||||||
NonZap = 4,
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SendSatsProps {
|
export interface SendSatsProps {
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
@ -97,91 +85,14 @@ export default function SendSats(props: SendSatsProps) {
|
|||||||
}
|
}
|
||||||
}, [props.targets, props.show]);
|
}, [props.targets, props.show]);
|
||||||
|
|
||||||
function successAction() {
|
|
||||||
if (!success) return null;
|
|
||||||
return (
|
|
||||||
<div className="flex items-center">
|
|
||||||
<p className="flex g12">
|
|
||||||
<Icon name="check" className="success" />
|
|
||||||
{success?.description ?? <FormattedMessage defaultMessage="Paid" id="u/vOPu" />}
|
|
||||||
</p>
|
|
||||||
{success.url && (
|
|
||||||
<p>
|
|
||||||
<a href={success.url} rel="noreferrer" target="_blank">
|
|
||||||
{success.url}
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function title() {
|
|
||||||
if (!props.targets) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h2>
|
|
||||||
{zapper?.canZap() ? (
|
|
||||||
<FormattedMessage defaultMessage="Send zap" id="5ykRmX" />
|
|
||||||
) : (
|
|
||||||
<FormattedMessage defaultMessage="Send sats" id="DKnriN" />
|
|
||||||
)}
|
|
||||||
</h2>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (props.targets.length === 1 && props.targets[0].name) {
|
|
||||||
const t = props.targets[0];
|
|
||||||
const values = {
|
|
||||||
name: t.name,
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{t.zap?.pubkey && <ProfileImage pubkey={t.zap.pubkey} showUsername={false} />}
|
|
||||||
<h2>
|
|
||||||
{zapper?.canZap() ? (
|
|
||||||
<FormattedMessage defaultMessage="Send zap to {name}" id="SMO+on" values={values} />
|
|
||||||
) : (
|
|
||||||
<FormattedMessage defaultMessage="Send sats to {name}" id="JGrt9q" values={values} />
|
|
||||||
)}
|
|
||||||
</h2>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (props.targets.length > 1) {
|
|
||||||
const total = props.targets.reduce((acc, v) => (acc += v.weight), 0);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col g12">
|
|
||||||
<h2>
|
|
||||||
{zapper?.canZap() ? (
|
|
||||||
<FormattedMessage defaultMessage="Send zap splits to" id="ZS+jRE" />
|
|
||||||
) : (
|
|
||||||
<FormattedMessage defaultMessage="Send sats splits to" id="uc0din" />
|
|
||||||
)}
|
|
||||||
</h2>
|
|
||||||
<div className="flex g4 f-wrap">
|
|
||||||
{props.targets.map(v => (
|
|
||||||
<ProfileImage
|
|
||||||
key={v.value}
|
|
||||||
pubkey={v.value}
|
|
||||||
showUsername={false}
|
|
||||||
showFollowDistance={false}
|
|
||||||
imageOverlay={formatShort(Math.floor((amount?.amount ?? 0) * (v.weight / total)))}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(props.show ?? false)) return null;
|
if (!(props.show ?? false)) return null;
|
||||||
return (
|
return (
|
||||||
<Modal id="send-sats" className="lnurl-modal" onClose={onClose}>
|
<Modal id="send-sats" className="lnurl-modal" onClose={onClose}>
|
||||||
<div className="p flex flex-col g12">
|
<div className="p flex flex-col g12">
|
||||||
<div className="flex g12">
|
<div className="flex g12">
|
||||||
<div className="flex items-center grow">{props.title || title()}</div>
|
<div className="flex items-center grow">
|
||||||
|
{props.title || <SendSatsTitle amount={amount} targets={props.targets} zapper={zapper} />}
|
||||||
|
</div>
|
||||||
<CloseButton onClick={onClose} />
|
<CloseButton onClick={onClose} />
|
||||||
</div>
|
</div>
|
||||||
{zapper && !invoice && (
|
{zapper && !invoice && (
|
||||||
@ -227,180 +138,8 @@ export default function SendSats(props: SendSatsProps) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{successAction()}
|
{success && <SuccessAction success={success} />}
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SendSatsInputSelection {
|
|
||||||
amount: number;
|
|
||||||
comment?: string;
|
|
||||||
type: ZapType;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SendSatsInput(props: {
|
|
||||||
zapper: Zapper;
|
|
||||||
onChange?: (v: SendSatsInputSelection) => void;
|
|
||||||
onNextStage: (v: SendSatsInputSelection) => Promise<void>;
|
|
||||||
}) {
|
|
||||||
const { defaultZapAmount, readonly } = useLogin(s => ({
|
|
||||||
defaultZapAmount: s.appData.item.preferences.defaultZapAmount,
|
|
||||||
readonly: s.readonly,
|
|
||||||
}));
|
|
||||||
const { formatMessage } = useIntl();
|
|
||||||
const amounts: Record<string, string> = {
|
|
||||||
[defaultZapAmount.toString()]: "",
|
|
||||||
"1000": "👍",
|
|
||||||
"5000": "💜",
|
|
||||||
"10000": "😍",
|
|
||||||
"20000": "🤩",
|
|
||||||
"50000": "🔥",
|
|
||||||
"100000": "🚀",
|
|
||||||
"1000000": "🤯",
|
|
||||||
};
|
|
||||||
const [comment, setComment] = useState<string>();
|
|
||||||
const [amount, setAmount] = useState<number>(defaultZapAmount);
|
|
||||||
const [customAmount, setCustomAmount] = useState<number>(defaultZapAmount);
|
|
||||||
const [zapType, setZapType] = useState(readonly ? ZapType.AnonZap : ZapType.PublicZap);
|
|
||||||
|
|
||||||
function getValue() {
|
|
||||||
return {
|
|
||||||
amount,
|
|
||||||
comment,
|
|
||||||
type: zapType,
|
|
||||||
} as SendSatsInputSelection;
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (props.onChange) {
|
|
||||||
props.onChange(getValue());
|
|
||||||
}
|
|
||||||
}, [amount, comment, zapType]);
|
|
||||||
|
|
||||||
function renderAmounts() {
|
|
||||||
const min = props.zapper.minAmount() / 1000;
|
|
||||||
const max = props.zapper.maxAmount() / 1000;
|
|
||||||
const filteredAmounts = Object.entries(amounts).filter(([k]) => Number(k) >= min && Number(k) <= max);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="amounts">
|
|
||||||
{filteredAmounts.map(([k, v]) => (
|
|
||||||
<span
|
|
||||||
className={`sat-amount ${amount === Number(k) ? "active" : ""}`}
|
|
||||||
key={k}
|
|
||||||
onClick={() => setAmount(Number(k))}>
|
|
||||||
{v}
|
|
||||||
{k === "1000" ? "1K" : formatShort(Number(k))}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function custom() {
|
|
||||||
const min = props.zapper.minAmount() / 1000;
|
|
||||||
const max = props.zapper.maxAmount() / 1000;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex g8">
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min={min}
|
|
||||||
max={max}
|
|
||||||
className="grow"
|
|
||||||
placeholder={formatMessage(messages.Custom)}
|
|
||||||
value={customAmount}
|
|
||||||
onChange={e => setCustomAmount(parseInt(e.target.value))}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
className="secondary"
|
|
||||||
type="button"
|
|
||||||
disabled={!customAmount}
|
|
||||||
onClick={() => setAmount(customAmount ?? 0)}>
|
|
||||||
<FormattedMessage {...messages.Confirm} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col g24">
|
|
||||||
<div className="flex flex-col g8">
|
|
||||||
<h3>
|
|
||||||
<FormattedMessage defaultMessage="Zap amount in sats" id="zcaOTs" />
|
|
||||||
</h3>
|
|
||||||
{renderAmounts()}
|
|
||||||
{custom()}
|
|
||||||
{props.zapper.maxComment() > 0 && (
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder={formatMessage(messages.Comment)}
|
|
||||||
className="grow"
|
|
||||||
maxLength={props.zapper.maxComment()}
|
|
||||||
onChange={e => setComment(e.target.value)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<SendSatsZapTypeSelector zapType={zapType} setZapType={setZapType} />
|
|
||||||
{(amount ?? 0) > 0 && (
|
|
||||||
<AsyncButton onClick={() => props.onNextStage(getValue())}>
|
|
||||||
<Icon name="zap" />
|
|
||||||
<FormattedMessage defaultMessage="Zap {n} sats" id="8QDesP" values={{ n: formatShort(amount) }} />
|
|
||||||
</AsyncButton>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SendSatsZapTypeSelector({ zapType, setZapType }: { zapType: ZapType; setZapType: (t: ZapType) => void }) {
|
|
||||||
const { readonly } = useLogin(s => ({ readonly: s.readonly }));
|
|
||||||
const makeTab = (t: ZapType, n: React.ReactNode) => (
|
|
||||||
<button type="button" className={zapType === t ? "" : "secondary"} onClick={() => setZapType(t)}>
|
|
||||||
{n}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col g8">
|
|
||||||
<h3>
|
|
||||||
<FormattedMessage defaultMessage="Zap Type" id="+aZY2h" />
|
|
||||||
</h3>
|
|
||||||
<div className="flex g8">
|
|
||||||
{!readonly &&
|
|
||||||
makeTab(ZapType.PublicZap, <FormattedMessage defaultMessage="Public" id="/PCavi" description="Public Zap" />)}
|
|
||||||
{/*makeTab(ZapType.PrivateZap, "Private")*/}
|
|
||||||
{makeTab(ZapType.AnonZap, <FormattedMessage defaultMessage="Anon" id="wWLwvh" description="Anonymous Zap" />)}
|
|
||||||
{makeTab(
|
|
||||||
ZapType.NonZap,
|
|
||||||
<FormattedMessage defaultMessage="Non-Zap" id="AnLrRC" description="Non-Zap, Regular LN payment" />,
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SendSatsInvoice(props: {
|
|
||||||
invoice: Array<ZapTargetResult>;
|
|
||||||
wallet?: LNWallet;
|
|
||||||
notice?: ReactNode;
|
|
||||||
onInvoicePaid: () => void;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center g12 txt-center">
|
|
||||||
{props.notice && <b className="error">{props.notice}</b>}
|
|
||||||
{props.invoice.map(v => (
|
|
||||||
<>
|
|
||||||
<QrCode data={v.pr} link={`lightning:${v.pr}`} />
|
|
||||||
<div className="flex flex-col g12">
|
|
||||||
<Copy text={v.pr} maxSize={26} className="items-center" />
|
|
||||||
<a href={`lightning:${v.pr}`}>
|
|
||||||
<button type="button">
|
|
||||||
<FormattedMessage defaultMessage="Open Wallet" id="HbefNb" />
|
|
||||||
</button>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
131
packages/app/src/Components/SendSats/SendSatsInput.tsx
Normal file
131
packages/app/src/Components/SendSats/SendSatsInput.tsx
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
|
||||||
|
import AsyncButton from "@/Components/Button/AsyncButton";
|
||||||
|
import Icon from "@/Components/Icons/Icon";
|
||||||
|
import messages from "@/Components/messages";
|
||||||
|
import { SendSatsZapTypeSelector } from "@/Components/SendSats/SendSatsZapTypeSelector";
|
||||||
|
import { ZapType } from "@/Components/SendSats/ZapType";
|
||||||
|
import useLogin from "@/Hooks/useLogin";
|
||||||
|
import { formatShort } from "@/Utils/Number";
|
||||||
|
import { Zapper } from "@/Utils/Zapper";
|
||||||
|
|
||||||
|
export interface SendSatsInputSelection {
|
||||||
|
amount: number;
|
||||||
|
comment?: string;
|
||||||
|
type: ZapType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SendSatsInput(props: {
|
||||||
|
zapper: Zapper;
|
||||||
|
onChange?: (v: SendSatsInputSelection) => void;
|
||||||
|
onNextStage: (v: SendSatsInputSelection) => Promise<void>;
|
||||||
|
}) {
|
||||||
|
const { defaultZapAmount, readonly } = useLogin(s => ({
|
||||||
|
defaultZapAmount: s.appData.item.preferences.defaultZapAmount,
|
||||||
|
readonly: s.readonly,
|
||||||
|
}));
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
const amounts: Record<string, string> = {
|
||||||
|
[defaultZapAmount.toString()]: "",
|
||||||
|
"1000": "👍",
|
||||||
|
"5000": "💜",
|
||||||
|
"10000": "😍",
|
||||||
|
"20000": "🤩",
|
||||||
|
"50000": "🔥",
|
||||||
|
"100000": "🚀",
|
||||||
|
"1000000": "🤯",
|
||||||
|
};
|
||||||
|
const [comment, setComment] = useState<string>();
|
||||||
|
const [amount, setAmount] = useState<number>(defaultZapAmount);
|
||||||
|
const [customAmount, setCustomAmount] = useState<number>(defaultZapAmount);
|
||||||
|
const [zapType, setZapType] = useState(readonly ? ZapType.AnonZap : ZapType.PublicZap);
|
||||||
|
|
||||||
|
function getValue() {
|
||||||
|
return {
|
||||||
|
amount,
|
||||||
|
comment,
|
||||||
|
type: zapType,
|
||||||
|
} as SendSatsInputSelection;
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.onChange) {
|
||||||
|
props.onChange(getValue());
|
||||||
|
}
|
||||||
|
}, [amount, comment, zapType]);
|
||||||
|
|
||||||
|
function renderAmounts() {
|
||||||
|
const min = props.zapper.minAmount() / 1000;
|
||||||
|
const max = props.zapper.maxAmount() / 1000;
|
||||||
|
const filteredAmounts = Object.entries(amounts).filter(([k]) => Number(k) >= min && Number(k) <= max);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="amounts">
|
||||||
|
{filteredAmounts.map(([k, v]) => (
|
||||||
|
<span
|
||||||
|
className={`sat-amount ${amount === Number(k) ? "active" : ""}`}
|
||||||
|
key={k}
|
||||||
|
onClick={() => setAmount(Number(k))}>
|
||||||
|
{v}
|
||||||
|
{k === "1000" ? "1K" : formatShort(Number(k))}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function custom() {
|
||||||
|
const min = props.zapper.minAmount() / 1000;
|
||||||
|
const max = props.zapper.maxAmount() / 1000;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex g8">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
className="grow"
|
||||||
|
placeholder={formatMessage(messages.Custom)}
|
||||||
|
value={customAmount}
|
||||||
|
onChange={e => setCustomAmount(parseInt(e.target.value))}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="secondary"
|
||||||
|
type="button"
|
||||||
|
disabled={!customAmount}
|
||||||
|
onClick={() => setAmount(customAmount ?? 0)}>
|
||||||
|
<FormattedMessage {...messages.Confirm} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col g24">
|
||||||
|
<div className="flex flex-col g8">
|
||||||
|
<h3>
|
||||||
|
<FormattedMessage defaultMessage="Zap amount in sats" id="zcaOTs" />
|
||||||
|
</h3>
|
||||||
|
{renderAmounts()}
|
||||||
|
{custom()}
|
||||||
|
{props.zapper.maxComment() > 0 && (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder={formatMessage(messages.Comment)}
|
||||||
|
className="grow"
|
||||||
|
maxLength={props.zapper.maxComment()}
|
||||||
|
onChange={e => setComment(e.target.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<SendSatsZapTypeSelector zapType={zapType} setZapType={setZapType} />
|
||||||
|
{(amount ?? 0) > 0 && (
|
||||||
|
<AsyncButton onClick={() => props.onNextStage(getValue())}>
|
||||||
|
<Icon name="zap" />
|
||||||
|
<FormattedMessage defaultMessage="Zap {n} sats" id="8QDesP" values={{ n: formatShort(amount) }} />
|
||||||
|
</AsyncButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
33
packages/app/src/Components/SendSats/SendSatsInvoice.tsx
Normal file
33
packages/app/src/Components/SendSats/SendSatsInvoice.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import React, { ReactNode } from "react";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
|
import Copy from "@/Components/Copy/Copy";
|
||||||
|
import QrCode from "@/Components/QrCode";
|
||||||
|
import { ZapTargetResult } from "@/Utils/Zapper";
|
||||||
|
import { LNWallet } from "@/Wallet";
|
||||||
|
|
||||||
|
export function SendSatsInvoice(props: {
|
||||||
|
invoice: Array<ZapTargetResult>;
|
||||||
|
wallet?: LNWallet;
|
||||||
|
notice?: ReactNode;
|
||||||
|
onInvoicePaid: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center g12 txt-center">
|
||||||
|
{props.notice && <b className="error">{props.notice}</b>}
|
||||||
|
{props.invoice.map(v => (
|
||||||
|
<>
|
||||||
|
<QrCode data={v.pr} link={`lightning:${v.pr}`} />
|
||||||
|
<div className="flex flex-col g12">
|
||||||
|
<Copy text={v.pr} maxSize={26} className="items-center" />
|
||||||
|
<a href={`lightning:${v.pr}`}>
|
||||||
|
<button type="button">
|
||||||
|
<FormattedMessage defaultMessage="Open Wallet" id="HbefNb" />
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
75
packages/app/src/Components/SendSats/SendSatsTitle.tsx
Normal file
75
packages/app/src/Components/SendSats/SendSatsTitle.tsx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
|
import { SendSatsInputSelection } from "@/Components/SendSats/SendSatsInput";
|
||||||
|
import ProfileImage from "@/Components/User/ProfileImage";
|
||||||
|
import { formatShort } from "@/Utils/Number";
|
||||||
|
import { Zapper, ZapTarget } from "@/Utils/Zapper";
|
||||||
|
|
||||||
|
export function SendSatsTitle({
|
||||||
|
targets,
|
||||||
|
zapper,
|
||||||
|
amount,
|
||||||
|
}: {
|
||||||
|
targets?: Array<ZapTarget>;
|
||||||
|
zapper?: Zapper;
|
||||||
|
amount?: SendSatsInputSelection;
|
||||||
|
}) {
|
||||||
|
if (!targets) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h2>
|
||||||
|
{zapper?.canZap() ? (
|
||||||
|
<FormattedMessage defaultMessage="Send zap" id="5ykRmX" />
|
||||||
|
) : (
|
||||||
|
<FormattedMessage defaultMessage="Send sats" id="DKnriN" />
|
||||||
|
)}
|
||||||
|
</h2>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (targets.length === 1 && targets[0].name) {
|
||||||
|
const t = targets[0];
|
||||||
|
const values = {
|
||||||
|
name: t.name,
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{t.zap?.pubkey && <ProfileImage pubkey={t.zap.pubkey} showUsername={false} />}
|
||||||
|
<h2>
|
||||||
|
{zapper?.canZap() ? (
|
||||||
|
<FormattedMessage defaultMessage="Send zap to {name}" id="SMO+on" values={values} />
|
||||||
|
) : (
|
||||||
|
<FormattedMessage defaultMessage="Send sats to {name}" id="JGrt9q" values={values} />
|
||||||
|
)}
|
||||||
|
</h2>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (targets.length > 1) {
|
||||||
|
const total = targets.reduce((acc, v) => (acc += v.weight), 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col g12">
|
||||||
|
<h2>
|
||||||
|
{zapper?.canZap() ? (
|
||||||
|
<FormattedMessage defaultMessage="Send zap splits to" id="ZS+jRE" />
|
||||||
|
) : (
|
||||||
|
<FormattedMessage defaultMessage="Send sats splits to" id="uc0din" />
|
||||||
|
)}
|
||||||
|
</h2>
|
||||||
|
<div className="flex g4 f-wrap">
|
||||||
|
{targets.map(v => (
|
||||||
|
<ProfileImage
|
||||||
|
key={v.value}
|
||||||
|
pubkey={v.value}
|
||||||
|
showUsername={false}
|
||||||
|
showFollowDistance={false}
|
||||||
|
imageOverlay={formatShort(Math.floor((amount?.amount ?? 0) * (v.weight / total)))}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
|
import { ZapType } from "@/Components/SendSats/ZapType";
|
||||||
|
import useLogin from "@/Hooks/useLogin";
|
||||||
|
|
||||||
|
export function SendSatsZapTypeSelector({
|
||||||
|
zapType,
|
||||||
|
setZapType,
|
||||||
|
}: {
|
||||||
|
zapType: ZapType;
|
||||||
|
setZapType: (t: ZapType) => void;
|
||||||
|
}) {
|
||||||
|
const { readonly } = useLogin(s => ({ readonly: s.readonly }));
|
||||||
|
const makeTab = (t: ZapType, n: React.ReactNode) => (
|
||||||
|
<button type="button" className={zapType === t ? "" : "secondary"} onClick={() => setZapType(t)}>
|
||||||
|
{n}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col g8">
|
||||||
|
<h3>
|
||||||
|
<FormattedMessage defaultMessage="Zap Type" id="+aZY2h" />
|
||||||
|
</h3>
|
||||||
|
<div className="flex g8">
|
||||||
|
{!readonly &&
|
||||||
|
makeTab(ZapType.PublicZap, <FormattedMessage defaultMessage="Public" id="/PCavi" description="Public Zap" />)}
|
||||||
|
{/*makeTab(ZapType.PrivateZap, "Private")*/}
|
||||||
|
{makeTab(ZapType.AnonZap, <FormattedMessage defaultMessage="Anon" id="wWLwvh" description="Anonymous Zap" />)}
|
||||||
|
{makeTab(
|
||||||
|
ZapType.NonZap,
|
||||||
|
<FormattedMessage defaultMessage="Non-Zap" id="AnLrRC" description="Non-Zap, Regular LN payment" />,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
23
packages/app/src/Components/SendSats/SuccessAction.tsx
Normal file
23
packages/app/src/Components/SendSats/SuccessAction.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { LNURLSuccessAction } from "@snort/shared";
|
||||||
|
import React from "react";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
|
import Icon from "@/Components/Icons/Icon";
|
||||||
|
|
||||||
|
export function SuccessAction({ success }: { success: LNURLSuccessAction }) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<p className="flex g12">
|
||||||
|
<Icon name="check" className="success" />
|
||||||
|
{success?.description ?? <FormattedMessage defaultMessage="Paid" id="u/vOPu" />}
|
||||||
|
</p>
|
||||||
|
{success.url && (
|
||||||
|
<p>
|
||||||
|
<a href={success.url} rel="noreferrer" target="_blank">
|
||||||
|
{success.url}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
6
packages/app/src/Components/SendSats/ZapType.tsx
Normal file
6
packages/app/src/Components/SendSats/ZapType.tsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export enum ZapType {
|
||||||
|
PublicZap = 1,
|
||||||
|
AnonZap = 2,
|
||||||
|
PrivateZap = 3,
|
||||||
|
NonZap = 4,
|
||||||
|
}
|
@ -1,3 +1,5 @@
|
|||||||
|
/* eslint-disable max-lines */
|
||||||
|
|
||||||
import { sha256 } from "@noble/hashes/sha256";
|
import { sha256 } from "@noble/hashes/sha256";
|
||||||
|
|
||||||
const animals = [
|
const animals = [
|
||||||
|
Reference in New Issue
Block a user