Merge branch 'zap-split' into 'main'

Display zap splits preview to users

Closes #1687

See merge request soapbox-pub/soapbox!3096
This commit is contained in:
Alex Gleason 2024-09-24 20:21:17 +00:00
commit 347083e8ea
12 changed files with 496 additions and 31 deletions

View File

@ -0,0 +1,90 @@
import { useState, useEffect } from 'react';
import { useApi } from 'soapbox/hooks';
import { type ZapSplitData } from 'soapbox/schemas/zap-split';
import type { Account as AccountEntity, Status as StatusEntity } from 'soapbox/types/entities';
interface SplitValue {
id: string;
amountSplit: number;
}
/**
* Custom hook to handle the logic for zap split calculations.
*
* This hook fetches zap split data from the server and calculates the amount to be received
* by the main account and the split amounts for other associated accounts.
*
* @param {StatusEntity | undefined} status - The current status entity.
* @param {AccountEntity} account - The account for which the zap split calculation is done.
*
* @returns {Object} An object containing the zap split arrays, zap split data, and a function to calculate the received amount.
*
* @property {ZapSplitData[]} zapArrays - Array of zap split data returned from the API.
* @property {Object} zapSplitData - Contains the total split amount, amount to receive, and individual split values.
* @property {Function} receiveAmount - A function to calculate the zap amount based on the split configuration.
*/
const useZapSplit = (status: StatusEntity | undefined, account: AccountEntity) => {
const api = useApi();
const [zapArrays, setZapArrays] = useState<ZapSplitData[]>([]);
const [zapSplitData, setZapSplitData] = useState<{splitAmount: number; receiveAmount: number; splitValues: SplitValue[]}>({ splitAmount: Number(), receiveAmount: Number(), splitValues: [] });
const fetchZapSplit = async (id: string) => {
return await api.get(`/api/v1/ditto/${id}/zap_splits`);
};
const loadZapSplitData = async () => {
if (status) {
const data = (await fetchZapSplit(status.id)).data;
setZapArrays(data);
}
};
/**
* Calculates and updates the zap amount that the main account will receive
* and the split amounts for other accounts.
*
* @param {number} zapAmount - The total amount of zaps to be split.
*/
const receiveAmount = (zapAmount: number) => {
if (zapArrays.length > 0) {
const zapAmountPrincipal = zapArrays.find((zapSplit: ZapSplitData) => zapSplit.account.id === account.id);
const zapAmountOthers = zapArrays.filter((zapSplit: ZapSplitData) => zapSplit.account.id !== account.id);
const totalWeightSplit = zapAmountOthers.reduce((e: number, b: ZapSplitData) => e + b.weight, 0);
const totalWeight = zapArrays.reduce((e: number, b: ZapSplitData) => e + b.weight, 0);
if (zapAmountPrincipal) {
const receiveZapAmount = Math.floor(zapAmountPrincipal.weight * (zapAmount / totalWeight));
const splitResult = zapAmount - receiveZapAmount;
let totalRoundedSplit = 0;
const values = zapAmountOthers.map((zapData) => {
const result = Math.floor(zapData.weight * (splitResult / totalWeightSplit));
totalRoundedSplit += result;
return { id: zapData.account.id, amountSplit: result };
});
const difference = splitResult - totalRoundedSplit;
if (difference !== 0 && values.length > 0) {
values[values.length - 1].amountSplit += difference;
}
if (zapSplitData.receiveAmount !== receiveZapAmount || zapSplitData.splitAmount !== splitResult) {
setZapSplitData({ splitAmount: splitResult, receiveAmount: receiveZapAmount, splitValues: values });
}
}
}
};
useEffect(() => {
loadZapSplitData();
}, [status]);
return { zapArrays, zapSplitData, receiveAmount };
};
export default useZapSplit;
export { SplitValue };

View File

@ -44,6 +44,7 @@ import {
VideoModal, VideoModal,
EditRuleModal, EditRuleModal,
ZapPayRequestModal, ZapPayRequestModal,
ZapSplitModal,
ZapInvoiceModal, ZapInvoiceModal,
ZapsModal, ZapsModal,
} from 'soapbox/features/ui/util/async-components'; } from 'soapbox/features/ui/util/async-components';
@ -96,6 +97,7 @@ const MODAL_COMPONENTS: Record<string, React.LazyExoticComponent<any>> = {
'ZAPS': ZapsModal, 'ZAPS': ZapsModal,
'ZAP_INVOICE': ZapInvoiceModal, 'ZAP_INVOICE': ZapInvoiceModal,
'ZAP_PAY_REQUEST': ZapPayRequestModal, 'ZAP_PAY_REQUEST': ZapPayRequestModal,
'ZAP_SPLIT': ZapSplitModal,
}; };
export type ModalType = keyof typeof MODAL_COMPONENTS | null; export type ModalType = keyof typeof MODAL_COMPONENTS | null;

View File

@ -75,7 +75,7 @@ const AvatarSelectionModal: React.FC<IAvatarSelectionModal> = ({ onClose, onNext
<div className='relative w-full'> <div className='relative w-full'>
<IconButton src={closeIcon} onClick={onClose} className='absolute -top-[6%] right-[2%] text-gray-500 hover:text-gray-700 rtl:rotate-180 dark:text-gray-300 dark:hover:text-gray-200' /> <IconButton src={closeIcon} onClick={onClose} className='absolute -top-[6%] right-[2%] text-gray-500 hover:text-gray-700 rtl:rotate-180 dark:text-gray-300 dark:hover:text-gray-200' />
<Stack space={2} justifyContent='center' alignItems='center' className='border-grey-200 -mx-4 mb-4 border-b border-solid pb-4 sm:-mx-10 sm:pb-10 dark:border-gray-800'> <Stack space={2} justifyContent='center' alignItems='center' className='border-grey-200 dark:border-grey-800 -mx-4 mb-4 border-b border-solid pb-4 sm:-mx-10 sm:pb-10'>
<Text size='2xl' align='center' weight='bold'> <Text size='2xl' align='center' weight='bold'>
<FormattedMessage id='onboarding.avatar.title' defaultMessage={'Choose a profile picture'} /> <FormattedMessage id='onboarding.avatar.title' defaultMessage={'Choose a profile picture'} />
</Text> </Text>

View File

@ -2,11 +2,13 @@ import { QRCodeCanvas } from 'qrcode.react';
import React from 'react'; import React from 'react';
import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
import { closeModal } from 'soapbox/actions/modals'; import { closeModal, openModal } from 'soapbox/actions/modals';
import { SplitValue } from 'soapbox/api/hooks/zap-split/useZapSplit';
import CopyableInput from 'soapbox/components/copyable-input'; import CopyableInput from 'soapbox/components/copyable-input';
import { Modal, Button, Stack, Avatar } from 'soapbox/components/ui'; import { Modal, Button, Stack, Avatar, HStack } from 'soapbox/components/ui';
import IconButton from 'soapbox/components/ui/icon-button/icon-button'; import IconButton from 'soapbox/components/ui/icon-button/icon-button';
import { useAppDispatch } from 'soapbox/hooks'; import { useAppDispatch } from 'soapbox/hooks';
import { ZapSplitData } from 'soapbox/schemas/zap-split';
import type { Account as AccountEntity } from 'soapbox/types/entities'; import type { Account as AccountEntity } from 'soapbox/types/entities';
@ -14,18 +16,26 @@ const closeIcon = require('@tabler/icons/outline/x.svg');
const messages = defineMessages({ const messages = defineMessages({
zap_open_wallet: { id: 'zap.open_wallet', defaultMessage: 'Open Wallet' }, zap_open_wallet: { id: 'zap.open_wallet', defaultMessage: 'Open Wallet' },
zap_next: { id: 'zap.next', defaultMessage: 'Next' },
}); });
interface ISplitData {
hasZapSplit: boolean;
zapSplitAccounts: ZapSplitData[];
splitValues: SplitValue[];
}
interface IZapInvoice{ interface IZapInvoice{
account: AccountEntity; account: AccountEntity;
invoice: string; invoice: string;
splitData: ISplitData;
onClose:(type?: string) => void; onClose:(type?: string) => void;
} }
const ZapInvoiceModal: React.FC<IZapInvoice> = ({ account, invoice, onClose }) => { const ZapInvoiceModal: React.FC<IZapInvoice> = ({ account, invoice, splitData, onClose }) => {
const intl = useIntl(); const intl = useIntl();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { hasZapSplit, zapSplitAccounts, splitValues } = splitData;
const onClickClose = () => { const onClickClose = () => {
onClose('ZAP_INVOICE'); onClose('ZAP_INVOICE');
dispatch(closeModal('ZAP_PAY_REQUEST')); dispatch(closeModal('ZAP_PAY_REQUEST'));
@ -35,6 +45,10 @@ const ZapInvoiceModal: React.FC<IZapInvoice> = ({ account, invoice, onClose }) =
return <FormattedMessage id='zap.send_to' defaultMessage='Send zaps to {target}' values={{ target: account.display_name }} />; return <FormattedMessage id='zap.send_to' defaultMessage='Send zaps to {target}' values={{ target: account.display_name }} />;
}; };
const handleNext = () => {
onClose('ZAP_INVOICE');
dispatch(openModal('ZAP_SPLIT', { zapSplitAccounts, splitValues }));
};
return ( return (
<Modal width='sm'> <Modal width='sm'>
@ -48,9 +62,12 @@ const ZapInvoiceModal: React.FC<IZapInvoice> = ({ account, invoice, onClose }) =
<div className='w-full'> <div className='w-full'>
<CopyableInput value={invoice} /> <CopyableInput value={invoice} />
</div> </div>
<a href={'lightning:' + invoice}> <HStack space={2}>
<Button type='submit' theme='primary' icon={require('@tabler/icons/outline/folder-open.svg')} text={intl.formatMessage(messages.zap_open_wallet)} /> <a href={'lightning:' + invoice}>
</a> <Button type='submit' theme='primary' icon={require('@tabler/icons/outline/folder-open.svg')} text={intl.formatMessage(messages.zap_open_wallet)} />
</a>
{hasZapSplit && <Button type='button' theme='muted' onClick={handleNext} text={intl.formatMessage(messages.zap_next)} />}
</HStack>
</Stack> </Stack>
</Modal> </Modal>
); );

View File

@ -0,0 +1,51 @@
import React from 'react';
import { HStack, Text } from 'soapbox/components/ui';
import VerificationBadge from 'soapbox/components/verification-badge';
import { useSoapboxConfig } from 'soapbox/hooks';
import { getAcct } from 'soapbox/utils/accounts';
import type { Account } from 'soapbox/schemas';
interface IDisplayName {
account: Pick<Account, 'id' | 'acct' | 'fqn' | 'verified' | 'display_name_html'>;
withSuffix?: boolean;
}
/**
* This component is different from other display name components because it displays the name inline.
*
* @param {IDisplayName} props - The properties for this component.
* @param {Pick<Account, 'id' | 'acct' | 'fqn' | 'verified' | 'display_name_html'>} props.account - The account object contains all the metadata for an account, such as the display name, ID, and more.
* @param {boolean} [props.withSuffix=true] - Determines whether to show the account suffix (eg, @danidfra).
*
* @returns {JSX.Element} The DisplayNameRow component.
*/
const DisplayNameRow: React.FC<IDisplayName> = ({ account, withSuffix = true }) => {
const { displayFqn = false } = useSoapboxConfig();
const { verified } = account;
const displayName = (
<HStack space={1} alignItems='center' justifyContent='center' grow>
<Text
size='sm'
weight='normal'
truncate
dangerouslySetInnerHTML={{ __html: account.display_name_html }}
/>
{verified && <VerificationBadge />}
</HStack>
);
const suffix = (<span className='display-name'>@{getAcct(account, displayFqn)}</span>);
return (
<div className='flex max-w-80 flex-col items-center justify-center text-center sm:flex-row sm:gap-2'>
{displayName}
<span className='hidden text-2xl font-bold sm:block'>-</span>
{withSuffix && suffix}
</div>
);
};
export default DisplayNameRow;

View File

@ -0,0 +1,109 @@
import React, { useState, useEffect } from 'react';
import { FormattedMessage } from 'react-intl';
import { useDispatch } from 'react-redux';
import { zap } from 'soapbox/actions/interactions';
import { SplitValue } from 'soapbox/api/hooks/zap-split/useZapSplit';
import { Modal } from 'soapbox/components/ui';
import ZapSplit from 'soapbox/features/ui/components/modals/zap-split/zap-split';
import { ZapSplitData } from 'soapbox/schemas/zap-split';
import type { AppDispatch } from 'soapbox/store';
interface IZapSplitModal {
zapSplitAccounts: ZapSplitData[];
splitValues: SplitValue[];
onClose:(type?: string) => void;
}
const ZapSplitModal: React.FC<IZapSplitModal> = ({ zapSplitAccounts, onClose, splitValues }) => {
const dispatch = useDispatch<AppDispatch>();
const [currentStep, setCurrentStep] = useState(0);
const [widthModal, setWidthModal] = useState<
'xl' | 'xs' | 'sm' | 'md' | 'lg' | '2xl' | '3xl' | '4xl' | undefined
>('sm');
const [invoice, setInvoice] = useState(undefined);
const handleNextStep = () => {
setInvoice(undefined);
setWidthModal('sm');
setCurrentStep((prevStep) => Math.min(prevStep + 1, zapSplitAccounts.length - 1));
};
const formattedData = zapSplitAccounts.map((splitData) => {
const amount =
splitValues.find((item) => item.id === splitData.account.id)?.amountSplit ??
0;
return {
acc: splitData,
zapAmount: amount,
};
});
const handleSubmit = async () => {
const zapComment = '';
const account = formattedData[currentStep].acc.account;
const zapAmount = formattedData[currentStep].zapAmount;
const invoice = await dispatch(
zap(account, undefined, zapAmount * 1000, zapComment),
);
if (!invoice) {
if (currentStep === zapSplitAccounts.length - 1) {
onClose('ZAP_SPLIT');
return;
}
handleNextStep();
return;
}
setInvoice(invoice);
setWidthModal('2xl');
};
useEffect(() => {
if (formattedData.length > 0) {
handleSubmit();
}
}, [currentStep]);
const onClickClose = () => {
onClose('ZAP_SPLIT');
};
const handleFinish = () => {
onClose('ZAP_SPLIT');
};
const renderTitle = () => {
return (
<FormattedMessage id='zap_split.title' defaultMessage='Zap Split' />
);
};
return (
<Modal title={renderTitle()} onClose={onClickClose} width={widthModal}>
<div className='relative flex flex-col sm:flex-row'>
{formattedData.length > 0 && (
<ZapSplit
zapData={formattedData[currentStep].acc}
zapAmount={formattedData[currentStep].zapAmount}
invoice={invoice}
onNext={handleNextStep}
isLastStep={currentStep === zapSplitAccounts.length - 1}
onFinish={handleFinish}
/>
)}
<p className='absolute -bottom-4 -right-2'>
<span className='font-bold'>
{currentStep + 1}/{zapSplitAccounts.length}
</span>
</p>
</div>
</Modal>
);
};
export default ZapSplitModal;

View File

@ -0,0 +1,119 @@
import { QRCodeCanvas } from 'qrcode.react';
import React from 'react';
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
import Account from 'soapbox/components/account';
import CopyableInput from 'soapbox/components/copyable-input';
import { Button, Stack, HStack } from 'soapbox/components/ui';
import { ZapSplitData } from 'soapbox/schemas/zap-split';
const messages = defineMessages({
zap_open_wallet: { id: 'zap.open_wallet', defaultMessage: 'Open Wallet' },
zap_next: { id: 'zap.next', defaultMessage: 'Next' },
});
interface IZapSplit {
zapData: ZapSplitData;
invoice: string | undefined;
zapAmount: number;
modalStep?: React.Dispatch<React.SetStateAction<number>>;
onNext: () => void;
onFinish: () => void;
isLastStep: boolean;
}
const ZapSplit = ({ zapData, zapAmount, invoice, onNext, isLastStep, onFinish }: IZapSplit) => {
const account = zapData.account;
const intl = useIntl();
const renderTitleQr = () => {
return (
<div className='max-w-[280px] truncate'>
<FormattedMessage id='zap.send_to' defaultMessage='Send zaps to {target}' values={{ target: account.display_name }} />
</div>
);
};
return (
<div className='flex w-full flex-col items-center justify-center sm:flex-row'>
<Stack space={10} alignItems='center' className='relative flex w-full pb-4 pt-2 sm:w-[80%]'>
<Stack space={4} justifyContent='center' className='w-full' alignItems='center'>
<Stack justifyContent='center' alignItems='center' className='w-3/5'>
<div className='max-w-[190px]'>
<Account account={account} showProfileHoverCard={false} />
</div>
</Stack>
<div className='bg-grey-500 dark:border-grey-800 -mx-4 w-full border-b border-solid sm:-mx-10' />
<Stack justifyContent='center' alignItems='center' className='min-w-72 text-center' space={4}>
<h3 className='text-xl font-bold'>
Help this community grow!
</h3>
<p className='flex h-[90px] w-3/5 items-center justify-center'>
{zapData.message ||
<FormattedMessage
id='zap_split.text'
defaultMessage='Your support will help us build an unstoppable empire and rule the galaxy!'
/>
}
</p>
</Stack>
</Stack>
<div className='flex justify-center'>
<div className='box-shadow:none rounded-none border-0 border-b-2 p-0.5 text-center !ring-0 dark:bg-transparent'>
<span className='!text-5xl font-bold'>{zapAmount}</span> sats
</div>
</div>
<a className='flex gap-2' href='/'>
<p className='text-sm'>
<FormattedMessage id='zap_split.question' defaultMessage='Why am I paying this?' />
</p>
</a>
</Stack>
{invoice && <div className='border-grey-500 mt-4 flex w-full border-t pt-4 sm:ml-4 sm:w-4/5 sm:border-l sm:border-t-0 sm:pl-4'>
<Stack space={6} className='relative m-auto' alignItems='center'>
<h3 className='text-xl font-bold'>
{renderTitleQr()}
</h3>
<QRCodeCanvas value={invoice} />
<div className='w-full'>
<CopyableInput value={invoice} />
</div>
<HStack space={2}>
<a href={'lightning:' + invoice}>
<Button type='submit' theme='primary' icon={require('@tabler/icons/outline/folder-open.svg')} text={intl.formatMessage(messages.zap_open_wallet)} />
</a>
{isLastStep ? (
<Button
type='button'
onClick={onFinish}
theme='muted'
className='!font-bold'
text={intl.formatMessage({ id: 'zap.finish', defaultMessage: 'Finish' })}
/>
) : (
<Button
type='button'
onClick={onNext}
theme='muted'
className='!font-bold'
text={intl.formatMessage(messages.zap_next)}
/>
)}
</HStack>
</Stack>
</div>}
</div>
);
};
export default ZapSplit;

View File

@ -179,3 +179,4 @@ export const AdminNostrRelays = lazy(() => import('soapbox/features/admin/nostr-
export const ZapPayRequestModal = lazy(() => import('soapbox/features/ui/components/modals/zap-pay-request-modal')); export const ZapPayRequestModal = lazy(() => import('soapbox/features/ui/components/modals/zap-pay-request-modal'));
export const ZapInvoiceModal = lazy(() => import('soapbox/features/ui/components/modals/zap-invoice')); export const ZapInvoiceModal = lazy(() => import('soapbox/features/ui/components/modals/zap-invoice'));
export const ZapsModal = lazy(() => import('soapbox/features/ui/components/modals/zaps-modal')); export const ZapsModal = lazy(() => import('soapbox/features/ui/components/modals/zaps-modal'));
export const ZapSplitModal = lazy(() => import('soapbox/features/ui/components/modals/zap-split/zap-split-modal'));

View File

@ -1,9 +1,10 @@
import closeIcon from '@tabler/icons/outline/x.svg'; import React, { useEffect, useState } from 'react';
import React, { useState } from 'react';
import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
import { Link } from 'react-router-dom';
import { zap } from 'soapbox/actions/interactions'; import { zap } from 'soapbox/actions/interactions';
import { openModal, closeModal } from 'soapbox/actions/modals'; import { openModal, closeModal } from 'soapbox/actions/modals';
import useZapSplit from 'soapbox/api/hooks/zap-split/useZapSplit';
import chestIcon from 'soapbox/assets/icons/chest.png'; import chestIcon from 'soapbox/assets/icons/chest.png';
import coinStack from 'soapbox/assets/icons/coin-stack.png'; import coinStack from 'soapbox/assets/icons/coin-stack.png';
import coinIcon from 'soapbox/assets/icons/coin.png'; import coinIcon from 'soapbox/assets/icons/coin.png';
@ -13,7 +14,6 @@ import DisplayNameInline from 'soapbox/components/display-name-inline';
import { Stack, Button, Input, Avatar, Text } from 'soapbox/components/ui'; import { Stack, Button, Input, Avatar, Text } from 'soapbox/components/ui';
import IconButton from 'soapbox/components/ui/icon-button/icon-button'; import IconButton from 'soapbox/components/ui/icon-button/icon-button';
import { useAppDispatch } from 'soapbox/hooks'; import { useAppDispatch } from 'soapbox/hooks';
import { shortNumberFormat } from 'soapbox/utils/numbers';
import ZapButton from './zap-button/zap-button'; import ZapButton from './zap-button/zap-button';
@ -33,29 +33,44 @@ interface IZapPayRequestForm {
onClose?(): void; onClose?(): void;
} }
const closeIcon = require('@tabler/icons/outline/x.svg');
const messages = defineMessages({ const messages = defineMessages({
zap_button_rounded: { id: 'zap.button.text.rounded', defaultMessage: 'Zap {amount}K sats' },
zap_button: { id: 'zap.button.text.raw', defaultMessage: 'Zap {amount} sats' }, zap_button: { id: 'zap.button.text.raw', defaultMessage: 'Zap {amount} sats' },
zap_commentPlaceholder: { id: 'zap.comment_input.placeholder', defaultMessage: 'Optional comment' }, zap_commentPlaceholder: { id: 'zap.comment_input.placeholder', defaultMessage: 'Optional comment' },
}); });
const ZapPayRequestForm = ({ account, status, onClose }: IZapPayRequestForm) => { const ZapPayRequestForm = ({ account, status, onClose }: IZapPayRequestForm) => {
const intl = useIntl(); const intl = useIntl();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const [zapComment, setZapComment] = useState(''); const [zapComment, setZapComment] = useState('');
// amount in millisatoshi const [zapAmount, setZapAmount] = useState(50); // amount in millisatoshi
const [zapAmount, setZapAmount] = useState(50); const { zapArrays, zapSplitData, receiveAmount } = useZapSplit(status, account);
const splitValues = zapSplitData.splitValues;
const hasZapSplit = zapArrays.length > 0;
const handleSubmit = async (e?: React.FormEvent<Element>) => { const handleSubmit = async (e?: React.FormEvent<Element>) => {
e?.preventDefault(); e?.preventDefault();
const invoice = await dispatch(zap(account, status, zapAmount * 1000, zapComment)); const zapSplitAccounts = zapArrays.filter(zapData => zapData.account.id !== account.id);
const splitData = { hasZapSplit, zapSplitAccounts, splitValues };
const invoice = hasZapSplit ? await dispatch(zap(account, status, zapSplitData.receiveAmount * 1000, zapComment)) : await dispatch(zap(account, status, zapAmount * 1000, zapComment));
// If invoice is undefined it means the user has paid through his extension // If invoice is undefined it means the user has paid through his extension
// In this case, we simply close the modal // In this case, we simply close the modal
if (!invoice) { if (!invoice) {
dispatch(closeModal('ZAP_PAY_REQUEST')); dispatch(closeModal('ZAP_PAY_REQUEST'));
// Dispatch the adm account
if (zapSplitAccounts.length > 0) {
dispatch(openModal('ZAP_SPLIT', { zapSplitAccounts, splitValues }));
}
return; return;
} }
// open QR code modal // open QR code modal
dispatch(openModal('ZAP_INVOICE', { invoice, account })); dispatch(closeModal('ZAP_PAY_REQUEST'));
dispatch(openModal('ZAP_INVOICE', { account, invoice, splitData }));
}; };
const handleCustomAmount = (e: React.ChangeEvent<HTMLInputElement>) => { const handleCustomAmount = (e: React.ChangeEvent<HTMLInputElement>) => {
@ -69,6 +84,17 @@ const ZapPayRequestForm = ({ account, status, onClose }: IZapPayRequestForm) =>
} }
}; };
const renderZapButtonText = () => {
if (zapAmount >= 1000) {
return intl.formatMessage(messages.zap_button_rounded, { amount: Math.round((zapAmount / 1000) * 10) / 10 });
}
return intl.formatMessage(messages.zap_button, { amount: zapAmount });
};
useEffect(() => {
receiveAmount(zapAmount);
}, [zapAmount, zapArrays]);
return ( return (
<Stack space={4} element='form' onSubmit={handleSubmit} justifyContent='center' alignItems='center' className='relative'> <Stack space={4} element='form' onSubmit={handleSubmit} justifyContent='center' alignItems='center' className='relative'>
<Stack space={2} justifyContent='center' alignItems='center' > <Stack space={2} justifyContent='center' alignItems='center' >
@ -101,29 +127,43 @@ const ZapPayRequestForm = ({ account, status, onClose }: IZapPayRequestForm) =>
<div className='relative flex items-end justify-center gap-4'> <div className='relative flex items-end justify-center gap-4'>
<Input <Input
type='text' onChange={handleCustomAmount} value={zapAmount} type='text' onChange={handleCustomAmount} value={zapAmount}
className='max-w-20 rounded-none border-0 border-b-4 p-0 text-center !text-2xl font-bold !ring-0 sm:max-w-28 sm:p-0.5 sm:!text-4xl dark:bg-transparent' className='box-shadow:none max-w-20 rounded-none border-0 border-b-4 p-0 text-center !text-2xl font-bold !ring-0 sm:max-w-28 sm:p-0.5 sm:!text-4xl dark:bg-transparent'
/> />
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} {hasZapSplit && <p className='absolute right-0 font-bold sm:-right-6 sm:text-xl'>sats</p>}
<p className='absolute -right-10 font-bold sm:-right-12 sm:text-xl'>
sats
</p>
</div> </div>
{hasZapSplit && <span className='flex justify-center text-xs'>
<FormattedMessage
id='zap.split_message.receiver'
defaultMessage='{receiver} will receive {amountReceiver} sats*' values={{ receiver: account.display_name, amountReceiver: zapSplitData.receiveAmount }}
/>
</span>}
</Stack> </Stack>
<div className='w-full'> <div className='w-full'>
<Input onChange={e => setZapComment(e.target.value)} type='text' placeholder={intl.formatMessage(messages.zap_commentPlaceholder)} /> <Input onChange={e => setZapComment(e.target.value)} type='text' placeholder={intl.formatMessage(messages.zap_commentPlaceholder)} />
</div> </div>
<Button {hasZapSplit ? <Stack space={2}>
className='m-auto w-auto'
type='submit' <Button className='m-auto w-auto' type='submit' theme='primary' icon={require('@tabler/icons/outline/bolt.svg')} text={'Zap sats'} disabled={zapAmount < 1 ? true : false} />
theme='primary'
icon={require('@tabler/icons/outline/bolt.svg')} <div className='flex items-center justify-center gap-2 sm:gap-4'>
disabled={zapAmount < 1 ? true : false} <span className='text-[10px] sm:text-xs'>
> <FormattedMessage
{intl.formatMessage(messages.zap_button, { amount: shortNumberFormat(zapAmount) })} id='zap.split_message.deducted'
</Button> defaultMessage='{amountDeducted} sats will deducted*' values={{ instance: account.display_name, amountDeducted: zapSplitData.splitAmount }}
/>
</span>
<Link to={'/'} className='text-xs underline'>
<img src={require('@tabler/icons/outline/info-square-rounded.svg')} className='w-4' alt='info-square-rounded' />
</Link>
</div>
</Stack> : <Button className='m-auto w-auto' type='submit' theme='primary' icon={require('@tabler/icons/outline/bolt.svg')} text={renderZapButtonText()} disabled={zapAmount < 1 ? true : false} />}
</Stack> </Stack>
); );
}; };

View File

@ -1650,7 +1650,15 @@
"video.unmute": "Unmute sound", "video.unmute": "Unmute sound",
"who_to_follow.title": "People To Follow", "who_to_follow.title": "People To Follow",
"zap.button.text.raw": "Zap {amount} sats", "zap.button.text.raw": "Zap {amount} sats",
"zap.button.text.rounded": "Zap {amount}K sats",
"zap.comment_input.placeholder": "Optional comment", "zap.comment_input.placeholder": "Optional comment",
"zap.finish": "Finish",
"zap.next": "Next",
"zap.open_wallet": "Open Wallet", "zap.open_wallet": "Open Wallet",
"zap.send_to": "Send zaps to {target}" "zap.send_to": "Send zaps to {target}",
"zap.split_message.deducted": "{amountDeducted} sats will deducted*",
"zap.split_message.receiver": "{receiver} will receive {amountReceiver} sats*",
"zap_split.question": "Why am I paying this?",
"zap_split.text": "Your support will help us build an unstoppable empire and rule the galaxy!",
"zap_split.title": "Zap Split"
} }

View File

@ -1651,5 +1651,11 @@
"zap.button.text.raw": "Zap {amount} sats", "zap.button.text.raw": "Zap {amount} sats",
"zap.comment_input.placeholder": "Comentário opcional", "zap.comment_input.placeholder": "Comentário opcional",
"zap.open_wallet": "Abrir Carteira", "zap.open_wallet": "Abrir Carteira",
"zap.send_to": "Enviar zaps para {target}" "zap.send_to": "Enviar zaps para {target}",
"zap.split_message.deducted": "{amountDeducted} sats serão deduzidos*",
"zap.split_message.receiver": "{receiver} receberá {amountReceiver} sats*",
"zap.unit": "Quantidade de zap em sats",
"zap_split.question": "Por que estou pagando isso?",
"zap_split.text": "Seu apoio nos ajudará a construir um império imparável e governar a galáxia!",
"zap_split.title": "Divisão de Zap"
} }

22
src/schemas/zap-split.ts Normal file
View File

@ -0,0 +1,22 @@
import { z } from 'zod';
import { type Account, accountSchema } from './account';
const addMethodsToAccount = (account: Account) => {
return {
...account,
get: (key: string) => (account as any)[key],
getIn: (path: string[]) => path.reduce((acc, key) => (acc as any)[key], account),
toJS: () => account,
};
};
const baseZapAccountSchema = z.object({
account: accountSchema.transform(addMethodsToAccount),
message: z.string().catch(''),
weight: z.number().catch(0),
});
type ZapSplitData = z.infer<typeof baseZapAccountSchema>;
export { baseZapAccountSchema, type ZapSplitData };