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:
commit
347083e8ea
|
@ -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 };
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||||
|
|
|
@ -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'));
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 };
|
Loading…
Reference in New Issue