kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Merge branch 'improve-fetch' into 'main'
Update hook to use react query See merge request soapbox-pub/soapbox!3367merge-requests/3369/merge
commit
c2376d15f6
|
@ -2,13 +2,14 @@ import { List as ImmutableList } from 'immutable';
|
||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
|
||||||
import { fetchZaps, expandZaps } from 'soapbox/actions/interactions.ts';
|
import { fetchZaps, expandZaps } from 'soapbox/actions/interactions.ts';
|
||||||
import ScrollableList from 'soapbox/components/scrollable-list.tsx';
|
import ScrollableList from 'soapbox/components/scrollable-list.tsx';
|
||||||
import Modal from 'soapbox/components/ui/modal.tsx';
|
import Modal from 'soapbox/components/ui/modal.tsx';
|
||||||
import Spinner from 'soapbox/components/ui/spinner.tsx';
|
import Spinner from 'soapbox/components/ui/spinner.tsx';
|
||||||
import Text from 'soapbox/components/ui/text.tsx';
|
import Text from 'soapbox/components/ui/text.tsx';
|
||||||
import AccountContainer from 'soapbox/containers/account-container.tsx';
|
import AccountContainer from 'soapbox/containers/account-container.tsx';
|
||||||
import { useWalletStore, useZappedByCashu } from 'soapbox/features/zap/hooks/useHooks.ts';
|
import { useWalletStore, useZappedByCashu } from 'soapbox/features/wallet/hooks/useHooks.ts';
|
||||||
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
||||||
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
||||||
import { shortNumberFormat } from 'soapbox/utils/numbers.tsx';
|
import { shortNumberFormat } from 'soapbox/utils/numbers.tsx';
|
||||||
|
@ -32,7 +33,7 @@ const ZapsModal: React.FC<IZapsModal> = ({ onClose, statusId }) => {
|
||||||
const next = useAppSelector((state) => state.user_lists.zapped_by.get(statusId)?.next);
|
const next = useAppSelector((state) => state.user_lists.zapped_by.get(statusId)?.next);
|
||||||
const nutzaps = useWalletStore().nutzappedRecord[statusId];
|
const nutzaps = useWalletStore().nutzappedRecord[statusId];
|
||||||
const { nextZaps } = useWalletStore();
|
const { nextZaps } = useWalletStore();
|
||||||
const { getNutzappedBy, expandNutzappedBy } = useZappedByCashu();
|
const { getNutzappedBy, expandNutzappedBy } = useZappedByCashu(statusId);
|
||||||
|
|
||||||
const accounts = useMemo((): ImmutableList<IAccountWithZaps> | undefined => {
|
const accounts = useMemo((): ImmutableList<IAccountWithZaps> | undefined => {
|
||||||
if (!zaps && !nutzaps) return;
|
if (!zaps && !nutzaps) return;
|
||||||
|
@ -51,7 +52,7 @@ const ZapsModal: React.FC<IZapsModal> = ({ onClose, statusId }) => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData();
|
fetchData();
|
||||||
getNutzappedBy(statusId);
|
getNutzappedBy();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onClickClose = () => {
|
const onClickClose = () => {
|
||||||
|
@ -63,7 +64,7 @@ const ZapsModal: React.FC<IZapsModal> = ({ onClose, statusId }) => {
|
||||||
dispatch(expandZaps(statusId, next));
|
dispatch(expandZaps(statusId, next));
|
||||||
}
|
}
|
||||||
if (nextZaps) {
|
if (nextZaps) {
|
||||||
expandNutzappedBy(statusId);
|
expandNutzappedBy();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,8 @@ import eyeIcon from '@tabler/icons/outline/eye.svg';
|
||||||
import walletIcon from '@tabler/icons/outline/wallet.svg';
|
import walletIcon from '@tabler/icons/outline/wallet.svg';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
|
||||||
import Button from 'soapbox/components/ui/button.tsx';
|
import Button from 'soapbox/components/ui/button.tsx';
|
||||||
import HStack from 'soapbox/components/ui/hstack.tsx';
|
import HStack from 'soapbox/components/ui/hstack.tsx';
|
||||||
|
@ -12,7 +14,7 @@ import Icon from 'soapbox/components/ui/icon.tsx';
|
||||||
import Stack from 'soapbox/components/ui/stack.tsx';
|
import Stack from 'soapbox/components/ui/stack.tsx';
|
||||||
import Text from 'soapbox/components/ui/text.tsx';
|
import Text from 'soapbox/components/ui/text.tsx';
|
||||||
import Balance from 'soapbox/features/wallet/components/balance.tsx';
|
import Balance from 'soapbox/features/wallet/components/balance.tsx';
|
||||||
import { useWallet } from 'soapbox/features/zap/hooks/useHooks.ts';
|
import { useWallet } from 'soapbox/features/wallet/hooks/useHooks.ts';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
wallet: { id: 'wallet.title', defaultMessage: 'Wallet' },
|
wallet: { id: 'wallet.title', defaultMessage: 'Wallet' },
|
||||||
|
@ -23,7 +25,7 @@ const messages = defineMessages({
|
||||||
|
|
||||||
const PocketWallet = () => {
|
const PocketWallet = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { wallet } = useWallet();
|
const { walletData } = useWallet();
|
||||||
|
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
const [eyeClosed, setEyeClosed] = useState(() => {
|
const [eyeClosed, setEyeClosed] = useState(() => {
|
||||||
|
@ -35,24 +37,26 @@ const PocketWallet = () => {
|
||||||
localStorage.setItem('soapbox:wallet:eye', JSON.stringify(eyeClosed));
|
localStorage.setItem('soapbox:wallet:eye', JSON.stringify(eyeClosed));
|
||||||
}, [eyeClosed]);
|
}, [eyeClosed]);
|
||||||
|
|
||||||
if (!wallet) {
|
if (!walletData) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack className='rounded-lg border p-2 px-4 black:border-gray-500 dark:border-gray-500' alignItems='center' space={4}>
|
<Stack className='rounded-lg border p-2 px-4 black:border-gray-500 dark:border-gray-500' alignItems='center' space={4}>
|
||||||
<HStack className='w-full' justifyContent='between' alignItems='center' >
|
<HStack className='w-full' justifyContent='between' alignItems='center' >
|
||||||
<HStack space={1} alignItems='center'>
|
<Link to={'/wallet'}>
|
||||||
<Icon src={walletIcon} size={20} className='text-gray-200' />
|
<HStack space={1} alignItems='center'>
|
||||||
<Text size='lg'>
|
<Icon src={walletIcon} size={20} className='text-gray-200' />
|
||||||
{intl.formatMessage(messages.wallet)}
|
<Text size='lg'>
|
||||||
</Text>
|
{intl.formatMessage(messages.wallet)}
|
||||||
</HStack>
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</Link>
|
||||||
|
|
||||||
<HStack alignItems='center' space={2}>
|
<HStack alignItems='center' space={2}>
|
||||||
{!expanded && <>
|
{!expanded && <>
|
||||||
{ eyeClosed ? <Text className='text-sm !text-gray-500'>{intl.formatMessage({ id: 'wallet.hidden.balance', defaultMessage: '••••••' })}</Text> : <Text>
|
{ eyeClosed ? <Text className='text-sm !text-gray-500'>{intl.formatMessage({ id: 'wallet.hidden.balance', defaultMessage: '••••••' })}</Text> : <Text>
|
||||||
{intl.formatMessage(messages.balance, { amount: wallet.balance })}
|
{intl.formatMessage(messages.balance, { amount: walletData?.balance })}
|
||||||
</Text>}
|
</Text>}
|
||||||
|
|
||||||
<Button className='!ml-1 space-x-2 !border-none !p-0 !text-gray-500 focus:!ring-transparent focus:ring-offset-transparent rtl:ml-0 rtl:mr-1 rtl:space-x-reverse' theme='transparent' onClick={() => setEyeClosed(!eyeClosed)}>
|
<Button className='!ml-1 space-x-2 !border-none !p-0 !text-gray-500 focus:!ring-transparent focus:ring-offset-transparent rtl:ml-0 rtl:mr-1 rtl:space-x-reverse' theme='transparent' onClick={() => setEyeClosed(!eyeClosed)}>
|
||||||
|
|
|
@ -7,6 +7,7 @@ import QRCode from 'qrcode.react';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
|
||||||
import CopyableInput from 'soapbox/components/copyable-input.tsx';
|
import CopyableInput from 'soapbox/components/copyable-input.tsx';
|
||||||
import Button from 'soapbox/components/ui/button.tsx';
|
import Button from 'soapbox/components/ui/button.tsx';
|
||||||
import Divider from 'soapbox/components/ui/divider.tsx';
|
import Divider from 'soapbox/components/ui/divider.tsx';
|
||||||
|
@ -17,7 +18,7 @@ import Input from 'soapbox/components/ui/input.tsx';
|
||||||
import Stack from 'soapbox/components/ui/stack.tsx';
|
import Stack from 'soapbox/components/ui/stack.tsx';
|
||||||
import Text from 'soapbox/components/ui/text.tsx';
|
import Text from 'soapbox/components/ui/text.tsx';
|
||||||
import { SelectDropdown } from 'soapbox/features/forms/index.tsx';
|
import { SelectDropdown } from 'soapbox/features/forms/index.tsx';
|
||||||
import { useTransactions, useWallet } from 'soapbox/features/zap/hooks/useHooks.ts';
|
import { useTransactions, useWallet } from 'soapbox/features/wallet/hooks/useHooks.ts';
|
||||||
import { useApi } from 'soapbox/hooks/useApi.ts';
|
import { useApi } from 'soapbox/hooks/useApi.ts';
|
||||||
import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts';
|
import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts';
|
||||||
import { Quote, quoteSchema } from 'soapbox/schemas/wallet.ts';
|
import { Quote, quoteSchema } from 'soapbox/schemas/wallet.ts';
|
||||||
|
@ -90,7 +91,7 @@ const NewMint = ({ onBack, list }: NewMintProps) => {
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { getWallet } = useWallet();
|
const { getWallet } = useWallet();
|
||||||
const { getTransactions } = useTransactions();
|
const { refetch } = useTransactions();
|
||||||
|
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
@ -113,7 +114,7 @@ const NewMint = ({ onBack, list }: NewMintProps) => {
|
||||||
toast.success(intl.formatMessage(messages.paidMessage));
|
toast.success(intl.formatMessage(messages.paidMessage));
|
||||||
onBack();
|
onBack();
|
||||||
getWallet();
|
getWallet();
|
||||||
getTransactions();
|
refetch();
|
||||||
handleClean();
|
handleClean();
|
||||||
setCurrentState('default');
|
setCurrentState('default');
|
||||||
}
|
}
|
||||||
|
@ -233,7 +234,7 @@ const NewMint = ({ onBack, list }: NewMintProps) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const Balance = () => {
|
const Balance = () => {
|
||||||
const { wallet } = useWallet();
|
const { walletData } = useWallet();
|
||||||
const [amount, setAmount] = useState(0);
|
const [amount, setAmount] = useState(0);
|
||||||
const [mints, setMints] = useState<string[]>([]);
|
const [mints, setMints] = useState<string[]>([]);
|
||||||
const { account } = useOwnAccount();
|
const { account } = useOwnAccount();
|
||||||
|
@ -246,11 +247,11 @@ const Balance = () => {
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() => {
|
() => {
|
||||||
if (wallet){
|
if (walletData){
|
||||||
setMints([...wallet.mints]);
|
setMints([...walletData.mints]);
|
||||||
setAmount(wallet.balance);
|
setAmount(walletData.balance);
|
||||||
}
|
}
|
||||||
}, [wallet],
|
}, [walletData],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
|
|
|
@ -12,12 +12,12 @@ import Stack from 'soapbox/components/ui/stack.tsx';
|
||||||
import Text from 'soapbox/components/ui/text.tsx';
|
import Text from 'soapbox/components/ui/text.tsx';
|
||||||
import Tooltip from 'soapbox/components/ui/tooltip.tsx';
|
import Tooltip from 'soapbox/components/ui/tooltip.tsx';
|
||||||
import { MintEditor } from 'soapbox/features/wallet/components/editable-lists.tsx';
|
import { MintEditor } from 'soapbox/features/wallet/components/editable-lists.tsx';
|
||||||
import { useWallet } from 'soapbox/features/zap/hooks/useHooks.ts';
|
import { useCreateWallet } from 'soapbox/features/wallet/hooks/useHooks.ts';
|
||||||
import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts';
|
import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'wallet.create_wallet.title', defaultMessage: 'You don\'t have a wallet' },
|
title: { id: 'wallet.create.title', defaultMessage: 'You don\'t have a wallet' },
|
||||||
question: { id: 'wallet.create_wallet.question', defaultMessage: 'Do you want create one?' },
|
question: { id: 'wallet.create.question', defaultMessage: 'Do you want create one?' },
|
||||||
button: { id: 'wallet.button.create_wallet', defaultMessage: 'Create' },
|
button: { id: 'wallet.button.create_wallet', defaultMessage: 'Create' },
|
||||||
mints: { id: 'wallet.mints', defaultMessage: 'Mints' },
|
mints: { id: 'wallet.mints', defaultMessage: 'Mints' },
|
||||||
});
|
});
|
||||||
|
@ -28,7 +28,7 @@ const CreateWallet = () => {
|
||||||
const [formActive, setFormActive] = useState(false);
|
const [formActive, setFormActive] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [mints, setMints] = useState<string[]>([]);
|
const [mints, setMints] = useState<string[]>([]);
|
||||||
const { createWallet } = useWallet();
|
const { createWallet } = useCreateWallet();
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
@ -38,8 +38,12 @@ const CreateWallet = () => {
|
||||||
relays: [],
|
relays: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
await createWallet(walletInfo);
|
try {
|
||||||
setIsLoading(false);
|
await createWallet(walletInfo);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
|
|
|
@ -9,7 +9,7 @@ import Spinner from 'soapbox/components/ui/spinner.tsx';
|
||||||
import Stack from 'soapbox/components/ui/stack.tsx';
|
import Stack from 'soapbox/components/ui/stack.tsx';
|
||||||
import SvgIcon from 'soapbox/components/ui/svg-icon.tsx';
|
import SvgIcon from 'soapbox/components/ui/svg-icon.tsx';
|
||||||
import Text from 'soapbox/components/ui/text.tsx';
|
import Text from 'soapbox/components/ui/text.tsx';
|
||||||
import { useTransactions } from 'soapbox/features/zap/hooks/useHooks.ts';
|
import { useWalletStore } from 'soapbox/features/wallet/hooks/useHooks.ts';
|
||||||
import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts';
|
import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts';
|
||||||
|
|
||||||
const themes = {
|
const themes = {
|
||||||
|
@ -91,7 +91,7 @@ interface ITransactions {
|
||||||
|
|
||||||
const Transactions = ({ limit }: ITransactions) => {
|
const Transactions = ({ limit }: ITransactions) => {
|
||||||
const { account } = useOwnAccount();
|
const { account } = useOwnAccount();
|
||||||
const { transactions } = useTransactions();
|
const { transactions } = useWalletStore();
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -7,30 +7,27 @@ import Spinner from 'soapbox/components/ui/spinner.tsx';
|
||||||
import Stack from 'soapbox/components/ui/stack.tsx';
|
import Stack from 'soapbox/components/ui/stack.tsx';
|
||||||
import Text from 'soapbox/components/ui/text.tsx';
|
import Text from 'soapbox/components/ui/text.tsx';
|
||||||
import { MintEditor } from 'soapbox/features/wallet/components/editable-lists.tsx';
|
import { MintEditor } from 'soapbox/features/wallet/components/editable-lists.tsx';
|
||||||
import { useWallet } from 'soapbox/features/zap/hooks/useHooks.ts';
|
import { useUpdateWallet, useWallet } from 'soapbox/features/wallet/hooks/useHooks.ts';
|
||||||
import { useApi } from 'soapbox/hooks/useApi.ts';
|
|
||||||
import toast from 'soapbox/toast.tsx';
|
import toast from 'soapbox/toast.tsx';
|
||||||
import { isURL } from 'soapbox/utils/auth.ts';
|
import { isURL } from 'soapbox/utils/auth.ts';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'wallet.mints', defaultMessage: 'Mints' },
|
title: { id: 'wallet.mints', defaultMessage: 'Mints' },
|
||||||
loadingError: { id: 'wallet.loading_error', defaultMessage: 'An unexpected error occurred while loading your wallet data.' },
|
loadingError: { id: 'wallet.loading_error', defaultMessage: 'An unexpected error occurred while loading your wallet data.' },
|
||||||
error: { id: 'wallet.mints.error', defaultMessage: 'Failed to update mints.' },
|
|
||||||
empty: { id: 'wallet.mints.empty', defaultMessage: 'At least one mint is required.' },
|
empty: { id: 'wallet.mints.empty', defaultMessage: 'At least one mint is required.' },
|
||||||
url: { id: 'wallet.invalid_url', defaultMessage: 'All strings must be valid URLs.' },
|
url: { id: 'wallet.invalid_url', defaultMessage: 'All strings must be valid URLs.' },
|
||||||
success: { id: 'wallet.mints.success', defaultMessage: 'Mints updated with success!' },
|
|
||||||
send: { id: 'common.send', defaultMessage: 'Send' },
|
send: { id: 'common.send', defaultMessage: 'Send' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const WalletMints = () => {
|
const WalletMints = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const api = useApi();
|
const { walletData } = useWallet();
|
||||||
const { wallet } = useWallet();
|
|
||||||
|
|
||||||
const [relays, setRelays] = useState<string[]>([]);
|
const [relays, setRelays] = useState<string[]>([]);
|
||||||
const [initialMints, setInitialMints] = useState<string[]>([]);
|
const [initialMints, setInitialMints] = useState<string[]>([]);
|
||||||
const [mints, setMints] = useState<string[]>([]);
|
const [mints, setMints] = useState<string[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
const { updateWallet, isLoading } = useUpdateWallet();
|
||||||
|
const [isInitialLoading, setIsInitialLoading] = useState<boolean>(isLoading);
|
||||||
const [hasError, setHasError] = useState<boolean>(false);
|
const [hasError, setHasError] = useState<boolean>(false);
|
||||||
|
|
||||||
const handleClick = async () =>{
|
const handleClick = async () =>{
|
||||||
|
@ -48,53 +45,46 @@ const WalletMints = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await api.put('/api/v1/ditto/cashu/wallet', { mints: mints, relays: relays });
|
await updateWallet({ mints, relays });
|
||||||
|
|
||||||
setInitialMints(mints);
|
setInitialMints(mints);
|
||||||
|
|
||||||
toast.success(intl.formatMessage(messages.success));
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : intl.formatMessage(messages.error);
|
|
||||||
toast.error(errorMessage);
|
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() => {
|
() => {
|
||||||
setIsLoading(true);
|
setIsInitialLoading(true);
|
||||||
setHasError(false);
|
setHasError(false);
|
||||||
|
|
||||||
if (wallet) {
|
if (walletData) {
|
||||||
try {
|
try {
|
||||||
setMints(wallet.mints ?? []);
|
setMints(walletData.mints ?? []);
|
||||||
setInitialMints(wallet.mints ?? []);
|
setInitialMints(walletData.mints ?? []);
|
||||||
setRelays(wallet.relays ?? []);
|
setRelays(walletData.relays ?? []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error setting wallet data:', error);
|
console.error('Error setting wallet data:', error);
|
||||||
setHasError(true);
|
setHasError(true);
|
||||||
toast.error(intl.formatMessage(messages.loadingError));
|
toast.error(intl.formatMessage(messages.loadingError));
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsInitialLoading(false);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Handle the case when wallet is null or undefined
|
setIsInitialLoading(false);
|
||||||
setIsLoading(false);
|
if (walletData === undefined) {
|
||||||
if (wallet === undefined) { // wallet is still loading
|
setIsInitialLoading(true);
|
||||||
// Keep loading state true
|
} else if (walletData === null) {
|
||||||
setIsLoading(true);
|
|
||||||
} else if (wallet === null) { // wallet failed to load
|
|
||||||
setHasError(true);
|
setHasError(true);
|
||||||
toast.error(intl.formatMessage(messages.loadingError));
|
toast.error(intl.formatMessage(messages.loadingError));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [wallet, intl],
|
}, [walletData, intl],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column label={intl.formatMessage(messages.title)} >
|
<Column label={intl.formatMessage(messages.title)} >
|
||||||
{(() => {
|
{(() => {
|
||||||
if (isLoading) {
|
if (isInitialLoading) {
|
||||||
return (
|
return (
|
||||||
<Stack space={2} className='flex h-32 items-center justify-center'>
|
<Stack space={2} className='flex h-32 items-center justify-center'>
|
||||||
<Spinner />
|
<Spinner />
|
||||||
|
@ -110,7 +100,7 @@ const WalletMints = () => {
|
||||||
return (
|
return (
|
||||||
<Stack space={2}>
|
<Stack space={2}>
|
||||||
<MintEditor items={mints} setItems={setMints} />
|
<MintEditor items={mints} setItems={setMints} />
|
||||||
<Button className='w-full' theme='primary' onClick={handleClick}>
|
<Button className='w-full' theme='primary' onClick={handleClick} disabled={isLoading}>
|
||||||
{intl.formatMessage(messages.send)}
|
{intl.formatMessage(messages.send)}
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
|
@ -7,30 +7,27 @@ import Spinner from 'soapbox/components/ui/spinner.tsx';
|
||||||
import Stack from 'soapbox/components/ui/stack.tsx';
|
import Stack from 'soapbox/components/ui/stack.tsx';
|
||||||
import Text from 'soapbox/components/ui/text.tsx';
|
import Text from 'soapbox/components/ui/text.tsx';
|
||||||
import { RelayEditor } from 'soapbox/features/wallet/components/editable-lists.tsx';
|
import { RelayEditor } from 'soapbox/features/wallet/components/editable-lists.tsx';
|
||||||
import { useWallet } from 'soapbox/features/zap/hooks/useHooks.ts';
|
import { useUpdateWallet, useWallet } from 'soapbox/features/wallet/hooks/useHooks.ts';
|
||||||
import { useApi } from 'soapbox/hooks/useApi.ts';
|
|
||||||
import toast from 'soapbox/toast.tsx';
|
import toast from 'soapbox/toast.tsx';
|
||||||
import { isURL } from 'soapbox/utils/auth.ts';
|
import { isURL } from 'soapbox/utils/auth.ts';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'wallet.relays', defaultMessage: 'Wallet Relays' },
|
title: { id: 'wallet.relays', defaultMessage: 'Wallet Relays' },
|
||||||
loadingError: { id: 'wallet.loading_error', defaultMessage: 'An unexpected error occurred while loading your wallet data.' },
|
loadingError: { id: 'wallet.loading_error', defaultMessage: 'An unexpected error occurred while loading your wallet data.' },
|
||||||
error: { id: 'wallet.relays.error', defaultMessage: 'Failed to update relays.' },
|
|
||||||
empty: { id: 'wallet.relays.empty', defaultMessage: 'At least one relay is required.' },
|
empty: { id: 'wallet.relays.empty', defaultMessage: 'At least one relay is required.' },
|
||||||
url: { id: 'wallet.invalid_url', defaultMessage: 'All strings must be valid URLs.' },
|
url: { id: 'wallet.invalid_url', defaultMessage: 'All strings must be valid URLs.' },
|
||||||
success: { id: 'wallet.relays.success', defaultMessage: 'Relays updated with success!' },
|
|
||||||
send: { id: 'common.send', defaultMessage: 'Send' },
|
send: { id: 'common.send', defaultMessage: 'Send' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const WalletRelays = () => {
|
const WalletRelays = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const api = useApi();
|
const { walletData } = useWallet();
|
||||||
const { wallet } = useWallet();
|
|
||||||
|
|
||||||
const [relays, setRelays] = useState<string[]>([]);
|
const [relays, setRelays] = useState<string[]>([]);
|
||||||
const [initialRelays, setInitialRelays] = useState<string[]>([]);
|
const [initialRelays, setInitialRelays] = useState<string[]>([]);
|
||||||
const [mints, setMints] = useState<string[]>([]);
|
const [mints, setMints] = useState<string[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
const { updateWallet, isLoading } = useUpdateWallet();
|
||||||
|
const [isInitialLoading, setIsInitialLoading] = useState<boolean>(isLoading);
|
||||||
const [hasError, setHasError] = useState<boolean>(false);
|
const [hasError, setHasError] = useState<boolean>(false);
|
||||||
|
|
||||||
const handleClick = async () =>{
|
const handleClick = async () =>{
|
||||||
|
@ -49,49 +46,46 @@ const WalletRelays = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.put('/api/v1/ditto/cashu/wallet', { mints: mints, relays: relays });
|
await updateWallet({ mints, relays });
|
||||||
setInitialRelays(relays);
|
setInitialRelays(relays);
|
||||||
toast.success(intl.formatMessage(messages.success));
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : intl.formatMessage(messages.error);
|
|
||||||
toast.error(errorMessage);
|
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() => {
|
() => {
|
||||||
setIsLoading(true);
|
setIsInitialLoading(true);
|
||||||
setHasError(false);
|
setHasError(false);
|
||||||
|
|
||||||
if (wallet) {
|
if (walletData) {
|
||||||
try {
|
try {
|
||||||
setMints(wallet.mints ?? []);
|
setMints(walletData.mints ?? []);
|
||||||
setInitialRelays(wallet.relays ?? []);
|
setInitialRelays(walletData.relays ?? []);
|
||||||
setRelays(wallet.relays ?? []);
|
setRelays(walletData.relays ?? []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error setting wallet data:', error);
|
console.error('Error setting wallet data:', error);
|
||||||
setHasError(true);
|
setHasError(true);
|
||||||
toast.error(intl.formatMessage(messages.loadingError));
|
toast.error(intl.formatMessage(messages.loadingError));
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsInitialLoading(false);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setIsLoading(false);
|
setIsInitialLoading(false);
|
||||||
if (wallet === undefined) {
|
if (walletData === undefined) {
|
||||||
setIsLoading(true);
|
setIsInitialLoading(true);
|
||||||
} else if (wallet === null) {
|
} else if (walletData === null) {
|
||||||
setHasError(true);
|
setHasError(true);
|
||||||
toast.error(intl.formatMessage(messages.loadingError));
|
toast.error(intl.formatMessage(messages.loadingError));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [wallet, intl],
|
}, [walletData, intl],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column label={intl.formatMessage(messages.title)} >
|
<Column label={intl.formatMessage(messages.title)} >
|
||||||
{(() => {
|
{(() => {
|
||||||
if (isLoading) {
|
if (isInitialLoading) {
|
||||||
return (
|
return (
|
||||||
<Stack space={2} className='flex h-32 items-center justify-center'>
|
<Stack space={2} className='flex h-32 items-center justify-center'>
|
||||||
<Spinner />
|
<Spinner />
|
||||||
|
@ -107,7 +101,7 @@ const WalletRelays = () => {
|
||||||
return (
|
return (
|
||||||
<Stack space={2}>
|
<Stack space={2}>
|
||||||
<RelayEditor items={relays} setItems={setRelays} />
|
<RelayEditor items={relays} setItems={setRelays} />
|
||||||
<Button className='w-full' theme='primary' onClick={handleClick}>
|
<Button className='w-full' theme='primary' onClick={handleClick} disabled={isLoading}>
|
||||||
{intl.formatMessage(messages.send)}
|
{intl.formatMessage(messages.send)}
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
|
||||||
import { Column } from 'soapbox/components/ui/column.tsx';
|
import { Column } from 'soapbox/components/ui/column.tsx';
|
||||||
import Spinner from 'soapbox/components/ui/spinner.tsx';
|
import Spinner from 'soapbox/components/ui/spinner.tsx';
|
||||||
import Transactions from 'soapbox/features/wallet/components/transactions.tsx';
|
import Transactions from 'soapbox/features/wallet/components/transactions.tsx';
|
||||||
import { useTransactions } from 'soapbox/features/zap/hooks/useHooks.ts';
|
import { useTransactions, useWalletStore } from 'soapbox/features/wallet/hooks/useHooks.ts';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'wallet.transactions', defaultMessage: 'Transactions' },
|
title: { id: 'wallet.transactions', defaultMessage: 'Transactions' },
|
||||||
|
@ -15,18 +16,16 @@ const messages = defineMessages({
|
||||||
|
|
||||||
const WalletTransactions = () => {
|
const WalletTransactions = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { isLoading, expandTransactions } = useTransactions();
|
const { isLoading, isExpanding, expandTransactions } = useTransactions();
|
||||||
const observerRef = useRef<HTMLDivElement | null>(null);
|
const observerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const [showSpinner, setShowSpinner] = useState(false);
|
const { nextTransaction } = useWalletStore();
|
||||||
const [hasMoreTransactions, setHasMoreTransactions] = useState(true);
|
const hasMore = !!nextTransaction;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const observer = new IntersectionObserver(
|
const observer = new IntersectionObserver(
|
||||||
async ([entry]) => {
|
async ([entry]) => {
|
||||||
if (entry.isIntersecting && !isLoading && hasMoreTransactions) {
|
if (entry.isIntersecting && !isLoading && hasMore) {
|
||||||
setShowSpinner(true);
|
await expandTransactions();
|
||||||
const hasMore = await expandTransactions();
|
|
||||||
setHasMoreTransactions(hasMore);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ rootMargin: '100px' },
|
{ rootMargin: '100px' },
|
||||||
|
@ -37,14 +36,14 @@ const WalletTransactions = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}, [expandTransactions, isLoading, hasMoreTransactions]);
|
}, [expandTransactions, isLoading, hasMore]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column label={intl.formatMessage(messages.title)}>
|
<Column label={intl.formatMessage(messages.title)}>
|
||||||
<Transactions />
|
<Transactions />
|
||||||
<div className='flex w-full justify-center' ref={observerRef}>
|
<div className='flex w-full justify-center' ref={observerRef}>
|
||||||
{showSpinner && isLoading && <Spinner />}
|
{isLoading || isExpanding && <Spinner />}
|
||||||
{!hasMoreTransactions && (
|
{!hasMore && (
|
||||||
<div className='py-4 text-center text-gray-600'>
|
<div className='py-4 text-center text-gray-600'>
|
||||||
{intl.formatMessage(messages.noMoreTransactions)}
|
{intl.formatMessage(messages.noMoreTransactions)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,297 @@
|
||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
import { useApi } from 'soapbox/hooks/useApi.ts';
|
||||||
|
import { NutzappedEntry, NutzappedRecord, Transactions, WalletData, baseWalletSchema, nutzappedEntry, transactionsSchema } from 'soapbox/schemas/wallet.ts';
|
||||||
|
import toast from 'soapbox/toast.tsx';
|
||||||
|
|
||||||
|
import type { Account as AccountEntity, Status as StatusEntity } from 'soapbox/types/entities.ts';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
createWallet: { id: 'wallet.create.success', defaultMessage: 'Wallet created successfully!' },
|
||||||
|
success: { id: 'wallet.success', defaultMessage: 'Data updated with success!' },
|
||||||
|
error: { id: 'wallet.error', defaultMessage: 'Failed to update.' },
|
||||||
|
});
|
||||||
|
|
||||||
|
interface WalletState {
|
||||||
|
wallet: WalletData | null;
|
||||||
|
transactions: Transactions | null;
|
||||||
|
zapCashuList: string[];
|
||||||
|
nutzappedRecord: NutzappedRecord;
|
||||||
|
prevTransaction?: string | null;
|
||||||
|
nextTransaction?: string | null;
|
||||||
|
prevZaps?: string | null;
|
||||||
|
nextZaps?: string | null;
|
||||||
|
hasFetchedWallet: boolean;
|
||||||
|
hasFetchedTransactions: boolean;
|
||||||
|
|
||||||
|
setNutzappedRecord: (statusId: string, nutzappedEntry: NutzappedEntry, prevZaps?: string | null, nextZaps?: string | null) => void;
|
||||||
|
setWallet: (wallet: WalletData | null) => void;
|
||||||
|
setHasFetchedWallet: (hasFetchedWallet: boolean) => void;
|
||||||
|
setTransactions: (transactions: Transactions | null, prevTransaction?: string | null, nextTransaction?: string | null) => void;
|
||||||
|
setHasFetchedTransactions: (hasFetched: boolean) => void;
|
||||||
|
addZapCashu: (statusId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IWalletInfo {
|
||||||
|
mints: string[];
|
||||||
|
relays: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IZapCashuPayload {
|
||||||
|
account: AccountEntity;
|
||||||
|
amount: number;
|
||||||
|
comment: string;
|
||||||
|
status?: StatusEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useWalletStore = create<WalletState>((set) => ({
|
||||||
|
wallet: null,
|
||||||
|
acceptsZapsCashu: false,
|
||||||
|
transactions: null,
|
||||||
|
prevTransaction: null,
|
||||||
|
nextTransaction: null,
|
||||||
|
prevZaps: null,
|
||||||
|
nextZaps: null,
|
||||||
|
zapCashuList: [],
|
||||||
|
nutzappedRecord: {},
|
||||||
|
hasFetchedWallet: false,
|
||||||
|
hasFetchedTransactions: false,
|
||||||
|
|
||||||
|
setNutzappedRecord: (statusId, nutzappedEntry, prevZaps, nextZaps) => set((state)=> ({
|
||||||
|
nutzappedRecord: {
|
||||||
|
...state.nutzappedRecord,
|
||||||
|
[statusId]: nutzappedEntry,
|
||||||
|
},
|
||||||
|
prevZaps,
|
||||||
|
nextZaps,
|
||||||
|
})),
|
||||||
|
setWallet: (wallet) => set({ wallet }),
|
||||||
|
setHasFetchedWallet: (hasFetchedWallet) => set({ hasFetchedWallet }),
|
||||||
|
setTransactions: (transactions, prevTransaction, nextTransaction) => set({ transactions, prevTransaction, nextTransaction }),
|
||||||
|
setHasFetchedTransactions: (hasFetched) => set({ hasFetchedTransactions: hasFetched }),
|
||||||
|
addZapCashu: (statusId) =>
|
||||||
|
set((state) => ({
|
||||||
|
zapCashuList: [
|
||||||
|
...state.zapCashuList,
|
||||||
|
statusId,
|
||||||
|
],
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const useCreateWallet = () => {
|
||||||
|
const api = useApi();
|
||||||
|
const intl = useIntl();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const { setWallet } = useWalletStore();
|
||||||
|
|
||||||
|
const createWallet = useMutation({
|
||||||
|
mutationFn: async (walletInfo: IWalletInfo) => {
|
||||||
|
const response = await api.put('/api/v1/ditto/cashu/wallet', walletInfo);
|
||||||
|
const data = await response.json();
|
||||||
|
return baseWalletSchema.parse(data);
|
||||||
|
},
|
||||||
|
onSuccess: (data) => {
|
||||||
|
toast.success(intl.formatMessage(messages.createWallet));
|
||||||
|
setWallet(data);
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['wallet'] });
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast.error(error?.message || 'An error occurred while creating the wallet');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { createWallet: createWallet.mutateAsync };
|
||||||
|
};
|
||||||
|
|
||||||
|
const useWallet = () => {
|
||||||
|
const api = useApi();
|
||||||
|
const {
|
||||||
|
setWallet,
|
||||||
|
setHasFetchedWallet,
|
||||||
|
} = useWalletStore();
|
||||||
|
|
||||||
|
const getWallet = useQuery({
|
||||||
|
queryKey: ['wallet'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await api.get('/api/v1/ditto/cashu/wallet');
|
||||||
|
const data = await response.json();
|
||||||
|
const normalizedData = baseWalletSchema.parse(data);
|
||||||
|
|
||||||
|
setWallet(normalizedData);
|
||||||
|
setHasFetchedWallet(true);
|
||||||
|
|
||||||
|
return normalizedData;
|
||||||
|
},
|
||||||
|
staleTime: 1000 * 60 * 2,
|
||||||
|
retry: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
walletData: getWallet.data,
|
||||||
|
error: getWallet.error?.message,
|
||||||
|
isLoading: getWallet.isLoading,
|
||||||
|
getWallet: getWallet.refetch,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const useTransactions = () => {
|
||||||
|
const api = useApi();
|
||||||
|
const { setTransactions } = useWalletStore();
|
||||||
|
|
||||||
|
const getTransactions = useQuery({
|
||||||
|
queryKey: ['transactions'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await api.get('/api/v1/ditto/cashu/transactions');
|
||||||
|
const { prev, next: nextPag } = response.pagination();
|
||||||
|
const data = await response.json();
|
||||||
|
const normalizedData = transactionsSchema.parse(data);
|
||||||
|
|
||||||
|
setTransactions(normalizedData, prev, nextPag);
|
||||||
|
return normalizedData;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const expandTransactions = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
const { transactions, nextTransaction } = useWalletStore.getState();
|
||||||
|
|
||||||
|
if (!nextTransaction || !transactions) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await api.get(nextTransaction);
|
||||||
|
const { prev, next: nextPag } = response.pagination();
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const normalizedData = transactionsSchema.parse(data);
|
||||||
|
const newTransactions = [...transactions, ...normalizedData];
|
||||||
|
|
||||||
|
setTransactions(newTransactions, prev, nextPag);
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast.error(error?.message || 'Error expanding transactions');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
transactions: getTransactions.data,
|
||||||
|
isLoading: getTransactions.isLoading,
|
||||||
|
error: getTransactions.error,
|
||||||
|
refetch: getTransactions.refetch,
|
||||||
|
expandTransactions: expandTransactions.mutateAsync,
|
||||||
|
isExpanding: expandTransactions.isPending,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const useZapCashuRequest = () => {
|
||||||
|
const api = useApi();
|
||||||
|
const { addZapCashu } = useWalletStore();
|
||||||
|
const { refetch } = useTransactions();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const zapCashuRequest = useMutation({
|
||||||
|
mutationFn: async ({ account, amount, comment, status }: IZapCashuPayload) => {
|
||||||
|
await api.post('/api/v1/ditto/cashu/nutzap', {
|
||||||
|
amount,
|
||||||
|
comment,
|
||||||
|
account_id: account.id,
|
||||||
|
status_id: status?.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (status) {
|
||||||
|
addZapCashu(status.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess: async () =>{
|
||||||
|
toast.success('Sats sent successfully!');
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['wallet'] });
|
||||||
|
await refetch();
|
||||||
|
},
|
||||||
|
onError: (err: unknown) => {
|
||||||
|
const messageError = err instanceof Error ? err.message : 'An unexpected error occurred';
|
||||||
|
toast.error(messageError);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { zapCashu: zapCashuRequest.mutateAsync, isLoading: zapCashuRequest.isPending };
|
||||||
|
};
|
||||||
|
|
||||||
|
const useZappedByCashu = (statusId: string) => {
|
||||||
|
const api = useApi();
|
||||||
|
const { nextZaps, nutzappedRecord, setNutzappedRecord } = useWalletStore();
|
||||||
|
|
||||||
|
const getNutzappedBy = useQuery({
|
||||||
|
queryKey: ['nutzappedBy', statusId],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await api.get(`/api/v1/ditto/cashu/statuses/${statusId}/nutzapped_by`);
|
||||||
|
const { prev, next } = response.pagination();
|
||||||
|
const data = await response.json();
|
||||||
|
const normalizedData = nutzappedEntry.parse(data);
|
||||||
|
setNutzappedRecord(statusId, normalizedData, prev, next);
|
||||||
|
return normalizedData;
|
||||||
|
},
|
||||||
|
enabled: !!statusId,
|
||||||
|
retry: false,
|
||||||
|
staleTime: 1000 * 60 * 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
const expandNutzappedBy = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
if (!nextZaps || !nutzappedRecord[statusId]) {
|
||||||
|
throw new Error('No more zaps to load');
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await api.get(nextZaps);
|
||||||
|
const { prev, next } = response.pagination();
|
||||||
|
const data = await response.json();
|
||||||
|
const normalizedData = nutzappedEntry.parse(data);
|
||||||
|
const newNutzappedBy = [...(nutzappedRecord[statusId] ?? []), ...normalizedData];
|
||||||
|
setNutzappedRecord(statusId, newNutzappedBy, prev, next);
|
||||||
|
return newNutzappedBy;
|
||||||
|
},
|
||||||
|
onError: (err: unknown) => {
|
||||||
|
const messageError = err instanceof Error ? err.message : 'Error expanding zaps';
|
||||||
|
toast.error(messageError);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
error: getNutzappedBy.error,
|
||||||
|
isLoading: getNutzappedBy.isLoading || expandNutzappedBy.isPending,
|
||||||
|
getNutzappedBy: getNutzappedBy.refetch,
|
||||||
|
expandNutzappedBy: expandNutzappedBy.mutateAsync,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const useUpdateWallet = () => {
|
||||||
|
const api = useApi();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const updateWallet = useMutation({
|
||||||
|
mutationFn: async ({ mints, relays }: IWalletInfo) => {
|
||||||
|
await api.put('/api/v1/ditto/cashu/wallet', { mints: mints, relays: relays });
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['wallet'] });
|
||||||
|
toast.success(intl.formatMessage(messages.success));
|
||||||
|
},
|
||||||
|
onError: (err: Error) => {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : intl.formatMessage(messages.error);
|
||||||
|
toast.error(errorMessage);
|
||||||
|
console.error(err);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateWallet: updateWallet.mutateAsync,
|
||||||
|
isSuccess: updateWallet.isSuccess,
|
||||||
|
error: updateWallet.error,
|
||||||
|
isLoading: updateWallet.isPending,
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export { useWalletStore, useWallet, useCreateWallet, useTransactions, useZapCashuRequest, useZappedByCashu, useUpdateWallet };
|
|
@ -2,6 +2,7 @@ import moreIcon from '@tabler/icons/outline/dots-circle-horizontal.svg';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
|
||||||
import List, { ListItem } from 'soapbox/components/list.tsx';
|
import List, { ListItem } from 'soapbox/components/list.tsx';
|
||||||
import Button from 'soapbox/components/ui/button.tsx';
|
import Button from 'soapbox/components/ui/button.tsx';
|
||||||
import { Card, CardBody, CardHeader, CardTitle } from 'soapbox/components/ui/card.tsx';
|
import { Card, CardBody, CardHeader, CardTitle } from 'soapbox/components/ui/card.tsx';
|
||||||
|
@ -13,7 +14,7 @@ import { SelectDropdown } from 'soapbox/features/forms/index.tsx';
|
||||||
import Balance from 'soapbox/features/wallet/components/balance.tsx';
|
import Balance from 'soapbox/features/wallet/components/balance.tsx';
|
||||||
import CreateWallet from 'soapbox/features/wallet/components/create-wallet.tsx';
|
import CreateWallet from 'soapbox/features/wallet/components/create-wallet.tsx';
|
||||||
import Transactions from 'soapbox/features/wallet/components/transactions.tsx';
|
import Transactions from 'soapbox/features/wallet/components/transactions.tsx';
|
||||||
import { useTransactions, useWallet } from 'soapbox/features/zap/hooks/useHooks.ts';
|
import { useTransactions, useWallet } from 'soapbox/features/wallet/hooks/useHooks.ts';
|
||||||
import { usePaymentMethod } from 'soapbox/features/zap/usePaymentMethod.ts';
|
import { usePaymentMethod } from 'soapbox/features/zap/usePaymentMethod.ts';
|
||||||
import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts';
|
import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts';
|
||||||
|
|
||||||
|
@ -46,7 +47,7 @@ const Wallet = () => {
|
||||||
const [retryTimer, setRetryTimer] = useState<NodeJS.Timeout | null>(null);
|
const [retryTimer, setRetryTimer] = useState<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
const { account } = useOwnAccount();
|
const { account } = useOwnAccount();
|
||||||
const { wallet: walletData, isLoading, error, getWallet } = useWallet();
|
const { walletData, getWallet, isLoading, error } = useWallet();
|
||||||
const { method, changeMethod } = usePaymentMethod();
|
const { method, changeMethod } = usePaymentMethod();
|
||||||
const { transactions } = useTransactions();
|
const { transactions } = useTransactions();
|
||||||
const hasTransactions = transactions && transactions.length > 0;
|
const hasTransactions = transactions && transactions.length > 0;
|
||||||
|
@ -60,7 +61,7 @@ const Wallet = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsRetrying(false);
|
setIsRetrying(false);
|
||||||
getWallet(true); // Trigger wallet reload with error messages
|
getWallet(); // Trigger wallet reload with error messages
|
||||||
};
|
};
|
||||||
|
|
||||||
// Setup automatic retry when there's an error
|
// Setup automatic retry when there's an error
|
||||||
|
|
|
@ -22,7 +22,7 @@ import Input from 'soapbox/components/ui/input.tsx';
|
||||||
import Stack from 'soapbox/components/ui/stack.tsx';
|
import Stack from 'soapbox/components/ui/stack.tsx';
|
||||||
import SvgIcon from 'soapbox/components/ui/svg-icon.tsx';
|
import SvgIcon from 'soapbox/components/ui/svg-icon.tsx';
|
||||||
import Text from 'soapbox/components/ui/text.tsx';
|
import Text from 'soapbox/components/ui/text.tsx';
|
||||||
import { useZapCashuRequest } from 'soapbox/features/zap/hooks/useHooks.ts';
|
import { useZapCashuRequest } from 'soapbox/features/wallet/hooks/useHooks.ts';
|
||||||
import { usePaymentMethod } from 'soapbox/features/zap/usePaymentMethod.ts';
|
import { usePaymentMethod } from 'soapbox/features/zap/usePaymentMethod.ts';
|
||||||
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
||||||
import { emojifyText } from 'soapbox/utils/emojify.tsx';
|
import { emojifyText } from 'soapbox/utils/emojify.tsx';
|
||||||
|
@ -46,23 +46,25 @@ interface IPayRequestForm {
|
||||||
}
|
}
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
zap_button_rounded: { id: 'zap.button.text.rounded', defaultMessage: 'Zap {amount}K sats' },
|
loading: { id: 'loading_indicator.label', defaultMessage: 'Loading…' },
|
||||||
zap_button: { id: 'payment_method.button.text.raw', defaultMessage: 'Zap {amount} sats' },
|
button_rounded: { id: 'zap.button.text.rounded', defaultMessage: 'Zap {amount}K sats' },
|
||||||
zap_commentPlaceholder: { id: 'payment_method.comment_input.placeholder', defaultMessage: 'Optional comment' },
|
button: { id: 'payment_method.button.text.raw', defaultMessage: 'Zap {amount} sats' },
|
||||||
|
commentPlaceholder: { id: 'payment_method.comment_input.placeholder', defaultMessage: 'Optional comment' },
|
||||||
|
zapSats: { id: 'payment_method.button.zap_sats', defaultMessage: 'Zap sats' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const PayRequestForm = ({ account, status, onClose }: IPayRequestForm) => {
|
const PayRequestForm = ({ account, status, onClose }: IPayRequestForm) => {
|
||||||
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const [zapComment, setZapComment] = useState('');
|
const [comment, setComment] = useState('');
|
||||||
const [amount, setAmount] = useState(50);
|
const [amount, setAmount] = useState(50);
|
||||||
const { zapArrays, zapSplitData, receiveAmount } = useZapSplit(status, account);
|
const { zapArrays, zapSplitData, receiveAmount } = useZapSplit(status, account);
|
||||||
const splitValues = zapSplitData.splitValues;
|
const splitValues = zapSplitData.splitValues;
|
||||||
const { method: paymentMethod, changeMethod } = usePaymentMethod();
|
const { method: paymentMethod, changeMethod } = usePaymentMethod();
|
||||||
const isCashu = paymentMethod === 'cashu';
|
const isCashu = paymentMethod === 'cashu';
|
||||||
const hasZapSplit = zapArrays.length > 0 && !isCashu;
|
const hasZapSplit = zapArrays.length > 0 && !isCashu;
|
||||||
const { zapCashuRequest } = useZapCashuRequest();
|
const { zapCashu, isLoading } = useZapCashuRequest();
|
||||||
|
|
||||||
const handleSubmit = async (e?: React.FormEvent<Element>) => {
|
const handleSubmit = async (e?: React.FormEvent<Element>) => {
|
||||||
e?.preventDefault();
|
e?.preventDefault();
|
||||||
|
@ -70,14 +72,14 @@ const PayRequestForm = ({ account, status, onClose }: IPayRequestForm) => {
|
||||||
const splitData = { hasZapSplit, zapSplitAccounts, splitValues };
|
const splitData = { hasZapSplit, zapSplitAccounts, splitValues };
|
||||||
|
|
||||||
if (isCashu) {
|
if (isCashu) {
|
||||||
await zapCashuRequest(account, amount, zapComment, status);
|
await zapCashu({ account, amount, comment, status });
|
||||||
dispatch(closeModal('PAY_REQUEST'));
|
dispatch(closeModal('PAY_REQUEST'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const invoice = hasZapSplit
|
const invoice = hasZapSplit
|
||||||
? await dispatch(zap(account, status, zapSplitData.receiveAmount * 1000, zapComment))
|
? await dispatch(zap(account, status, zapSplitData.receiveAmount * 1000, comment))
|
||||||
: await dispatch(zap(account, status, amount * 1000, zapComment));
|
: await dispatch(zap(account, status, amount * 1000, comment));
|
||||||
|
|
||||||
if (!invoice) {
|
if (!invoice) {
|
||||||
dispatch(closeModal('PAY_REQUEST'));
|
dispatch(closeModal('PAY_REQUEST'));
|
||||||
|
@ -105,10 +107,12 @@ const PayRequestForm = ({ account, status, onClose }: IPayRequestForm) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderPaymentButtonText = () => {
|
const renderPaymentButtonText = () => {
|
||||||
|
if (isLoading) return intl.formatMessage(messages.loading);
|
||||||
|
|
||||||
if (amount >= 1000) {
|
if (amount >= 1000) {
|
||||||
return intl.formatMessage(messages.zap_button_rounded, { amount: Math.round((amount / 1000) * 10) / 10 });
|
return intl.formatMessage(messages.button_rounded, { amount: Math.round((amount / 1000) * 10) / 10 });
|
||||||
}
|
}
|
||||||
return intl.formatMessage(messages.zap_button, { amount: amount });
|
return intl.formatMessage(messages.button, { amount: amount });
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -212,12 +216,12 @@ const PayRequestForm = ({ account, status, onClose }: IPayRequestForm) => {
|
||||||
</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 => setComment(e.target.value)} type='text' placeholder={intl.formatMessage(messages.commentPlaceholder)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasZapSplit ? <Stack space={2}>
|
{hasZapSplit ? <Stack space={2}>
|
||||||
|
|
||||||
<Button className='m-auto w-auto' type='submit' theme='primary' icon={boltIcon} text={intl.formatMessage({ id: 'payment_method.button.zap_sats', defaultMessage: 'Zap sats' })} disabled={amount < 1 ? true : false} />
|
<Button className='m-auto w-auto' type='submit' theme='primary' icon={boltIcon} text={isLoading ? intl.formatMessage(messages.loading) : intl.formatMessage(messages.zapSats)} disabled={(amount < 1 ? true : false) || isLoading} />
|
||||||
|
|
||||||
<div className='flex items-center justify-center gap-2 sm:gap-4'>
|
<div className='flex items-center justify-center gap-2 sm:gap-4'>
|
||||||
<span className='text-[10px] sm:text-xs'>
|
<span className='text-[10px] sm:text-xs'>
|
||||||
|
@ -233,7 +237,7 @@ const PayRequestForm = ({ account, status, onClose }: IPayRequestForm) => {
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</Stack> : <Button className='m-auto w-auto' type='submit' theme='primary' icon={boltIcon} text={renderPaymentButtonText()} disabled={amount < 1 ? true : false} />}
|
</Stack> : <Button className='m-auto w-auto' type='submit' theme='primary' icon={isLoading ? '' : boltIcon} text={renderPaymentButtonText()} disabled={(amount < 1 ? true : false) || isLoading} />}
|
||||||
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,279 +0,0 @@
|
||||||
import { debounce } from 'es-toolkit';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { create } from 'zustand';
|
|
||||||
|
|
||||||
import { useApi } from 'soapbox/hooks/useApi.ts';
|
|
||||||
import { NutzappedEntry, NutzappedRecord, Transactions, WalletData, baseWalletSchema, nutzappedEntry, transactionsSchema } from 'soapbox/schemas/wallet.ts';
|
|
||||||
import toast from 'soapbox/toast.tsx';
|
|
||||||
|
|
||||||
import type { Account as AccountEntity, Status as StatusEntity } from 'soapbox/types/entities.ts';
|
|
||||||
|
|
||||||
interface WalletState {
|
|
||||||
wallet: WalletData | null;
|
|
||||||
acceptsZapsCashu: boolean;
|
|
||||||
transactions: Transactions | null;
|
|
||||||
zapCashuList: string[];
|
|
||||||
nutzappedRecord: NutzappedRecord;
|
|
||||||
prevTransaction?: string | null;
|
|
||||||
nextTransaction?: string | null;
|
|
||||||
prevZaps?: string | null;
|
|
||||||
nextZaps?: string | null;
|
|
||||||
hasFetchedWallet: boolean;
|
|
||||||
hasFetchedTransactions: boolean;
|
|
||||||
|
|
||||||
setNutzappedRecord: (statusId: string, nutzappedEntry: NutzappedEntry, prevZaps?: string | null, nextZaps?: string | null) => void;
|
|
||||||
setAcceptsZapsCashu: (acceptsZapsCashu: boolean) => void;
|
|
||||||
setWallet: (wallet: WalletData | null) => void;
|
|
||||||
setHasFetchedWallet: (hasFetchedWallet: boolean) => void;
|
|
||||||
setTransactions: (transactions: Transactions | null, prevTransaction?: string | null, nextTransaction?: string | null) => void;
|
|
||||||
setHasFetchedTransactions: (hasFetched: boolean) => void;
|
|
||||||
addZapCashu: (statusId: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IWalletInfo {
|
|
||||||
mints: string[];
|
|
||||||
relays: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const useWalletStore = create<WalletState>((set) => ({
|
|
||||||
wallet: null,
|
|
||||||
acceptsZapsCashu: false,
|
|
||||||
transactions: null,
|
|
||||||
prevTransaction: null,
|
|
||||||
nextTransaction: null,
|
|
||||||
prevZaps: null,
|
|
||||||
nextZaps: null,
|
|
||||||
zapCashuList: [],
|
|
||||||
nutzappedRecord: {},
|
|
||||||
hasFetchedWallet: false,
|
|
||||||
hasFetchedTransactions: false,
|
|
||||||
|
|
||||||
setNutzappedRecord: (statusId, nutzappedEntry, prevZaps, nextZaps) => set((state)=> ({
|
|
||||||
nutzappedRecord: {
|
|
||||||
...state.nutzappedRecord,
|
|
||||||
[statusId]: nutzappedEntry,
|
|
||||||
},
|
|
||||||
prevZaps,
|
|
||||||
nextZaps,
|
|
||||||
})),
|
|
||||||
setAcceptsZapsCashu: (acceptsZapsCashu) => set({ acceptsZapsCashu }),
|
|
||||||
setWallet: (wallet) => set({ wallet }),
|
|
||||||
setHasFetchedWallet: (hasFetchedWallet) => set({ hasFetchedWallet }),
|
|
||||||
setTransactions: (transactions, prevTransaction, nextTransaction) => set({ transactions, prevTransaction, nextTransaction }),
|
|
||||||
setHasFetchedTransactions: (hasFetched) => set({ hasFetchedTransactions: hasFetched }),
|
|
||||||
addZapCashu: (statusId) =>
|
|
||||||
set((state) => ({
|
|
||||||
zapCashuList: [
|
|
||||||
...state.zapCashuList,
|
|
||||||
statusId,
|
|
||||||
],
|
|
||||||
})),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const useWallet = () => {
|
|
||||||
const api = useApi();
|
|
||||||
const { wallet, setWallet, setAcceptsZapsCashu, hasFetchedWallet, setHasFetchedWallet } = useWalletStore();
|
|
||||||
const [isLoading, setIsLoading] = useState(!hasFetchedWallet);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const createWallet = async (walletInfo: IWalletInfo) => {
|
|
||||||
setIsLoading(true);
|
|
||||||
try {
|
|
||||||
const response = await api.put('/api/v1/ditto/cashu/wallet', walletInfo);
|
|
||||||
const data = await response.json();
|
|
||||||
if (data) {
|
|
||||||
const normalizedData = baseWalletSchema.parse(data);
|
|
||||||
toast.success('Wallet created successfully');
|
|
||||||
setWallet(normalizedData);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
const messageError = err instanceof Error ? err.message : 'An error has occurred';
|
|
||||||
setError(messageError);
|
|
||||||
toast.error(messageError);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getWallet = async (hasMessage = true) => {
|
|
||||||
setIsLoading(true);
|
|
||||||
try {
|
|
||||||
const response = await api.get('/api/v1/ditto/cashu/wallet');
|
|
||||||
const data = await response.json();
|
|
||||||
if (data) {
|
|
||||||
const normalizedData = baseWalletSchema.parse(data);
|
|
||||||
setAcceptsZapsCashu(true);
|
|
||||||
|
|
||||||
setWallet(normalizedData);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
const messageError = err instanceof Error ? err.message : 'Wallet not found';
|
|
||||||
if (hasMessage) toast.error(messageError);
|
|
||||||
setError(messageError);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
setHasFetchedWallet(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!hasFetchedWallet) {
|
|
||||||
getWallet(false);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return { wallet, isLoading, error, createWallet, getWallet };
|
|
||||||
};
|
|
||||||
|
|
||||||
const useTransactions = () => {
|
|
||||||
const api = useApi();
|
|
||||||
const { transactions, nextTransaction, setTransactions, hasFetchedTransactions, setHasFetchedTransactions } = useWalletStore();
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const getTransactions = async () => {
|
|
||||||
setIsLoading(true);
|
|
||||||
try {
|
|
||||||
const response = await api.get('/api/v1/ditto/cashu/transactions');
|
|
||||||
const { prev, next } = response.pagination();
|
|
||||||
const data = await response.json();
|
|
||||||
if (data) {
|
|
||||||
const normalizedData = transactionsSchema.parse(data);
|
|
||||||
setTransactions(normalizedData, prev, next);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
const messageError = err instanceof Error ? err.message : 'Transactions not found';
|
|
||||||
toast.error(messageError);
|
|
||||||
setError(messageError);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const expandTransactions = async () => {
|
|
||||||
if (!nextTransaction || !transactions) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
setIsLoading(true);
|
|
||||||
const response = await api.get(nextTransaction);
|
|
||||||
const { prev, next } = response.pagination();
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
const normalizedData = transactionsSchema.parse(data);
|
|
||||||
const newTransactions = [...(transactions ?? []), ...normalizedData ];
|
|
||||||
|
|
||||||
setTransactions(newTransactions, prev, next);
|
|
||||||
return true;
|
|
||||||
} catch (err) {
|
|
||||||
const messageError = err instanceof Error ? err.message : 'Error expanding transactions';
|
|
||||||
toast.error(messageError);
|
|
||||||
setError(messageError);
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!hasFetchedTransactions) {
|
|
||||||
setHasFetchedTransactions(true);
|
|
||||||
getTransactions();
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return { transactions, isLoading, error, getTransactions, expandTransactions };
|
|
||||||
};
|
|
||||||
|
|
||||||
const useZapCashuRequest = () => {
|
|
||||||
const api = useApi();
|
|
||||||
const { zapCashuList, addZapCashu } = useWalletStore();
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const { getWallet } = useWallet();
|
|
||||||
const { getTransactions } = useTransactions();
|
|
||||||
|
|
||||||
const zapCashuRequest = async (account: AccountEntity, amount: number, comment: string, status?: StatusEntity) => {
|
|
||||||
setIsLoading(true);
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
await api.post('/api/v1/ditto/cashu/nutzap', {
|
|
||||||
amount,
|
|
||||||
comment,
|
|
||||||
account_id: account.id,
|
|
||||||
status_id: status?.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (status) {
|
|
||||||
addZapCashu(status.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.success('Sats sent successfully!');
|
|
||||||
getWallet();
|
|
||||||
getTransactions();
|
|
||||||
} catch (err) {
|
|
||||||
const messageError = err instanceof Error ? err.message : 'An unexpected error occurred';
|
|
||||||
setError(messageError);
|
|
||||||
toast.error('An unexpected error occurred');
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return { zapCashuList, isLoading, error, zapCashuRequest };
|
|
||||||
};
|
|
||||||
|
|
||||||
const useZappedByCashu = () => {
|
|
||||||
const api = useApi();
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const { nextZaps, nutzappedRecord, setNutzappedRecord } = useWalletStore();
|
|
||||||
|
|
||||||
const getNutzappedBy = async (statusId: string) => {
|
|
||||||
setIsLoading(true);
|
|
||||||
try {
|
|
||||||
const response = await api.get(`/api/v1/ditto/cashu/statuses/${statusId}/nutzapped_by`);
|
|
||||||
const { prev, next } = response.pagination();
|
|
||||||
const data = await response.json();
|
|
||||||
if (data) {
|
|
||||||
const normalizedData = nutzappedEntry.parse(data);
|
|
||||||
setNutzappedRecord(statusId, normalizedData, prev, next);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
const messageError = err instanceof Error ? err.message : 'Zaps not found';
|
|
||||||
toast.error('Zaps not foud');
|
|
||||||
setError(messageError);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const expandNutzappedBy = debounce(async (id: string) => {
|
|
||||||
if (!nextZaps || !nutzappedRecord[id]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setIsLoading(true);
|
|
||||||
const response = await api.get(nextZaps);
|
|
||||||
const { prev, next } = response.pagination();
|
|
||||||
const data = await response.json();
|
|
||||||
if (data) {
|
|
||||||
const normalizedData = nutzappedEntry.parse(data);
|
|
||||||
const newNutzappedBy = [...(nutzappedRecord[id] ?? []), ...normalizedData ];
|
|
||||||
|
|
||||||
setNutzappedRecord(id, newNutzappedBy, prev, next);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
const messageError = err instanceof Error ? err.message : 'Error expanding transactions';
|
|
||||||
toast.error(messageError);
|
|
||||||
setError(messageError);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}, 700);
|
|
||||||
|
|
||||||
return { error, isLoading, getNutzappedBy, expandNutzappedBy };
|
|
||||||
};
|
|
||||||
|
|
||||||
export { useWalletStore, useWallet, useTransactions, useZapCashuRequest, useZappedByCashu };
|
|
|
@ -1729,8 +1729,10 @@
|
||||||
"wallet.button.create_wallet": "Create",
|
"wallet.button.create_wallet": "Create",
|
||||||
"wallet.button.mint": "Mint",
|
"wallet.button.mint": "Mint",
|
||||||
"wallet.button.withdraw": "Withdraw",
|
"wallet.button.withdraw": "Withdraw",
|
||||||
"wallet.create_wallet.question": "Do you want create one?",
|
"wallet.create.question": "Do you want create one?",
|
||||||
"wallet.create_wallet.title": "You don't have a wallet",
|
"wallet.create.success": "Wallet created successfully!",
|
||||||
|
"wallet.create.title": "You don't have a wallet",
|
||||||
|
"wallet.error": "Failed to update.",
|
||||||
"wallet.hidden.balance": "••••••",
|
"wallet.hidden.balance": "••••••",
|
||||||
"wallet.invalid_url": "All strings must be valid URLs.",
|
"wallet.invalid_url": "All strings must be valid URLs.",
|
||||||
"wallet.loading": "Loading…",
|
"wallet.loading": "Loading…",
|
||||||
|
@ -1738,16 +1740,13 @@
|
||||||
"wallet.management": "Wallet Management",
|
"wallet.management": "Wallet Management",
|
||||||
"wallet.mints": "Mints",
|
"wallet.mints": "Mints",
|
||||||
"wallet.mints.empty": "At least one mint is required.",
|
"wallet.mints.empty": "At least one mint is required.",
|
||||||
"wallet.mints.error": "Failed to update mints.",
|
|
||||||
"wallet.mints.success": "Mints updated with success!",
|
|
||||||
"wallet.payment": "Payment Method",
|
"wallet.payment": "Payment Method",
|
||||||
"wallet.relays": "Wallet Relays",
|
"wallet.relays": "Wallet Relays",
|
||||||
"wallet.relays.empty": "At least one relay is required.",
|
"wallet.relays.empty": "At least one relay is required.",
|
||||||
"wallet.relays.error": "Failed to update relays.",
|
|
||||||
"wallet.relays.success": "Relays updated with success!",
|
|
||||||
"wallet.retry": "Retry Now",
|
"wallet.retry": "Retry Now",
|
||||||
"wallet.retrying": "Retrying in {seconds}s…",
|
"wallet.retrying": "Retrying in {seconds}s…",
|
||||||
"wallet.sats": "{amount} sats",
|
"wallet.sats": "{amount} sats",
|
||||||
|
"wallet.success": "Data updated with success!",
|
||||||
"wallet.title": "Wallet",
|
"wallet.title": "Wallet",
|
||||||
"wallet.transactions": "Transactions",
|
"wallet.transactions": "Transactions",
|
||||||
"wallet.transactions.more": "More",
|
"wallet.transactions.more": "More",
|
||||||
|
|
|
@ -9,9 +9,9 @@ import {
|
||||||
CtaBanner,
|
CtaBanner,
|
||||||
PocketWallet,
|
PocketWallet,
|
||||||
} from 'soapbox/features/ui/util/async-components.ts';
|
} from 'soapbox/features/ui/util/async-components.ts';
|
||||||
import { useWallet } from 'soapbox/features/zap/hooks/useHooks.ts';
|
|
||||||
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
||||||
import { useFeatures } from 'soapbox/hooks/useFeatures.ts';
|
import { useFeatures } from 'soapbox/hooks/useFeatures.ts';
|
||||||
|
import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts';
|
||||||
|
|
||||||
interface IDefaultPage {
|
interface IDefaultPage {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
@ -20,9 +20,9 @@ interface IDefaultPage {
|
||||||
const DefaultPage: React.FC<IDefaultPage> = ({ children }) => {
|
const DefaultPage: React.FC<IDefaultPage> = ({ children }) => {
|
||||||
const me = useAppSelector(state => state.me);
|
const me = useAppSelector(state => state.me);
|
||||||
const features = useFeatures();
|
const features = useFeatures();
|
||||||
const { wallet } = useWallet();
|
const { account } = useOwnAccount();
|
||||||
const path = useLocation().pathname;
|
const path = useLocation().pathname;
|
||||||
const hasPocketWallet = wallet && path !== '/wallet';
|
const hasPocketWallet = account?.ditto.accepts_zaps_cashu && path !== '/wallet';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -38,7 +38,7 @@ const DefaultPage: React.FC<IDefaultPage> = ({ children }) => {
|
||||||
{!me && (
|
{!me && (
|
||||||
<SignUpPanel />
|
<SignUpPanel />
|
||||||
)}
|
)}
|
||||||
{hasPocketWallet && (
|
{me && features.nostr && hasPocketWallet && (
|
||||||
<PocketWallet />
|
<PocketWallet />
|
||||||
)}
|
)}
|
||||||
{features.trends && (
|
{features.trends && (
|
||||||
|
|
|
@ -59,8 +59,9 @@ const HomePage: React.FC<IHomePage> = ({ children }) => {
|
||||||
dispatch(uploadCompose(composeId, files, intl));
|
dispatch(uploadCompose(composeId, files, intl));
|
||||||
});
|
});
|
||||||
|
|
||||||
const acct = account ? account.acct : '';
|
const acct = account?.acct ?? '';
|
||||||
const avatar = account ? account.avatar : '';
|
const avatar = account?.avatar ?? '';
|
||||||
|
const hasWallet = account?.ditto.accepts_zaps_cashu ?? false;
|
||||||
|
|
||||||
const renderSuggestions = () => {
|
const renderSuggestions = () => {
|
||||||
if (features.suggestionsLocal && !isGlobalPage) {
|
if (features.suggestionsLocal && !isGlobalPage) {
|
||||||
|
@ -113,7 +114,7 @@ const HomePage: React.FC<IHomePage> = ({ children }) => {
|
||||||
{!me && (
|
{!me && (
|
||||||
<SignUpPanel />
|
<SignUpPanel />
|
||||||
)}
|
)}
|
||||||
{me && features.nostr && (
|
{me && features.nostr && hasWallet && (
|
||||||
<PocketWallet />
|
<PocketWallet />
|
||||||
)}
|
)}
|
||||||
{me && features.announcements && (
|
{me && features.announcements && (
|
||||||
|
|
|
@ -19,7 +19,6 @@ import {
|
||||||
AccountNotePanel,
|
AccountNotePanel,
|
||||||
PocketWallet,
|
PocketWallet,
|
||||||
} from 'soapbox/features/ui/util/async-components.ts';
|
} from 'soapbox/features/ui/util/async-components.ts';
|
||||||
import { useWallet } from 'soapbox/features/zap/hooks/useHooks.ts';
|
|
||||||
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
||||||
import { useFeatures } from 'soapbox/hooks/useFeatures.ts';
|
import { useFeatures } from 'soapbox/hooks/useFeatures.ts';
|
||||||
import { useSoapboxConfig } from 'soapbox/hooks/useSoapboxConfig.ts';
|
import { useSoapboxConfig } from 'soapbox/hooks/useSoapboxConfig.ts';
|
||||||
|
@ -42,7 +41,7 @@ const ProfilePage: React.FC<IProfilePage> = ({ params, children }) => {
|
||||||
const me = useAppSelector(state => state.me);
|
const me = useAppSelector(state => state.me);
|
||||||
const features = useFeatures();
|
const features = useFeatures();
|
||||||
const { displayFqn } = useSoapboxConfig();
|
const { displayFqn } = useSoapboxConfig();
|
||||||
const { wallet } = useWallet();
|
const hasWallet = account?.ditto.accepts_zaps_cashu ?? false;
|
||||||
|
|
||||||
// Fix case of username
|
// Fix case of username
|
||||||
if (account && account.acct !== username) {
|
if (account && account.acct !== username) {
|
||||||
|
@ -121,7 +120,7 @@ const ProfilePage: React.FC<IProfilePage> = ({ params, children }) => {
|
||||||
<SignUpPanel />
|
<SignUpPanel />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{wallet && (
|
{me && features.nostr && hasWallet && (
|
||||||
<PocketWallet />
|
<PocketWallet />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
@ -11,9 +11,9 @@ import {
|
||||||
SuggestedGroupsPanel,
|
SuggestedGroupsPanel,
|
||||||
PocketWallet,
|
PocketWallet,
|
||||||
} from 'soapbox/features/ui/util/async-components.ts';
|
} from 'soapbox/features/ui/util/async-components.ts';
|
||||||
import { useWallet } from 'soapbox/features/zap/hooks/useHooks.ts';
|
|
||||||
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
||||||
import { useFeatures } from 'soapbox/hooks/useFeatures.ts';
|
import { useFeatures } from 'soapbox/hooks/useFeatures.ts';
|
||||||
|
import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts';
|
||||||
|
|
||||||
interface IExplorePage {
|
interface IExplorePage {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
@ -23,7 +23,8 @@ const ExplorePage: React.FC<IExplorePage> = ({ children }) => {
|
||||||
const me = useAppSelector(state => state.me);
|
const me = useAppSelector(state => state.me);
|
||||||
const features = useFeatures();
|
const features = useFeatures();
|
||||||
const accountsPath = useLocation().pathname === '/explore/accounts';
|
const accountsPath = useLocation().pathname === '/explore/accounts';
|
||||||
const { wallet } = useWallet();
|
const { account } = useOwnAccount();
|
||||||
|
const hasWallet = account?.ditto.accepts_zaps_cashu ?? false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -40,7 +41,7 @@ const ExplorePage: React.FC<IExplorePage> = ({ children }) => {
|
||||||
<SignUpPanel />
|
<SignUpPanel />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{wallet && (
|
{hasWallet && (
|
||||||
<PocketWallet />
|
<PocketWallet />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
@ -5,9 +5,11 @@ import {
|
||||||
TrendsPanel,
|
TrendsPanel,
|
||||||
SignUpPanel,
|
SignUpPanel,
|
||||||
CtaBanner,
|
CtaBanner,
|
||||||
|
PocketWallet,
|
||||||
} from 'soapbox/features/ui/util/async-components.ts';
|
} from 'soapbox/features/ui/util/async-components.ts';
|
||||||
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
||||||
import { useFeatures } from 'soapbox/hooks/useFeatures.ts';
|
import { useFeatures } from 'soapbox/hooks/useFeatures.ts';
|
||||||
|
import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts';
|
||||||
|
|
||||||
interface IStatusPage {
|
interface IStatusPage {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
@ -16,6 +18,8 @@ interface IStatusPage {
|
||||||
const StatusPage: React.FC<IStatusPage> = ({ children }) => {
|
const StatusPage: React.FC<IStatusPage> = ({ children }) => {
|
||||||
const me = useAppSelector(state => state.me);
|
const me = useAppSelector(state => state.me);
|
||||||
const features = useFeatures();
|
const features = useFeatures();
|
||||||
|
const { account } = useOwnAccount();
|
||||||
|
const hasPocketWallet = account?.ditto.accepts_zaps_cashu;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -31,6 +35,9 @@ const StatusPage: React.FC<IStatusPage> = ({ children }) => {
|
||||||
{!me && (
|
{!me && (
|
||||||
<SignUpPanel />
|
<SignUpPanel />
|
||||||
)}
|
)}
|
||||||
|
{me && features.nostr && hasPocketWallet && (
|
||||||
|
<PocketWallet />
|
||||||
|
)}
|
||||||
{features.trends && (
|
{features.trends && (
|
||||||
<TrendsPanel limit={5} />
|
<TrendsPanel limit={5} />
|
||||||
)}
|
)}
|
||||||
|
|
Ładowanie…
Reference in New Issue