From e6bb36103fcd7a4e732af1ea3f3ef1ce9f0599b3 Mon Sep 17 00:00:00 2001 From: danidfra Date: Wed, 12 Feb 2025 18:11:04 -0300 Subject: [PATCH 01/62] Create path to wallet --- src/components/sidebar-navigation.tsx | 8 ++++++++ src/features/ui/index.tsx | 2 ++ src/features/ui/util/async-components.ts | 1 + src/locales/en.json | 2 ++ 4 files changed, 13 insertions(+) diff --git a/src/components/sidebar-navigation.tsx b/src/components/sidebar-navigation.tsx index 9f8641b56..a8097481d 100644 --- a/src/components/sidebar-navigation.tsx +++ b/src/components/sidebar-navigation.tsx @@ -19,6 +19,7 @@ import searchIcon from '@tabler/icons/outline/search.svg'; import settingsIcon from '@tabler/icons/outline/settings.svg'; import userPlusIcon from '@tabler/icons/outline/user-plus.svg'; import userIcon from '@tabler/icons/outline/user.svg'; +import walletIcon from '@tabler/icons/outline/wallet.svg'; import worldIcon from '@tabler/icons/outline/world.svg'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { Link } from 'react-router-dom'; @@ -195,6 +196,13 @@ const SidebarNavigation = () => { text={} /> + } + /> + = ({ children }) => + diff --git a/src/features/ui/util/async-components.ts b/src/features/ui/util/async-components.ts index ff6b05aca..8ed81c4e8 100644 --- a/src/features/ui/util/async-components.ts +++ b/src/features/ui/util/async-components.ts @@ -181,3 +181,4 @@ export const ZapsModal = lazy(() => import('soapbox/features/ui/components/modal export const ZapSplitModal = lazy(() => import('soapbox/features/ui/components/modals/zap-split/zap-split-modal.tsx')); export const CaptchaModal = lazy(() => import('soapbox/features/ui/components/modals/captcha-modal/captcha-modal.tsx')); export const NostrBunkerLogin = lazy(() => import('soapbox/features/nostr/nostr-bunker-login.tsx')); +export const MyWallet = lazy(() => import('soapbox/features/my-wallet/index.tsx')); diff --git a/src/locales/en.json b/src/locales/en.json index 3a531ab37..b93896f8c 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1465,6 +1465,7 @@ "settings.security": "Security", "settings.sessions": "Active sessions", "settings.settings": "Settings", + "my_wallet.my_wallet": "My Wallet", "shared.tos": "Terms of Service", "signup_panel.subtitle": "Sign up now to discuss what's happening.", "signup_panel.title": "New to {site_title}?", @@ -1616,6 +1617,7 @@ "tabs_bar.more": "More", "tabs_bar.notifications": "Notifications", "tabs_bar.profile": "Profile", + "tabs_bar.wallet": "My Wallet", "tabs_bar.search": "Discover", "tabs_bar.settings": "Settings", "textarea.counter.label": "{count} characters remaining", From 839156031fe89be02d770a3dc5d28bd5df6e1a9d Mon Sep 17 00:00:00 2001 From: danidfra Date: Thu, 13 Feb 2025 13:52:36 -0300 Subject: [PATCH 02/62] Just to save --- src/features/my-wallet/components/balance.tsx | 55 ++++++ .../my-wallet/components/create-wallet.tsx | 34 ++++ .../my-wallet/components/transactions.tsx | 166 ++++++++++++++++++ src/features/my-wallet/index.tsx | 105 +++++++++++ 4 files changed, 360 insertions(+) create mode 100644 src/features/my-wallet/components/balance.tsx create mode 100644 src/features/my-wallet/components/create-wallet.tsx create mode 100644 src/features/my-wallet/components/transactions.tsx create mode 100644 src/features/my-wallet/index.tsx diff --git a/src/features/my-wallet/components/balance.tsx b/src/features/my-wallet/components/balance.tsx new file mode 100644 index 000000000..fab0f52ae --- /dev/null +++ b/src/features/my-wallet/components/balance.tsx @@ -0,0 +1,55 @@ +// import IconButton from 'soapbox/components/ui/icon-button.tsx'; +import withddrawIcon from '@tabler/icons/outline/cash.svg'; +import editIcon from '@tabler/icons/outline/edit.svg'; +import exchangeIcon from '@tabler/icons/outline/transfer.svg'; +import { FormattedMessage } from 'react-intl'; + +import Button from 'soapbox/components/ui/button.tsx'; +import Divider from 'soapbox/components/ui/divider.tsx'; +import HStack from 'soapbox/components/ui/hstack.tsx'; +import Stack from 'soapbox/components/ui/stack.tsx'; +import SvgIcon from 'soapbox/components/ui/svg-icon.tsx'; +import Text from 'soapbox/components/ui/text.tsx'; +import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts'; + + +// const messages = defineMessages({ +// label: { id: 'settings.messages.label', defaultMessage: 'Allow users to start a new chat with you' }, +// }); + +const Balance = () => { + const { account } = useOwnAccount(); + + if (!account) { + return null; + } + + return ( + + + + + + + + + + + {/* 166,186 sats */} + + + +
+ +
+ + + + + +
+ ); +}; + +export default Transactions; \ No newline at end of file diff --git a/src/features/my-wallet/index.tsx b/src/features/my-wallet/index.tsx new file mode 100644 index 000000000..6faff9589 --- /dev/null +++ b/src/features/my-wallet/index.tsx @@ -0,0 +1,105 @@ +import { useEffect } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + +import { fetchMfa } from 'soapbox/actions/mfa.ts'; +import List, { ListItem } from 'soapbox/components/list.tsx'; +import { Card, CardBody, CardHeader, CardTitle } from 'soapbox/components/ui/card.tsx'; +import { Column } from 'soapbox/components/ui/column.tsx'; +import Balance from 'soapbox/features/my-wallet/components/balance.tsx'; +import CreateWallet from 'soapbox/features/my-wallet/components/create-wallet.tsx'; +import Transactions from 'soapbox/features/my-wallet/components/transactions.tsx'; +import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; +import { useFeatures } from 'soapbox/hooks/useFeatures.ts'; +import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts'; + + +const messages = defineMessages({ + accountAliases: { id: 'navigation_bar.account_aliases', defaultMessage: 'Account aliases' }, + accountMigration: { id: 'settings.account_migration', defaultMessage: 'Move Account' }, + backups: { id: 'column.backups', defaultMessage: 'Backups' }, + blocks: { id: 'settings.blocks', defaultMessage: 'Blocks' }, + changeEmail: { id: 'settings.change_email', defaultMessage: 'Change Email' }, + changePassword: { id: 'settings.change_password', defaultMessage: 'Change Password' }, + configureMfa: { id: 'settings.configure_mfa', defaultMessage: 'Configure MFA' }, + deleteAccount: { id: 'settings.delete_account', defaultMessage: 'Delete Account' }, + editProfile: { id: 'settings.edit_profile', defaultMessage: 'Edit Profile' }, + editIdentity: { id: 'settings.edit_identity', defaultMessage: 'Identity' }, + editRelays: { id: 'nostr_relays.title', defaultMessage: 'Relays' }, + exportData: { id: 'column.export_data', defaultMessage: 'Export data' }, + importData: { id: 'navigation_bar.import_data', defaultMessage: 'Import data' }, + mfaDisabled: { id: 'mfa.disabled', defaultMessage: 'Disabled' }, + mfaEnabled: { id: 'mfa.enabled', defaultMessage: 'Enabled' }, + mutes: { id: 'settings.mutes', defaultMessage: 'Mutes' }, + other: { id: 'settings.other', defaultMessage: 'Other Options' }, + preferences: { id: 'settings.preferences', defaultMessage: 'Preferences' }, + transactions: { id: 'my_wallet.transactions', defaultMessage: 'Transactions' }, + myWallet: { id: 'my_wallet.my_wallet', defaultMessage: 'My Wallet' }, + security: { id: 'settings.security', defaultMessage: 'Security' }, + sessions: { id: 'settings.sessions', defaultMessage: 'Active sessions' }, + settings: { id: 'settings.settings', defaultMessage: 'Settings' }, +}); + +/** User settings page. */ +const MyWallet = () => { + const dispatch = useAppDispatch(); + const intl = useIntl(); + + const features = useFeatures(); + const { account } = useOwnAccount(); + + useEffect(() => { + if (features.security) dispatch(fetchMfa()); + }, [dispatch]); + + if (!account) return null; + + const hasWallet = false; + + return ( + + + + + + + {hasWallet ? ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + ) + : + <> + + + + + + } + + + ); +}; + +export default MyWallet; \ No newline at end of file From dcf80bd8a1d124f36805fe7852b7cf9f83c67407 Mon Sep 17 00:00:00 2001 From: danidfra Date: Mon, 17 Feb 2025 22:56:10 -0300 Subject: [PATCH 03/62] Create translated texts --- src/locales/en.json | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/locales/en.json b/src/locales/en.json index 15d755e89..92fe8227a 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -438,6 +438,7 @@ "column_forbidden.body": "You do not have permission to access this page.", "column_forbidden.title": "Forbidden", "common.cancel": "Cancel", + "common.save": "Save", "compare_history_modal.header": "Edit history", "compose.character_counter.title": "Used {chars} out of {maxChars} {maxChars, plural, one {character} other {characters}}", "compose.edit_success": "Your post was edited", @@ -1109,6 +1110,16 @@ "mute_modal.duration": "Duration", "mute_modal.hide_notifications": "Hide notifications from this user?", "mutes.empty.groups": "You haven't muted any groups yet.", + "my_wallet": "My Wallet", + "my_wallet.balance.exchange_button": "Exchange", + "my_wallet.balance.withdraw_button": "Withdraw", + "my_wallet.create_wallet.button": "Create wallet", + "my_wallet.create_wallet.question": "Do you want create one?", + "my_wallet.create_wallet.title": "You don't have a wallet", + "my_wallet.management": "Wallet Management", + "my_wallet.mints": "Mints", + "my_wallet.relays": "Relays", + "my_wallet.transactions": "Transactions", "navbar.login.action": "Log in", "navbar.login.email.placeholder": "E-mail address", "navbar.login.forgot_password": "Forgot password?", @@ -1465,7 +1476,6 @@ "settings.security": "Security", "settings.sessions": "Active sessions", "settings.settings": "Settings", - "my_wallet.my_wallet": "My Wallet", "shared.tos": "Terms of Service", "signup_panel.subtitle": "Sign up now to discuss what's happening.", "signup_panel.title": "New to {site_title}?", @@ -1621,9 +1631,9 @@ "tabs_bar.more": "More", "tabs_bar.notifications": "Notifications", "tabs_bar.profile": "Profile", - "tabs_bar.wallet": "My Wallet", "tabs_bar.search": "Discover", "tabs_bar.settings": "Settings", + "tabs_bar.wallet": "My Wallet", "textarea.counter.label": "{count} characters remaining", "theme_editor.colors.accent": "Accent", "theme_editor.colors.accent_blue": "Accent Blue", From b6fd4c4421cda05b6dca2302b750938e13fe16f3 Mon Sep 17 00:00:00 2001 From: danidfra Date: Mon, 17 Feb 2025 22:57:52 -0300 Subject: [PATCH 04/62] Create WalletSchema --- src/schemas/wallet.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/schemas/wallet.ts diff --git a/src/schemas/wallet.ts b/src/schemas/wallet.ts new file mode 100644 index 000000000..9bec9b174 --- /dev/null +++ b/src/schemas/wallet.ts @@ -0,0 +1,14 @@ +import { NSchema as n } from '@nostrify/nostrify'; +import { z } from 'zod'; + +const baseWalletSchema = z.object({ + pubkey_p2pk: n.id(), + mints: z.array(z.string()), + // .nonempty() + relays: z.array(z.string().url()).nonempty(), + balance: z.number(), +}); + +type WalletData = z.infer; + +export { baseWalletSchema, type WalletData }; \ No newline at end of file From ae7bbb84e62934d9785c1e15491f276a89ee3f98 Mon Sep 17 00:00:00 2001 From: danidfra Date: Mon, 17 Feb 2025 23:03:20 -0300 Subject: [PATCH 05/62] Refactor MyWallet component and improve data fetching --- src/features/my-wallet/index.tsx | 75 +++++++++++++++----------------- 1 file changed, 35 insertions(+), 40 deletions(-) diff --git a/src/features/my-wallet/index.tsx b/src/features/my-wallet/index.tsx index 6faff9589..c005018a3 100644 --- a/src/features/my-wallet/index.tsx +++ b/src/features/my-wallet/index.tsx @@ -1,71 +1,66 @@ -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { fetchMfa } from 'soapbox/actions/mfa.ts'; import List, { ListItem } from 'soapbox/components/list.tsx'; import { Card, CardBody, CardHeader, CardTitle } from 'soapbox/components/ui/card.tsx'; import { Column } from 'soapbox/components/ui/column.tsx'; import Balance from 'soapbox/features/my-wallet/components/balance.tsx'; import CreateWallet from 'soapbox/features/my-wallet/components/create-wallet.tsx'; import Transactions from 'soapbox/features/my-wallet/components/transactions.tsx'; -import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; -import { useFeatures } from 'soapbox/hooks/useFeatures.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts'; +import { WalletData, baseWalletSchema } from 'soapbox/schemas/wallet.ts'; +import toast from 'soapbox/toast.tsx'; const messages = defineMessages({ - accountAliases: { id: 'navigation_bar.account_aliases', defaultMessage: 'Account aliases' }, - accountMigration: { id: 'settings.account_migration', defaultMessage: 'Move Account' }, - backups: { id: 'column.backups', defaultMessage: 'Backups' }, - blocks: { id: 'settings.blocks', defaultMessage: 'Blocks' }, - changeEmail: { id: 'settings.change_email', defaultMessage: 'Change Email' }, - changePassword: { id: 'settings.change_password', defaultMessage: 'Change Password' }, - configureMfa: { id: 'settings.configure_mfa', defaultMessage: 'Configure MFA' }, - deleteAccount: { id: 'settings.delete_account', defaultMessage: 'Delete Account' }, - editProfile: { id: 'settings.edit_profile', defaultMessage: 'Edit Profile' }, - editIdentity: { id: 'settings.edit_identity', defaultMessage: 'Identity' }, - editRelays: { id: 'nostr_relays.title', defaultMessage: 'Relays' }, - exportData: { id: 'column.export_data', defaultMessage: 'Export data' }, - importData: { id: 'navigation_bar.import_data', defaultMessage: 'Import data' }, - mfaDisabled: { id: 'mfa.disabled', defaultMessage: 'Disabled' }, - mfaEnabled: { id: 'mfa.enabled', defaultMessage: 'Enabled' }, - mutes: { id: 'settings.mutes', defaultMessage: 'Mutes' }, - other: { id: 'settings.other', defaultMessage: 'Other Options' }, - preferences: { id: 'settings.preferences', defaultMessage: 'Preferences' }, + relays: { id: 'my_wallet.relays', defaultMessage: 'Relays' }, transactions: { id: 'my_wallet.transactions', defaultMessage: 'Transactions' }, - myWallet: { id: 'my_wallet.my_wallet', defaultMessage: 'My Wallet' }, - security: { id: 'settings.security', defaultMessage: 'Security' }, - sessions: { id: 'settings.sessions', defaultMessage: 'Active sessions' }, - settings: { id: 'settings.settings', defaultMessage: 'Settings' }, + myWallet: { id: 'my_wallet', defaultMessage: 'My Wallet' }, + management: { id: 'my_wallet.management', defaultMessage: 'Wallet Management' }, + mints: { id: 'my_wallet.mints', defaultMessage: 'Mints' }, }); -/** User settings page. */ +/** User Wallet page. */ const MyWallet = () => { - const dispatch = useAppDispatch(); + const api = useApi(); const intl = useIntl(); - const features = useFeatures(); const { account } = useOwnAccount(); + const [walletData, setWalletData] = useState(undefined); + + const fetchWallet = async () => { + + try { + const response = await api.get('/api/v1/ditto/cashu/wallet'); + const data: WalletData = await response.json(); + if (data) { + const normalizedData = baseWalletSchema.parse(data); + setWalletData(normalizedData); + } + + } catch (error) { + toast.error('Wallet not found'); + } + }; useEffect(() => { - if (features.security) dispatch(fetchMfa()); - }, [dispatch]); + fetchWallet(); + }, []); if (!account) return null; - const hasWallet = false; - return ( - + - {hasWallet ? ( + {walletData ? ( <> - + @@ -77,13 +72,13 @@ const MyWallet = () => { - + - - + + @@ -92,7 +87,7 @@ const MyWallet = () => { : <> - + From ebfc283b226c4e77d0d48c0a3c62dc6c62e44e56 Mon Sep 17 00:00:00 2001 From: danidfra Date: Mon, 17 Feb 2025 23:16:21 -0300 Subject: [PATCH 06/62] Refactor balance component and implement balance data --- src/features/my-wallet/components/balance.tsx | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/features/my-wallet/components/balance.tsx b/src/features/my-wallet/components/balance.tsx index fab0f52ae..d02fcbae9 100644 --- a/src/features/my-wallet/components/balance.tsx +++ b/src/features/my-wallet/components/balance.tsx @@ -1,24 +1,28 @@ // import IconButton from 'soapbox/components/ui/icon-button.tsx'; import withddrawIcon from '@tabler/icons/outline/cash.svg'; -import editIcon from '@tabler/icons/outline/edit.svg'; import exchangeIcon from '@tabler/icons/outline/transfer.svg'; -import { FormattedMessage } from 'react-intl'; +import { defineMessages, useIntl } from 'react-intl'; import Button from 'soapbox/components/ui/button.tsx'; import Divider from 'soapbox/components/ui/divider.tsx'; import HStack from 'soapbox/components/ui/hstack.tsx'; import Stack from 'soapbox/components/ui/stack.tsx'; -import SvgIcon from 'soapbox/components/ui/svg-icon.tsx'; import Text from 'soapbox/components/ui/text.tsx'; import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts'; -// const messages = defineMessages({ -// label: { id: 'settings.messages.label', defaultMessage: 'Allow users to start a new chat with you' }, -// }); +const messages = defineMessages({ + balance: { id: 'my_wallet.balance.sats', defaultMessage: '{amount} sats' }, + withdraw: { id: 'my_wallet.balance.withdraw_button', defaultMessage: 'Withdraw' }, + exchange: { id: 'my_wallet.balance.exchange_button', defaultMessage: 'Exchange' }, +}); +interface IBalance { + balance: number; +} -const Balance = () => { +const Balance = ({ balance }: IBalance) => { const { account } = useOwnAccount(); + const intl = useIntl(); if (!account) { return null; @@ -28,14 +32,8 @@ const Balance = () => { - - - - - - - {/* 166,186 sats */} + {intl.formatMessage(messages.balance, { amount: balance })} @@ -45,8 +43,8 @@ const Balance = () => { - + + + + + + ) + } ); From 39ae6e1badf94db0190fb2a338d80a67ce3327b3 Mon Sep 17 00:00:00 2001 From: danidfra Date: Wed, 19 Feb 2025 12:56:33 -0300 Subject: [PATCH 09/62] Run yarn i18n --- src/locales/en.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/locales/en.json b/src/locales/en.json index 92fe8227a..8ce7f9e7f 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1112,6 +1112,7 @@ "mutes.empty.groups": "You haven't muted any groups yet.", "my_wallet": "My Wallet", "my_wallet.balance.exchange_button": "Exchange", + "my_wallet.balance.sats": "{amount} sats", "my_wallet.balance.withdraw_button": "Withdraw", "my_wallet.create_wallet.button": "Create wallet", "my_wallet.create_wallet.question": "Do you want create one?", From f2df74886940fd824270a305a56f9feb9d8528f9 Mon Sep 17 00:00:00 2001 From: danidfra Date: Wed, 19 Feb 2025 14:01:34 -0300 Subject: [PATCH 10/62] Update walletSchema --- src/schemas/wallet.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/schemas/wallet.ts b/src/schemas/wallet.ts index 9bec9b174..ca4fe7278 100644 --- a/src/schemas/wallet.ts +++ b/src/schemas/wallet.ts @@ -3,9 +3,8 @@ import { z } from 'zod'; const baseWalletSchema = z.object({ pubkey_p2pk: n.id(), - mints: z.array(z.string()), - // .nonempty() - relays: z.array(z.string().url()).nonempty(), + mints: z.array(z.string().url()).nonempty(), + relays: z.array(z.string()).nonempty(), balance: z.number(), }); From d55f892236208fead790e4a2e6856e7f7c7e3d36 Mon Sep 17 00:00:00 2001 From: danidfra Date: Thu, 20 Feb 2025 20:55:01 -0300 Subject: [PATCH 11/62] Refactor the code and add a spin --- src/features/my-wallet/index.tsx | 87 +++++++++++++++++++------------- 1 file changed, 51 insertions(+), 36 deletions(-) diff --git a/src/features/my-wallet/index.tsx b/src/features/my-wallet/index.tsx index c005018a3..a6e47614f 100644 --- a/src/features/my-wallet/index.tsx +++ b/src/features/my-wallet/index.tsx @@ -4,6 +4,8 @@ import { defineMessages, useIntl } from 'react-intl'; import List, { ListItem } from 'soapbox/components/list.tsx'; import { Card, CardBody, CardHeader, CardTitle } from 'soapbox/components/ui/card.tsx'; import { Column } from 'soapbox/components/ui/column.tsx'; +import Spinner from 'soapbox/components/ui/spinner.tsx'; +import Stack from 'soapbox/components/ui/stack.tsx'; import Balance from 'soapbox/features/my-wallet/components/balance.tsx'; import CreateWallet from 'soapbox/features/my-wallet/components/create-wallet.tsx'; import Transactions from 'soapbox/features/my-wallet/components/transactions.tsx'; @@ -28,6 +30,7 @@ const MyWallet = () => { const { account } = useOwnAccount(); const [walletData, setWalletData] = useState(undefined); + const [isLoading, setIsLoading] = useState(true); const fetchWallet = async () => { @@ -41,6 +44,8 @@ const MyWallet = () => { } catch (error) { toast.error('Wallet not found'); + } finally { + setIsLoading(false); } }; @@ -51,49 +56,59 @@ const MyWallet = () => { if (!account) return null; return ( - - - - - + <> + {isLoading ? + + + + : + ( + + + + + - {walletData ? ( - <> - - - + {walletData ? ( + <> + + + - - - + + + - - - + + + - - - + + + - - - - - - + + + + + + - + + ) + : + <> + + + + + + } + + ) - : - <> - - - - - - } - - + } + ); }; From 9edcd3464357a44b5730f4072e5de3567d052227 Mon Sep 17 00:00:00 2001 From: danidfra Date: Thu, 20 Feb 2025 21:07:02 -0300 Subject: [PATCH 12/62] Add relays and mints pages --- .../my-wallet/components/create-wallet.tsx | 2 +- .../{relay-field => }/editable-lists.tsx | 3 +- .../my-wallet/components/wallet-mints.tsx | 69 +++++++++++++++++++ .../my-wallet/components/wallet-relays.tsx | 69 +++++++++++++++++++ src/features/ui/index.tsx | 4 ++ src/features/ui/util/async-components.ts | 2 + src/locales/en.json | 4 +- 7 files changed, 150 insertions(+), 3 deletions(-) rename src/features/my-wallet/components/{relay-field => }/editable-lists.tsx (96%) create mode 100644 src/features/my-wallet/components/wallet-mints.tsx create mode 100644 src/features/my-wallet/components/wallet-relays.tsx diff --git a/src/features/my-wallet/components/create-wallet.tsx b/src/features/my-wallet/components/create-wallet.tsx index 7934d7532..72d3fe648 100644 --- a/src/features/my-wallet/components/create-wallet.tsx +++ b/src/features/my-wallet/components/create-wallet.tsx @@ -10,7 +10,7 @@ import HStack from 'soapbox/components/ui/hstack.tsx'; import Stack from 'soapbox/components/ui/stack.tsx'; import SvgIcon from 'soapbox/components/ui/svg-icon.tsx'; import Text from 'soapbox/components/ui/text.tsx'; -import { MintEditor } from 'soapbox/features/my-wallet/components/relay-field/editable-lists.tsx'; +import { MintEditor } from 'soapbox/features/my-wallet/components/editable-lists.tsx'; import { useApi } from 'soapbox/hooks/useApi.ts'; import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts'; import { WalletData, baseWalletSchema } from 'soapbox/schemas/wallet.ts'; diff --git a/src/features/my-wallet/components/relay-field/editable-lists.tsx b/src/features/my-wallet/components/editable-lists.tsx similarity index 96% rename from src/features/my-wallet/components/relay-field/editable-lists.tsx rename to src/features/my-wallet/components/editable-lists.tsx index 8f5b60124..3b84593a1 100644 --- a/src/features/my-wallet/components/relay-field/editable-lists.tsx +++ b/src/features/my-wallet/components/editable-lists.tsx @@ -55,4 +55,5 @@ const RelayEditor: React.FC> = ({ items, setItems }) => { return ; }; -export { RelayEditor, MintEditor }; \ No newline at end of file +export { RelayEditor, MintEditor }; +export type { IEditableList }; \ No newline at end of file diff --git a/src/features/my-wallet/components/wallet-mints.tsx b/src/features/my-wallet/components/wallet-mints.tsx new file mode 100644 index 000000000..138e1f3ed --- /dev/null +++ b/src/features/my-wallet/components/wallet-mints.tsx @@ -0,0 +1,69 @@ +import { useEffect, useState } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + +import Button from 'soapbox/components/ui/button.tsx'; +import { Column } from 'soapbox/components/ui/column.tsx'; +import Stack from 'soapbox/components/ui/stack.tsx'; +import { RelayEditor } from 'soapbox/features/my-wallet/components/editable-lists.tsx'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { WalletData, baseWalletSchema } from 'soapbox/schemas/wallet.ts'; +import toast from 'soapbox/toast.tsx'; + +const messages = defineMessages({ + title: { id: 'my_wallet.mints', defaultMessage: 'Mints' }, + error: { id: 'my_wallet.loading_error', defaultMessage: 'An unexpected error occurred while loading your wallet data.' }, + send: { id: 'common.send', defaultMessage: 'Send' }, +}); + +const MyWalletMints = () => { + const intl = useIntl(); + const api = useApi(); + + const [mints, setMints] = useState([]); + + const fetchWallet = async () => { + try { + const response = await api.get('/api/v1/ditto/cashu/wallet'); + const data: WalletData = await response.json(); + if (data) { + const normalizedData = baseWalletSchema.parse(data); + setMints(normalizedData.mints); + } + + } catch (error) { + toast.error(intl.formatMessage(messages.error)); + } + }; + + const handleClick = async () =>{ + try { + const response = await api.post('/api/v1/ditto/cashu/wallet'); + const data: WalletData = await response.json(); + if (data) { + const normalizedData = baseWalletSchema.parse(data); + setMints(normalizedData.mints); + } + + } catch (error) { + toast.error('Wallet not found'); + } + }; + + useEffect(() => { + fetchWallet(); + }, []); + + return ( + + + + + + + + ); +}; + +export default MyWalletMints; diff --git a/src/features/my-wallet/components/wallet-relays.tsx b/src/features/my-wallet/components/wallet-relays.tsx new file mode 100644 index 000000000..cb7eaf7d9 --- /dev/null +++ b/src/features/my-wallet/components/wallet-relays.tsx @@ -0,0 +1,69 @@ +import { useEffect, useState } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + +import Button from 'soapbox/components/ui/button.tsx'; +import { Column } from 'soapbox/components/ui/column.tsx'; +import Stack from 'soapbox/components/ui/stack.tsx'; +import { RelayEditor } from 'soapbox/features/my-wallet/components/editable-lists.tsx'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { WalletData, baseWalletSchema } from 'soapbox/schemas/wallet.ts'; +import toast from 'soapbox/toast.tsx'; + +const messages = defineMessages({ + title: { id: 'my_wallet.relays', defaultMessage: 'Wallet Relays' }, + error: { id: 'my_wallet.loading_error', defaultMessage: 'An unexpected error occurred while loading your wallet data.' }, + send: { id: 'common.send', defaultMessage: 'Send' }, +}); + +const MyWalletRelays = () => { + const intl = useIntl(); + const api = useApi(); + + const [relays, setRelays] = useState(['teste.com']); + + const fetchWallet = async () => { + try { + const response = await api.get('/api/v1/ditto/cashu/wallet'); + const data: WalletData = await response.json(); + if (data) { + const normalizedData = baseWalletSchema.parse(data); + setRelays(normalizedData.relays); + } + + } catch (error) { + toast.error(intl.formatMessage(messages.error)); + } + }; + + const handleClick = async () =>{ + try { + const response = await api.post('/api/v1/ditto/cashu/wallet'); + const data: WalletData = await response.json(); + if (data) { + const normalizedData = baseWalletSchema.parse(data); + setRelays(normalizedData.relays); + } + + } catch (error) { + toast.error('Wallet not found'); + } + }; + + useEffect(() => { + fetchWallet(); + }, []); + + return ( + + + + + + + + ); +}; + +export default MyWalletRelays; diff --git a/src/features/ui/index.tsx b/src/features/ui/index.tsx index 17e2b09e1..eff69bc7d 100644 --- a/src/features/ui/index.tsx +++ b/src/features/ui/index.tsx @@ -74,6 +74,8 @@ import { Bookmarks, Settings, MyWallet, + MyWalletRelays, + MyWalletMints, EditProfile, EditEmail, EditPassword, @@ -333,6 +335,8 @@ const SwitchingColumnsArea: React.FC = ({ children }) => + + diff --git a/src/features/ui/util/async-components.ts b/src/features/ui/util/async-components.ts index 7565054e0..6b630b00c 100644 --- a/src/features/ui/util/async-components.ts +++ b/src/features/ui/util/async-components.ts @@ -182,4 +182,6 @@ export const ZapSplitModal = lazy(() => import('soapbox/features/ui/components/m export const CaptchaModal = lazy(() => import('soapbox/features/ui/components/modals/captcha-modal/captcha-modal.tsx')); export const NostrBunkerLogin = lazy(() => import('soapbox/features/nostr/nostr-bunker-login.tsx')); export const MyWallet = lazy(() => import('soapbox/features/my-wallet/index.tsx')); +export const MyWalletRelays = lazy(() => import('soapbox/features/my-wallet/components/wallet-relays.tsx')); +export const MyWalletMints = lazy(() => import('soapbox/features/my-wallet/components/wallet-mints.tsx')); export const StreakModal = lazy(() => import('soapbox/features/ui/components/modals/streak-modal.tsx')); diff --git a/src/locales/en.json b/src/locales/en.json index 8ce7f9e7f..e3005d672 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -439,6 +439,7 @@ "column_forbidden.title": "Forbidden", "common.cancel": "Cancel", "common.save": "Save", + "common.send": "Send", "compare_history_modal.header": "Edit history", "compose.character_counter.title": "Used {chars} out of {maxChars} {maxChars, plural, one {character} other {characters}}", "compose.edit_success": "Your post was edited", @@ -1117,9 +1118,10 @@ "my_wallet.create_wallet.button": "Create wallet", "my_wallet.create_wallet.question": "Do you want create one?", "my_wallet.create_wallet.title": "You don't have a wallet", + "my_wallet.loading_error": "An unexpected error occurred while loading your wallet data.", "my_wallet.management": "Wallet Management", "my_wallet.mints": "Mints", - "my_wallet.relays": "Relays", + "my_wallet.relays": "Wallet Relays", "my_wallet.transactions": "Transactions", "navbar.login.action": "Log in", "navbar.login.email.placeholder": "E-mail address", From 1220371992a0abcef2c4043084a56f48a895a496 Mon Sep 17 00:00:00 2001 From: danidfra Date: Tue, 11 Mar 2025 15:49:46 -0300 Subject: [PATCH 13/62] Just to save --- src/features/my-wallet/components/balance.tsx | 171 ++++++++++++++++-- src/features/my-wallet/index.tsx | 2 +- src/schemas/wallet.ts | 12 +- 3 files changed, 165 insertions(+), 20 deletions(-) diff --git a/src/features/my-wallet/components/balance.tsx b/src/features/my-wallet/components/balance.tsx index d02fcbae9..fd044a061 100644 --- a/src/features/my-wallet/components/balance.tsx +++ b/src/features/my-wallet/components/balance.tsx @@ -1,53 +1,188 @@ // import IconButton from 'soapbox/components/ui/icon-button.tsx'; -import withddrawIcon from '@tabler/icons/outline/cash.svg'; +import cancelIcon from '@tabler/icons/outline/cancel.svg'; +import withdrawIcon from '@tabler/icons/outline/cash.svg'; +import mIcon from '@tabler/icons/outline/circle-dotted-letter-m.svg'; +import libraryPlusIcon from '@tabler/icons/outline/library-plus.svg'; import exchangeIcon from '@tabler/icons/outline/transfer.svg'; +import QRCode from 'qrcode.react'; +import { useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; +import CopyableInput from 'soapbox/components/copyable-input.tsx'; import Button from 'soapbox/components/ui/button.tsx'; import Divider from 'soapbox/components/ui/divider.tsx'; +import FormGroup from 'soapbox/components/ui/form-group.tsx'; +import Form from 'soapbox/components/ui/form.tsx'; import HStack from 'soapbox/components/ui/hstack.tsx'; +import Input from 'soapbox/components/ui/input.tsx'; import Stack from 'soapbox/components/ui/stack.tsx'; import Text from 'soapbox/components/ui/text.tsx'; +import { SelectDropdown } from 'soapbox/features/forms/index.tsx'; +import { useApi } from 'soapbox/hooks/useApi.ts'; import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts'; +import { Quote, WalletData, quoteShema } from 'soapbox/schemas/wallet.ts'; +import toast from 'soapbox/toast.tsx'; + const messages = defineMessages({ balance: { id: 'my_wallet.balance.sats', defaultMessage: '{amount} sats' }, withdraw: { id: 'my_wallet.balance.withdraw_button', defaultMessage: 'Withdraw' }, exchange: { id: 'my_wallet.balance.exchange_button', defaultMessage: 'Exchange' }, + mint: { id: 'my_wallet.balance.mint_button', defaultMessage: 'Mint' }, }); -interface IBalance { - balance: number; + +interface AmountProps { + amount: number; + onMintClick: () => void; } -const Balance = ({ balance }: IBalance) => { - const { account } = useOwnAccount(); +interface NewMintProps { + list: string[]; + onCancel: () => void; +} + + +const Amount = ({ amount, onMintClick }: AmountProps) => { const intl = useIntl(); - if (!account) { - return null; - } - return ( - - - - - {intl.formatMessage(messages.balance, { amount: balance })} - - + + + {intl.formatMessage(messages.balance, { amount })} +
- - diff --git a/src/features/my-wallet/components/editable-lists.tsx b/src/features/wallet/components/editable-lists.tsx similarity index 100% rename from src/features/my-wallet/components/editable-lists.tsx rename to src/features/wallet/components/editable-lists.tsx diff --git a/src/features/my-wallet/components/transactions.tsx b/src/features/wallet/components/transactions.tsx similarity index 100% rename from src/features/my-wallet/components/transactions.tsx rename to src/features/wallet/components/transactions.tsx diff --git a/src/features/my-wallet/components/wallet-mints.tsx b/src/features/wallet/components/wallet-mints.tsx similarity index 84% rename from src/features/my-wallet/components/wallet-mints.tsx rename to src/features/wallet/components/wallet-mints.tsx index 138e1f3ed..7172a5a0a 100644 --- a/src/features/my-wallet/components/wallet-mints.tsx +++ b/src/features/wallet/components/wallet-mints.tsx @@ -4,18 +4,18 @@ import { defineMessages, useIntl } from 'react-intl'; import Button from 'soapbox/components/ui/button.tsx'; import { Column } from 'soapbox/components/ui/column.tsx'; import Stack from 'soapbox/components/ui/stack.tsx'; -import { RelayEditor } from 'soapbox/features/my-wallet/components/editable-lists.tsx'; +import { RelayEditor } from 'soapbox/features/wallet/components/editable-lists.tsx'; import { useApi } from 'soapbox/hooks/useApi.ts'; import { WalletData, baseWalletSchema } from 'soapbox/schemas/wallet.ts'; import toast from 'soapbox/toast.tsx'; const messages = defineMessages({ - title: { id: 'my_wallet.mints', defaultMessage: 'Mints' }, - error: { id: 'my_wallet.loading_error', defaultMessage: 'An unexpected error occurred while loading your wallet data.' }, + title: { id: 'wallet.mints', defaultMessage: 'Mints' }, + error: { id: 'wallet.loading_error', defaultMessage: 'An unexpected error occurred while loading your wallet data.' }, send: { id: 'common.send', defaultMessage: 'Send' }, }); -const MyWalletMints = () => { +const WalletMints = () => { const intl = useIntl(); const api = useApi(); @@ -66,4 +66,4 @@ const MyWalletMints = () => { ); }; -export default MyWalletMints; +export default WalletMints; diff --git a/src/features/my-wallet/components/wallet-relays.tsx b/src/features/wallet/components/wallet-relays.tsx similarity index 83% rename from src/features/my-wallet/components/wallet-relays.tsx rename to src/features/wallet/components/wallet-relays.tsx index cb7eaf7d9..49172cbf4 100644 --- a/src/features/my-wallet/components/wallet-relays.tsx +++ b/src/features/wallet/components/wallet-relays.tsx @@ -4,18 +4,18 @@ import { defineMessages, useIntl } from 'react-intl'; import Button from 'soapbox/components/ui/button.tsx'; import { Column } from 'soapbox/components/ui/column.tsx'; import Stack from 'soapbox/components/ui/stack.tsx'; -import { RelayEditor } from 'soapbox/features/my-wallet/components/editable-lists.tsx'; +import { RelayEditor } from 'soapbox/features/wallet/components/editable-lists.tsx'; import { useApi } from 'soapbox/hooks/useApi.ts'; import { WalletData, baseWalletSchema } from 'soapbox/schemas/wallet.ts'; import toast from 'soapbox/toast.tsx'; const messages = defineMessages({ - title: { id: 'my_wallet.relays', defaultMessage: 'Wallet Relays' }, - error: { id: 'my_wallet.loading_error', defaultMessage: 'An unexpected error occurred while loading your wallet data.' }, + title: { id: 'wallet.relays', defaultMessage: 'Wallet Relays' }, + error: { id: 'wallet.loading_error', defaultMessage: 'An unexpected error occurred while loading your wallet data.' }, send: { id: 'common.send', defaultMessage: 'Send' }, }); -const MyWalletRelays = () => { +const WalletRelays = () => { const intl = useIntl(); const api = useApi(); @@ -66,4 +66,4 @@ const MyWalletRelays = () => { ); }; -export default MyWalletRelays; +export default WalletRelays; diff --git a/src/features/my-wallet/index.tsx b/src/features/wallet/index.tsx similarity index 80% rename from src/features/my-wallet/index.tsx rename to src/features/wallet/index.tsx index 06e5b3755..aa8c9c6a7 100644 --- a/src/features/my-wallet/index.tsx +++ b/src/features/wallet/index.tsx @@ -7,9 +7,9 @@ import { Column } from 'soapbox/components/ui/column.tsx'; import Spinner from 'soapbox/components/ui/spinner.tsx'; import Stack from 'soapbox/components/ui/stack.tsx'; import { SelectDropdown } from 'soapbox/features/forms/index.tsx'; -import Balance from 'soapbox/features/my-wallet/components/balance.tsx'; -import CreateWallet from 'soapbox/features/my-wallet/components/create-wallet.tsx'; -import Transactions from 'soapbox/features/my-wallet/components/transactions.tsx'; +import Balance from 'soapbox/features/wallet/components/balance.tsx'; +import CreateWallet from 'soapbox/features/wallet/components/create-wallet.tsx'; +import Transactions from 'soapbox/features/wallet/components/transactions.tsx'; import { usePaymentMethod } from 'soapbox/features/zap/usePaymentMethod.ts'; import { useApi } from 'soapbox/hooks/useApi.ts'; import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts'; @@ -18,12 +18,12 @@ import toast from 'soapbox/toast.tsx'; const messages = defineMessages({ - payment: { id: 'my_wallet.payment', defaultMessage: 'Payment Method' }, - relays: { id: 'my_wallet.relays', defaultMessage: 'Relays' }, - transactions: { id: 'my_wallet.transactions', defaultMessage: 'Transactions' }, - myWallet: { id: 'my_wallet', defaultMessage: 'My Wallet' }, - management: { id: 'my_wallet.management', defaultMessage: 'Wallet Management' }, - mints: { id: 'my_wallet.mints', defaultMessage: 'Mints' }, + payment: { id: 'wallet.payment', defaultMessage: 'Payment Method' }, + relays: { id: 'wallet.relays', defaultMessage: 'Relays' }, + transactions: { id: 'wallet.transactions', defaultMessage: 'Transactions' }, + wallet: { id: 'wallet', defaultMessage: 'Wallet' }, + management: { id: 'wallet.management', defaultMessage: 'Wallet Management' }, + mints: { id: 'wallet.mints', defaultMessage: 'Mints' }, }); const paymentMethods = { @@ -32,7 +32,7 @@ const paymentMethods = { }; /** User Wallet page. */ -const MyWallet = () => { +const Wallet = () => { const api = useApi(); const intl = useIntl(); @@ -72,10 +72,10 @@ const MyWallet = () => {
: ( - + - + {walletData ? ( @@ -131,4 +131,4 @@ const MyWallet = () => { ); }; -export default MyWallet; \ No newline at end of file +export default Wallet; \ No newline at end of file diff --git a/src/locales/en.json b/src/locales/en.json index 07aae2578..416621125 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1139,22 +1139,22 @@ "mute_modal.duration": "Duration", "mute_modal.hide_notifications": "Hide notifications from this user?", "mutes.empty.groups": "You haven't muted any groups yet.", - "my_wallet": "My Wallet", - "my_wallet.balance.cashu": "{amount} cashus", - "my_wallet.balance.exchange_button": "Exchange", - "my_wallet.balance.mint.paid_message": "Your mint was successful, and your cashus are now in your balance. Enjoy!", - "my_wallet.balance.mint.unpaid_message": "Your mint is still unpaid. Complete the payment to receive your cashus.", - "my_wallet.balance.mint_button": "Mint", - "my_wallet.balance.withdraw_button": "Withdraw", - "my_wallet.create_wallet.button": "Create wallet", - "my_wallet.create_wallet.question": "Do you want create one?", - "my_wallet.create_wallet.title": "You don't have a wallet", - "my_wallet.loading_error": "An unexpected error occurred while loading your wallet data.", - "my_wallet.management": "Wallet Management", - "my_wallet.mints": "Mints", - "my_wallet.relays": "Wallet Relays", - "my_wallet.payment": "Payment Method", - "my_wallet.transactions": "Transactions", + "wallet": "Wallet", + "wallet.balance.cashu": "{amount} cashus", + "wallet.balance.exchange_button": "Exchange", + "wallet.balance.mint.paid_message": "Your mint was successful, and your cashus are now in your balance. Enjoy!", + "wallet.balance.mint.unpaid_message": "Your mint is still unpaid. Complete the payment to receive your cashus.", + "wallet.balance.mint_button": "Mint", + "wallet.balance.withdraw_button": "Withdraw", + "wallet.create_wallet.button": "Create wallet", + "wallet.create_wallet.question": "Do you want create one?", + "wallet.create_wallet.title": "You don't have a wallet", + "wallet.loading_error": "An unexpected error occurred while loading your wallet data.", + "wallet.management": "Wallet Management", + "wallet.mints": "Mints", + "wallet.relays": "Wallet Relays", + "wallet.payment": "Payment Method", + "wallet.transactions": "Transactions", "navbar.login.action": "Log in", "navbar.login.email.placeholder": "E-mail address", "navbar.login.forgot_password": "Forgot password?", @@ -1669,7 +1669,7 @@ "tabs_bar.profile": "Profile", "tabs_bar.search": "Explore", "tabs_bar.settings": "Settings", - "tabs_bar.wallet": "My Wallet", + "tabs_bar.wallet": "Wallet", "textarea.counter.label": "{count} characters remaining", "theme_editor.colors.accent": "Accent", "theme_editor.colors.accent_blue": "Accent Blue", From 6fb35188a96efb99c7ad23518d2051abed65b98f Mon Sep 17 00:00:00 2001 From: danidfra Date: Tue, 18 Mar 2025 11:20:24 -0300 Subject: [PATCH 24/62] Add "Wallet" button in sidebar-menu --- src/components/sidebar-menu.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/sidebar-menu.tsx b/src/components/sidebar-menu.tsx index 2401e1617..87adca801 100644 --- a/src/components/sidebar-menu.tsx +++ b/src/components/sidebar-menu.tsx @@ -13,6 +13,7 @@ import plusIcon from '@tabler/icons/outline/plus.svg'; import settingsIcon from '@tabler/icons/outline/settings.svg'; import userPlusIcon from '@tabler/icons/outline/user-plus.svg'; import userIcon from '@tabler/icons/outline/user.svg'; +import walletIcon from '@tabler/icons/outline/wallet.svg'; import xIcon from '@tabler/icons/outline/x.svg'; import clsx from 'clsx'; import { useCallback, useEffect, useRef, useState } from 'react'; @@ -213,6 +214,13 @@ const SidebarMenu: React.FC = (): JSX.Element | null => { onClick={onClose} /> + } + onClick={onClose} + /> + {(account.locked || followRequestsCount > 0) && ( Date: Tue, 18 Mar 2025 11:31:11 -0300 Subject: [PATCH 25/62] Update translated text in zap-modal --- src/features/zap/components/pay-request-form.tsx | 9 +++++---- src/locales/en.json | 4 ++-- src/locales/es.json | 2 +- src/locales/ga.json | 4 ++-- src/locales/pt-BR.json | 2 +- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/features/zap/components/pay-request-form.tsx b/src/features/zap/components/pay-request-form.tsx index 0b508e8a6..e2d4d6fc6 100644 --- a/src/features/zap/components/pay-request-form.tsx +++ b/src/features/zap/components/pay-request-form.tsx @@ -24,6 +24,7 @@ import Text from 'soapbox/components/ui/text.tsx'; import { usePaymentMethod } from 'soapbox/features/zap/usePaymentMethod.ts'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; import { emojifyText } from 'soapbox/utils/emojify.tsx'; +import { capitalize } from 'soapbox/utils/strings.ts'; import PaymentButton from './zap-button/payment-button.tsx'; @@ -46,8 +47,8 @@ interface IPayRequestForm { const closeIcon = xIcon; const messages = defineMessages({ - zap_button_rounded: { id: 'zap.button.text.rounded', defaultMessage: 'Zap {amount}K sats' }, - zap_button: { id: 'payment_method.button.text.raw', defaultMessage: 'Zap {amount} sats' }, + zap_button_rounded: { id: 'zap.button.text.rounded', defaultMessage: '{method} {amount}K sats' }, + zap_button: { id: 'payment_method.button.text.raw', defaultMessage: '{method} {amount} sats' }, zap_commentPlaceholder: { id: 'payment_method.comment_input.placeholder', defaultMessage: 'Optional comment' }, }); @@ -106,9 +107,9 @@ const PayRequestForm = ({ account, status, onClose }: IPayRequestForm) => { const renderPaymentButtonText = () => { if (amount >= 1000) { - return intl.formatMessage(messages.zap_button_rounded, { amount: Math.round((amount / 1000) * 10) / 10 }); + return intl.formatMessage(messages.zap_button_rounded, { amount: Math.round((amount / 1000) * 10) / 10, method: isCashu ? 'Nutzap' : capitalize(paymentMethod) }); } - return intl.formatMessage(messages.zap_button, { amount: amount }); + return intl.formatMessage(messages.zap_button, { amount: amount, method: isCashu ? 'Nutzap' : capitalize(paymentMethod) }); }; useEffect(() => { diff --git a/src/locales/en.json b/src/locales/en.json index 416621125..18e747361 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1727,8 +1727,8 @@ "video.play": "Play", "video.unmute": "Unmute sound", "who_to_follow.title": "People To Follow", - "payment_method.button.text.raw": "Zap {amount} sats", - "zap.button.text.rounded": "Zap {amount}K sats", + "payment_method.button.text.raw": "{method} {amount} sats", + "zap.button.text.rounded": "{method} {amount}K sats", "payment_method.comment_input.placeholder": "Optional comment", "zap.finish": "Finish", "zap.next": "Next", diff --git a/src/locales/es.json b/src/locales/es.json index 6df0403ed..bac461ddb 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -1626,7 +1626,7 @@ "video.play": "Reproducir", "video.unmute": "Dejar de silenciar sonido", "who_to_follow.title": "Personas a seguir", - "payment_method.button.text.raw": "Zap {amount} sats", + "payment_method.button.text.raw": "{method} {amount} sats", "payment_method.comment_input.placeholder": "Comentario opcional", "zap.open_wallet": "Abrir Wallet", "zap.send_to": "Enviar zaps a {target}", diff --git a/src/locales/ga.json b/src/locales/ga.json index a7de77b12..d8d70e1e6 100644 --- a/src/locales/ga.json +++ b/src/locales/ga.json @@ -1679,8 +1679,8 @@ "video.play": "Seinn", "video.unmute": "Fuaim gan iomrá", "who_to_follow.title": "Daoine le Leanúint", - "payment_method.button.text.raw": "Suíonn Zap {amount}", - "zap.button.text.rounded": "Zap {amount}K shuíonn", + "payment_method.button.text.raw": "Suíonn {method} {amount}", + "zap.button.text.rounded": "{method} {amount}K shuíonn", "payment_method.comment_input.placeholder": "Nóta roghnach", "zap.finish": "Críochnaigh", "zap.next": "Ar Aghaidh", diff --git a/src/locales/pt-BR.json b/src/locales/pt-BR.json index 1657c2da3..298539676 100644 --- a/src/locales/pt-BR.json +++ b/src/locales/pt-BR.json @@ -1635,7 +1635,7 @@ "video.play": "Reproduzir", "video.unmute": "Reativar som", "who_to_follow.title": "Quem seguir", - "payment_method.button.text.raw": "Zap {amount} sats", + "payment_method.button.text.raw": "{method} {amount} sats", "payment_method.comment_input.placeholder": "Comentário opcional", "zap.open_wallet": "Abrir Carteira", "zap.send_to": "Enviar zaps para {target}", From e6b1df6850992fc193138dfa209a62aacaa839b5 Mon Sep 17 00:00:00 2001 From: danidfra Date: Tue, 18 Mar 2025 11:40:12 -0300 Subject: [PATCH 26/62] Rename from "cashus" to "sats" in balance --- src/features/wallet/components/balance.tsx | 6 +++--- src/locales/en.json | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/features/wallet/components/balance.tsx b/src/features/wallet/components/balance.tsx index 586d9191b..8a8d480f3 100644 --- a/src/features/wallet/components/balance.tsx +++ b/src/features/wallet/components/balance.tsx @@ -28,12 +28,12 @@ import toast from 'soapbox/toast.tsx'; const messages = defineMessages({ - balance: { id: 'wallet.balance.cashu', defaultMessage: '{amount} cashus' }, + balance: { id: 'wallet.balance.cashu', defaultMessage: '{amount} sats' }, withdraw: { id: 'wallet.balance.withdraw_button', defaultMessage: 'Withdraw' }, exchange: { id: 'wallet.balance.exchange_button', defaultMessage: 'Exchange' }, mint: { id: 'wallet.balance.mint_button', defaultMessage: 'Mint' }, - paidMessage: { id: 'wallet.balance.mint.paid_message', defaultMessage: 'Your mint was successful, and your cashus are now in your balance. Enjoy!' }, - unpaidMessage: { id: 'wallet.balance.mint.unpaid_message', defaultMessage: 'Your mint is still unpaid. Complete the payment to receive your cashus.' }, + paidMessage: { id: 'wallet.balance.mint.paid_message', defaultMessage: 'Your mint was successful, and your sats are now in your balance. Enjoy!' }, + unpaidMessage: { id: 'wallet.balance.mint.unpaid_message', defaultMessage: 'Your mint is still unpaid. Complete the payment to receive your sats.' }, }); interface AmountProps { diff --git a/src/locales/en.json b/src/locales/en.json index 18e747361..ae3bff6b8 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1140,10 +1140,10 @@ "mute_modal.hide_notifications": "Hide notifications from this user?", "mutes.empty.groups": "You haven't muted any groups yet.", "wallet": "Wallet", - "wallet.balance.cashu": "{amount} cashus", + "wallet.balance.cashu": "{amount} sats", "wallet.balance.exchange_button": "Exchange", - "wallet.balance.mint.paid_message": "Your mint was successful, and your cashus are now in your balance. Enjoy!", - "wallet.balance.mint.unpaid_message": "Your mint is still unpaid. Complete the payment to receive your cashus.", + "wallet.balance.mint.paid_message": "Your mint was successful, and your sats are now in your balance. Enjoy!", + "wallet.balance.mint.unpaid_message": "Your mint is still unpaid. Complete the payment to receive your sats.", "wallet.balance.mint_button": "Mint", "wallet.balance.withdraw_button": "Withdraw", "wallet.create_wallet.button": "Create wallet", From e00135b03cd06cacfe402bb908af0a932924785d Mon Sep 17 00:00:00 2001 From: danidfra Date: Tue, 18 Mar 2025 21:32:14 -0300 Subject: [PATCH 27/62] Remove unnecessary button and update balance style --- src/features/wallet/components/balance.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/features/wallet/components/balance.tsx b/src/features/wallet/components/balance.tsx index 8a8d480f3..95c8113cc 100644 --- a/src/features/wallet/components/balance.tsx +++ b/src/features/wallet/components/balance.tsx @@ -5,7 +5,6 @@ import withdrawIcon from '@tabler/icons/outline/cash.svg'; import mIcon from '@tabler/icons/outline/circle-dotted-letter-m.svg'; import creditCardPayIcon from '@tabler/icons/outline/credit-card-pay.svg'; import libraryPlusIcon from '@tabler/icons/outline/library-plus.svg'; -import exchangeIcon from '@tabler/icons/outline/transfer.svg'; import QRCode from 'qrcode.react'; import { useCallback, useEffect, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; @@ -61,8 +60,8 @@ const Amount = ({ amount, onMintClick }: AmountProps) => { const intl = useIntl(); return ( - - + + {intl.formatMessage(messages.balance, { amount })} @@ -72,7 +71,6 @@ const Amount = ({ amount, onMintClick }: AmountProps) => { diff --git a/src/features/wallet/components/wallet-relays.tsx b/src/features/wallet/components/wallet-relays.tsx index 49172cbf4..0e362bafb 100644 --- a/src/features/wallet/components/wallet-relays.tsx +++ b/src/features/wallet/components/wallet-relays.tsx @@ -8,10 +8,15 @@ import { RelayEditor } from 'soapbox/features/wallet/components/editable-lists.t import { useApi } from 'soapbox/hooks/useApi.ts'; import { WalletData, baseWalletSchema } from 'soapbox/schemas/wallet.ts'; import toast from 'soapbox/toast.tsx'; +import { isURL } from 'soapbox/utils/auth.ts'; const messages = defineMessages({ title: { id: 'wallet.relays', defaultMessage: 'Wallet Relays' }, - error: { 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 mints.' }, + empty: { id: 'wallet.relays.empty', defaultMessage: 'At least one relay is required.' }, + url: { id: 'wallet.invalid_url', defaultMessage: 'All strings must be valid URLs.' }, + sucess: { id: 'wallet.relays.sucess', defaultMessage: 'Relays updated with success!' }, send: { id: 'common.send', defaultMessage: 'Send' }, }); @@ -19,7 +24,9 @@ const WalletRelays = () => { const intl = useIntl(); const api = useApi(); - const [relays, setRelays] = useState(['teste.com']); + const [relays, setRelays] = useState([]); + const [initialRelays, setInitialRelays] = useState([]); + const [mints, setMints] = useState([]); const fetchWallet = async () => { try { @@ -27,25 +34,38 @@ const WalletRelays = () => { const data: WalletData = await response.json(); if (data) { const normalizedData = baseWalletSchema.parse(data); + setMints(normalizedData.mints); setRelays(normalizedData.relays); + setInitialRelays(normalizedData.relays); } } catch (error) { - toast.error(intl.formatMessage(messages.error)); + toast.error(intl.formatMessage(messages.loadingError)); } }; const handleClick = async () =>{ - try { - const response = await api.post('/api/v1/ditto/cashu/wallet'); - const data: WalletData = await response.json(); - if (data) { - const normalizedData = baseWalletSchema.parse(data); - setRelays(normalizedData.relays); - } + if (relays.length <= 0) { + toast.error(intl.formatMessage(messages.empty)); + return; + } + if (relays.some((relay) => !isURL(relay))) { + toast.error(intl.formatMessage(messages.url)); + return; + } + + if (JSON.stringify(initialRelays) === JSON.stringify(relays)) { + return; + } + + try { + await api.put('/api/v1/ditto/cashu/wallet', { mints: mints, relays: relays }); + toast.success(intl.formatMessage(messages.sucess)); } catch (error) { - toast.error('Wallet not found'); + const errorMessage = error instanceof Error ? error.message : intl.formatMessage(messages.error); + toast.error(errorMessage); + console.error(error); } }; diff --git a/src/locales/en.json b/src/locales/en.json index ae3bff6b8..1836cc499 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1139,22 +1139,6 @@ "mute_modal.duration": "Duration", "mute_modal.hide_notifications": "Hide notifications from this user?", "mutes.empty.groups": "You haven't muted any groups yet.", - "wallet": "Wallet", - "wallet.balance.cashu": "{amount} sats", - "wallet.balance.exchange_button": "Exchange", - "wallet.balance.mint.paid_message": "Your mint was successful, and your sats are now in your balance. Enjoy!", - "wallet.balance.mint.unpaid_message": "Your mint is still unpaid. Complete the payment to receive your sats.", - "wallet.balance.mint_button": "Mint", - "wallet.balance.withdraw_button": "Withdraw", - "wallet.create_wallet.button": "Create wallet", - "wallet.create_wallet.question": "Do you want create one?", - "wallet.create_wallet.title": "You don't have a wallet", - "wallet.loading_error": "An unexpected error occurred while loading your wallet data.", - "wallet.management": "Wallet Management", - "wallet.mints": "Mints", - "wallet.relays": "Wallet Relays", - "wallet.payment": "Payment Method", - "wallet.transactions": "Transactions", "navbar.login.action": "Log in", "navbar.login.email.placeholder": "E-mail address", "navbar.login.forgot_password": "Forgot password?", @@ -1275,8 +1259,6 @@ "notifications.filter.statuses": "Updates from people you follow", "notifications.group": "{count, plural, one {# notification} other {# notifications}}", "notifications.queue_label": "Click to see {count} new {count, plural, one {notification} other {notifications}}", - "zap.send_to": "Send zaps to {target}", - "payment_method.send_to": "Send {method} to {target}", "oauth_consumer.tooltip": "Sign in with {provider}", "oauth_consumers.title": "Other ways to sign in", "onboarding.avatar.subtitle": "Just have fun with it.", @@ -1313,6 +1295,10 @@ "password_reset.reset": "Reset password", "patron.donate": "Donate", "patron.title": "Funding Goal", + "payment_method.button.text.raw": "{method} {amount} sats", + "payment_method.comment_input.placeholder": "Optional comment", + "payment_method.send_to": "Send {method} to {target}", + "payment_method.split_message.deducted": "{amountDeducted} sats will deducted*", "pinned_accounts.title": "{name}’s choices", "pinned_statuses.none": "No pins to show.", "poll.choose_multiple": "Choose as many as you'd like.", @@ -1726,14 +1712,37 @@ "video.pause": "Pause", "video.play": "Play", "video.unmute": "Unmute sound", + "wallet": "Wallet", + "wallet.balance.cashu": "{amount} sats", + "wallet.balance.exchange_button": "Exchange", + "wallet.balance.expired": "Expired", + "wallet.balance.mint.paid_message": "Your mint was successful, and your sats are now in your balance. Enjoy!", + "wallet.balance.mint.payment": "Make the payment to complete:", + "wallet.balance.mint.unpaid_message": "Your mint is still unpaid. Complete the payment to receive your sats.", + "wallet.balance.mint_button": "Mint", + "wallet.balance.withdraw_button": "Withdraw", + "wallet.create_wallet.button": "Create wallet", + "wallet.create_wallet.question": "Do you want create one?", + "wallet.create_wallet.title": "You don't have a wallet", + "wallet.invalid_url": "All strings must be valid URLs.", + "wallet.loading_error": "An unexpected error occurred while loading your wallet data.", + "wallet.management": "Wallet Management", + "wallet.mints": "Mints", + "wallet.mints.empty": "At least one mint is required.", + "wallet.mints.error": "Failed to update mints.", + "wallet.mints.sucess": "Mints updated with success!", + "wallet.payment": "Payment Method", + "wallet.relays": "Wallet Relays", + "wallet.relays.empty": "At least one relay is required.", + "wallet.relays.error": "Failed to update mints.", + "wallet.relays.sucess": "Relays updated with success!", + "wallet.transactions": "Transactions", "who_to_follow.title": "People To Follow", - "payment_method.button.text.raw": "{method} {amount} sats", "zap.button.text.rounded": "{method} {amount}K sats", - "payment_method.comment_input.placeholder": "Optional comment", "zap.finish": "Finish", "zap.next": "Next", "zap.open_wallet": "Open Wallet", - "payment_method.split_message.deducted": "{amountDeducted} sats will deducted*", + "zap.send_to": "Send zaps to {target}", "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!", From 1d2c7b2de664c39aa1dda34c466eaa05aef39f2f Mon Sep 17 00:00:00 2001 From: danidfra Date: Thu, 20 Mar 2025 16:48:32 -0300 Subject: [PATCH 33/62] Fix missing boltIcon, refactor and rename hook --- src/components/pure-status-action-bar.tsx | 18 +++- src/components/status-action-bar.tsx | 16 +++- src/features/account/components/header.tsx | 17 +++- .../zap/components/pay-request-form.tsx | 4 +- src/features/zap/hooks/useCashu.ts | 83 +++++++++++++++++++ src/features/zap/hooks/useNutzap.ts | 42 ---------- 6 files changed, 128 insertions(+), 52 deletions(-) create mode 100644 src/features/zap/hooks/useCashu.ts delete mode 100644 src/features/zap/hooks/useNutzap.ts diff --git a/src/components/pure-status-action-bar.tsx b/src/components/pure-status-action-bar.tsx index 189460635..1a527e82c 100644 --- a/src/components/pure-status-action-bar.tsx +++ b/src/components/pure-status-action-bar.tsx @@ -29,6 +29,7 @@ import thumbUpIcon from '@tabler/icons/outline/thumb-up.svg'; import trashIcon from '@tabler/icons/outline/trash.svg'; import uploadIcon from '@tabler/icons/outline/upload.svg'; import volume3Icon from '@tabler/icons/outline/volume-3.svg'; +import { useEffect } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { useHistory, useRouteMatch } from 'react-router-dom'; @@ -48,7 +49,8 @@ import DropdownMenu from 'soapbox/components/dropdown-menu/index.ts'; import PureStatusReactionWrapper from 'soapbox/components/pure-status-reaction-wrapper.tsx'; import StatusActionButton from 'soapbox/components/status-action-button.tsx'; import HStack from 'soapbox/components/ui/hstack.tsx'; -import { useNutzap } from 'soapbox/features/zap/hooks/useNutzap.ts'; +import { useCashu } from 'soapbox/features/zap/hooks/useCashu.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; import { useDislike } from 'soapbox/hooks/useDislike.ts'; @@ -164,6 +166,7 @@ const PureStatusActionBar: React.FC = ({ statusActionButtonTheme = 'default', }) => { const intl = useIntl(); + const api = useApi(); // TODO: Remove this part after patrick implement in backend const history = useHistory(); const dispatch = useAppDispatch(); const match = useRouteMatch<{ groupSlug: string }>('/group/:groupSlug'); @@ -180,8 +183,8 @@ const PureStatusActionBar: React.FC = ({ const features = useFeatures(); const { boostModal, deleteModal } = useSettings(); - const { nutzapsList } = useNutzap(); - const isNutzapped = Object.keys(nutzapsList).some((nutzap)=> nutzap === status.id); + const { wallet, getWallet, nutzapsList } = useCashu(); + const isNutzapped = Object.keys(nutzapsList).some((nutzap)=> nutzap === status.id); // TODO: Remove "getWallet" to const { account } = useOwnAccount(); const isStaff = account ? account.staff : false; @@ -197,6 +200,12 @@ const PureStatusActionBar: React.FC = ({ const { unpinFromGroup, pinToGroup } = usePinGroup(); const { initReport } = useInitReport(); + useEffect( + () => { + getWallet(api, false); + } // TODO: Remove + , []); + if (!status) { return null; } @@ -763,6 +772,7 @@ const PureStatusActionBar: React.FC = ({ const canShare = ('share' in navigator) && (status.visibility === 'public' || status.visibility === 'group'); const acceptsZaps = status.account.ditto.accepts_zaps === true; + const hasWallet = wallet !== null; const spacing: { [key: string]: React.ComponentProps['space']; @@ -846,7 +856,7 @@ const PureStatusActionBar: React.FC = ({ /> )} - {(acceptsZaps) && ( + {(acceptsZaps || hasWallet) && ( = ({ }) => { const intl = useIntl(); const history = useHistory(); + const api = useApi(); // TODO: Remove this part after patrick implement in backend const dispatch = useAppDispatch(); const match = useRouteMatch<{ groupSlug: string }>('/group/:groupSlug'); @@ -175,7 +178,7 @@ const StatusActionBar: React.FC = ({ const features = useFeatures(); const { boostModal, deleteModal } = useSettings(); - const { nutzapsList } = useNutzap(); + const { wallet, getWallet, nutzapsList } = useCashu(); const isNutzapped = Object.keys(nutzapsList).some((nutzap)=> nutzap === status.id); const { account } = useOwnAccount(); @@ -185,6 +188,12 @@ const StatusActionBar: React.FC = ({ const { toggleReblog } = useReblog(); const { bookmark, unbookmark } = useBookmark(); + useEffect( + () => { + getWallet(api, false); + } // TODO: remove + , []); + if (!status) { return null; } @@ -753,6 +762,7 @@ const StatusActionBar: React.FC = ({ const canShare = ('share' in navigator) && (status.visibility === 'public' || status.visibility === 'group'); const acceptsZaps = status.account.ditto.accepts_zaps === true; + const hasWallet = wallet !== null; const spacing: { [key: string]: React.ComponentProps['space']; @@ -836,7 +846,7 @@ const StatusActionBar: React.FC = ({ /> )} - {(acceptsZaps) && ( + {(acceptsZaps || hasWallet) && ( = ({ account }) => { const intl = useIntl(); const history = useHistory(); + const api = useApi(); // TODO: Remove this part after patrick implement in backend const dispatch = useAppDispatch(); const features = useFeatures(); @@ -120,6 +124,9 @@ const Header: React.FC = ({ account }) => { const { software } = useAppSelector((state) => parseVersion(state.instance.version)); + const { wallet, getWallet } = useCashu(); + + const { getOrCreateChatByAccountId } = useChats(); const createAndNavigateToChat = useMutation({ @@ -138,6 +145,13 @@ const Header: React.FC = ({ account }) => { }, }); + + useEffect( + () => { + getWallet(api, false); + } // TODO: remove + , []); + if (!account) { return (
@@ -680,6 +694,7 @@ const Header: React.FC = ({ account }) => { const info = makeInfo(); const menu = makeMenu(); const acceptsZaps = account.ditto.accepts_zaps === true; + const hasWallet = wallet !== null; return (
@@ -721,7 +736,7 @@ const Header: React.FC = ({ account }) => { {renderMessageButton()} {renderShareButton()} - {acceptsZaps && renderZapAccount()} + {(acceptsZaps || hasWallet) && renderZapAccount()} {menu.length > 0 && ( diff --git a/src/features/zap/components/pay-request-form.tsx b/src/features/zap/components/pay-request-form.tsx index 8fd31da0d..d57d2fddf 100644 --- a/src/features/zap/components/pay-request-form.tsx +++ b/src/features/zap/components/pay-request-form.tsx @@ -21,7 +21,7 @@ import Input from 'soapbox/components/ui/input.tsx'; import Stack from 'soapbox/components/ui/stack.tsx'; import SvgIcon from 'soapbox/components/ui/svg-icon.tsx'; import Text from 'soapbox/components/ui/text.tsx'; -import { useNutzap } from 'soapbox/features/zap/hooks/useNutzap.ts'; +import { useCashu } from 'soapbox/features/zap/hooks/useCashu.ts'; import { usePaymentMethod } from 'soapbox/features/zap/usePaymentMethod.ts'; import { useApi } from 'soapbox/hooks/useApi.ts'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; @@ -66,7 +66,7 @@ const PayRequestForm = ({ account, status, onClose }: IPayRequestForm) => { const { method: paymentMethod } = usePaymentMethod(); const isCashu = paymentMethod === 'cashu'; const hasZapSplit = zapArrays.length > 0 && !isCashu; - const { nutzapRequest } = useNutzap(); + const { nutzapRequest } = useCashu(); const handleSubmit = async (e?: React.FormEvent) => { e?.preventDefault(); diff --git a/src/features/zap/hooks/useCashu.ts b/src/features/zap/hooks/useCashu.ts new file mode 100644 index 000000000..bb1353564 --- /dev/null +++ b/src/features/zap/hooks/useCashu.ts @@ -0,0 +1,83 @@ +import { produce } from 'immer'; +import { create } from 'zustand'; + +import { MastodonClient } from 'soapbox/api/MastodonClient.ts'; +import { WalletData, baseWalletSchema } 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 IWalletInfo { + mints: string[]; + relays: string[]; +} + +interface ICashuState { + nutzapsList: Record; + isLoading: boolean; + error: string | null; + wallet: WalletData | null; + + createWallet: (api: MastodonClient, walletInfo: IWalletInfo) => Promise; + getWallet: (api: MastodonClient, hasMessage?: boolean) => Promise; + nutzapRequest: (api: MastodonClient, account: AccountEntity, amount: number, comment: string, status?: StatusEntity) => void; +} + +export const useCashu = create((set) => ({ + nutzapsList: {}, + isLoading: false, + error: null, + wallet: null, + + createWallet: async (api, walletInfo) => { + 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'); // TO DO: create translated text + set({ wallet: normalizedData }); + } + } catch (e) { + toast.error('An error has occurred'); // TO DO: create translated text + } + }, + + getWallet: async (api, hasMessage = true) => { + try { + const response = await api.get('/api/v1/ditto/cashu/wallet'); + const data: WalletData = await response.json(); + if (data) { + const normalizedData = baseWalletSchema.parse(data); + set({ wallet: normalizedData }); + } + } catch (error) { + if (hasMessage) toast.error('Wallet not found'); + } + }, + + nutzapRequest: async (api, account, amount, comment, status) => { + set((state) => ({ ...state, isLoading: true, error: null })); + + try { + const response = await api.post('/api/v1/ditto/cashu/nutzap', { + amount, + comment, + account_id: account.id, + status_id: status?.id, + }); + + const data = await response.json(); + + set(produce((state) => { + if (status) state.nutzapsList[status.id] = { status, amount, comment }; + state.isLoading = false; + })); + + toast.success(data.message || 'Nutzap sent successfully!'); + } catch (e) { + set((state) => ({ ...state, isLoading: false, error: e instanceof Error ? e.message : 'An unexpected error occurred' })); + toast.error(e instanceof Error ? e.message : 'An unexpected error occurred'); + } + }, +})); \ No newline at end of file diff --git a/src/features/zap/hooks/useNutzap.ts b/src/features/zap/hooks/useNutzap.ts deleted file mode 100644 index 54e0f7f35..000000000 --- a/src/features/zap/hooks/useNutzap.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { produce } from 'immer'; -import { create } from 'zustand'; - -import { MastodonClient } from 'soapbox/api/MastodonClient.ts'; -import toast from 'soapbox/toast.tsx'; - -import type { Account as AccountEntity, Status as StatusEntity } from 'soapbox/types/entities.ts'; - -interface NutzapState { - nutzapsList: Record; - - nutzapRequest: (api: MastodonClient, account: AccountEntity, amount: number, comment: string, status?: StatusEntity) => void; -} - -export const useNutzap = create((set) => ({ - nutzapsList: {}, - - nutzapRequest: async (api, account, amount, comment, status) => { - set(produce((state) => { - state.isLoading = true; state.error = null; - })); - - try { - const response = await api.post('/api/v1/ditto/cashu/nutzap', { - amount, - comment, - account_id: account.id, - status_id: status?.id, - }); - - const data = await response.json(); - - set(produce((state) => { - if (status) state.nutzapsList[status.id] = { status, amount, comment }; - })); - - toast.success(data.message || 'Nutzap sent successfully!'); - } catch (e) { - toast.error(e instanceof Error ? e.message : 'An unexpected error occurred'); - } - }, -})); \ No newline at end of file From b8fa6631adac384cc232c6a72a060fa0fa72f171 Mon Sep 17 00:00:00 2001 From: danidfra Date: Thu, 20 Mar 2025 16:58:52 -0300 Subject: [PATCH 34/62] Refactor fetch code --- .../wallet/components/create-wallet.tsx | 22 +++---------- .../wallet/components/wallet-mints.tsx | 32 ++++++++----------- src/features/wallet/index.tsx | 27 +++------------- 3 files changed, 24 insertions(+), 57 deletions(-) diff --git a/src/features/wallet/components/create-wallet.tsx b/src/features/wallet/components/create-wallet.tsx index 2d9625da0..e8088041b 100644 --- a/src/features/wallet/components/create-wallet.tsx +++ b/src/features/wallet/components/create-wallet.tsx @@ -11,10 +11,9 @@ import Stack from 'soapbox/components/ui/stack.tsx'; import SvgIcon from 'soapbox/components/ui/svg-icon.tsx'; import Text from 'soapbox/components/ui/text.tsx'; import { MintEditor } from 'soapbox/features/wallet/components/editable-lists.tsx'; +import { useCashu } from 'soapbox/features/zap/hooks/useCashu.ts'; import { useApi } from 'soapbox/hooks/useApi.ts'; import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts'; -import { WalletData, baseWalletSchema } from 'soapbox/schemas/wallet.ts'; -import toast from 'soapbox/toast.tsx'; const messages = defineMessages({ title: { id: 'wallet.create_wallet.title', defaultMessage: 'You don\'t have a wallet' }, @@ -23,35 +22,24 @@ const messages = defineMessages({ mints: { id: 'wallet.mints', defaultMessage: 'Mints' }, }); -const CreateWallet: React.FC<{ setWalletData: React.Dispatch> }> = ({ setWalletData }) => { +const CreateWallet = () => { const api = useApi(); const intl = useIntl(); const { account } = useOwnAccount(); const [formActive, setFormActive] = useState(false); const [isLoading, setIsLoading] = useState(false); const [mints, setMints] = useState([]); + const { createWallet } = useCashu(); const handleSubmit = async () => { setIsLoading(true); - const wallet = { + const walletInfo = { mints: mints, relays: [], }; - try { - const response = await api.put('/api/v1/ditto/cashu/wallet', wallet); - const data = await response.json(); - if (data) { - // toast.success('Deu certo garai') - const normalizedData = baseWalletSchema.parse(data); - toast.success('Walllet Created with success'); // TO DO : create translated text - setWalletData(normalizedData); - } - } catch (e) { - toast.error('An error had occured'); // TO DO : create translated text - } - + await createWallet(api, walletInfo); setIsLoading(false); }; diff --git a/src/features/wallet/components/wallet-mints.tsx b/src/features/wallet/components/wallet-mints.tsx index cca555345..5a30df0a6 100644 --- a/src/features/wallet/components/wallet-mints.tsx +++ b/src/features/wallet/components/wallet-mints.tsx @@ -5,8 +5,8 @@ import Button from 'soapbox/components/ui/button.tsx'; import { Column } from 'soapbox/components/ui/column.tsx'; import Stack from 'soapbox/components/ui/stack.tsx'; import { MintEditor } from 'soapbox/features/wallet/components/editable-lists.tsx'; +import { useCashu } from 'soapbox/features/zap/hooks/useCashu.ts'; import { useApi } from 'soapbox/hooks/useApi.ts'; -import { WalletData, baseWalletSchema } from 'soapbox/schemas/wallet.ts'; import toast from 'soapbox/toast.tsx'; import { isURL } from 'soapbox/utils/auth.ts'; @@ -23,27 +23,12 @@ const messages = defineMessages({ const WalletMints = () => { const intl = useIntl(); const api = useApi(); + const { wallet, getWallet } = useCashu(); const [relays, setRelays] = useState([]); const [initialMints, setInitialMints] = useState([]); const [mints, setMints] = useState([]); - const fetchWallet = async () => { - try { - const response = await api.get('/api/v1/ditto/cashu/wallet'); - const data: WalletData = await response.json(); - if (data) { - const normalizedData = baseWalletSchema.parse(data); - setMints(normalizedData.mints); - setInitialMints(normalizedData.mints); - setRelays(normalizedData.relays); - } - - } catch (error) { - toast.error(intl.formatMessage(messages.loadingError)); - } - }; - const handleClick = async () =>{ if (mints.length <= 0) { toast.error(intl.formatMessage(messages.empty)); @@ -70,9 +55,20 @@ const WalletMints = () => { }; useEffect(() => { - fetchWallet(); + getWallet(api, false); + toast.error(intl.formatMessage(messages.loadingError)); }, []); + useEffect( + () => { + if (wallet) { + setMints(wallet.mints ?? []); + setInitialMints(wallet.mints ?? []); + setRelays(wallet.relays ?? []); + } + }, [wallet], + ); + return ( diff --git a/src/features/wallet/index.tsx b/src/features/wallet/index.tsx index ed11e5ca7..7f972d3d5 100644 --- a/src/features/wallet/index.tsx +++ b/src/features/wallet/index.tsx @@ -10,11 +10,10 @@ import { SelectDropdown } from 'soapbox/features/forms/index.tsx'; import Balance from 'soapbox/features/wallet/components/balance.tsx'; import CreateWallet from 'soapbox/features/wallet/components/create-wallet.tsx'; import Transactions from 'soapbox/features/wallet/components/transactions.tsx'; +import { useCashu } from 'soapbox/features/zap/hooks/useCashu.ts'; import { usePaymentMethod } from 'soapbox/features/zap/usePaymentMethod.ts'; import { useApi } from 'soapbox/hooks/useApi.ts'; import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts'; -import { WalletData, baseWalletSchema } from 'soapbox/schemas/wallet.ts'; -import toast from 'soapbox/toast.tsx'; const messages = defineMessages({ @@ -37,29 +36,13 @@ const Wallet = () => { const intl = useIntl(); const { account } = useOwnAccount(); - const [walletData, setWalletData] = useState(undefined); + const { wallet: walletData, getWallet } = useCashu(); const [isLoading, setIsLoading] = useState(true); const { method, changeMethod } = usePaymentMethod(); - const fetchWallet = async () => { - - try { - const response = await api.get('/api/v1/ditto/cashu/wallet'); - const data: WalletData = await response.json(); - if (data) { - const normalizedData = baseWalletSchema.parse(data); - setWalletData(normalizedData); - } - - } catch (error) { - toast.error('Wallet not found'); - } finally { - setIsLoading(false); - } - }; - useEffect(() => { - fetchWallet(); + getWallet(api); + setIsLoading(false); }, []); if (!account) return null; @@ -118,7 +101,7 @@ const Wallet = () => { : <> - + From 0f3d5e46cf0f47f846e430e2cbf820ee724ba132 Mon Sep 17 00:00:00 2001 From: danidfra Date: Thu, 20 Mar 2025 17:01:36 -0300 Subject: [PATCH 35/62] Refactor fetch logic and implement expiry check --- src/features/wallet/components/balance.tsx | 55 +++++++++++----------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/src/features/wallet/components/balance.tsx b/src/features/wallet/components/balance.tsx index 2b48890e1..602b30251 100644 --- a/src/features/wallet/components/balance.tsx +++ b/src/features/wallet/components/balance.tsx @@ -19,9 +19,10 @@ import Input from 'soapbox/components/ui/input.tsx'; import Stack from 'soapbox/components/ui/stack.tsx'; import Text from 'soapbox/components/ui/text.tsx'; import { SelectDropdown } from 'soapbox/features/forms/index.tsx'; +import { useCashu } from 'soapbox/features/zap/hooks/useCashu.ts'; import { useApi } from 'soapbox/hooks/useApi.ts'; import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts'; -import { Quote, WalletData, baseWalletSchema, quoteShema } from 'soapbox/schemas/wallet.ts'; +import { Quote, quoteShema } from 'soapbox/schemas/wallet.ts'; import toast from 'soapbox/toast.tsx'; @@ -45,7 +46,6 @@ interface AmountProps { interface NewMintProps { list: string[]; onBack: () => void; - onChange: () => void; } const openExtension = async (invoice: string) => { @@ -79,7 +79,7 @@ const Amount = ({ amount, onMintClick }: AmountProps) => { ); }; -const NewMint = ({ onBack, list, onChange }: NewMintProps) => { +const NewMint = ({ onBack, list }: NewMintProps) => { const [mintAmount, setMintAmount] = useState(''); const [quote, setQuote] = useState(() => { const storedQuote = localStorage.getItem('soapbox:wallet:quote'); @@ -90,6 +90,7 @@ const NewMint = ({ onBack, list, onChange }: NewMintProps) => { const [currentState, setCurrentState] = useState<'loading' | 'paid' | 'default'>('default'); const api = useApi(); const intl = useIntl(); + const { getWallet } = useCashu(); const now = Math.floor(Date.now() / 1000); @@ -109,8 +110,9 @@ const NewMint = ({ onBack, list, onChange }: NewMintProps) => { setCurrentState('paid'); } else { toast.success(intl.formatMessage(messages.paidMessage)); - onChange(); onBack(); + // onChange(); + getWallet(api); handleClean(); setCurrentState('default'); } @@ -139,8 +141,7 @@ const NewMint = ({ onBack, list, onChange }: NewMintProps) => { } else { if (now > quote.expiry) { toast.error(intl.formatMessage(messages.expired)); - setQuote(undefined); - setCurrentState('default'); + handleClean(); } else { checkQuoteStatus(quote.quote); } @@ -157,11 +158,16 @@ const NewMint = ({ onBack, list, onChange }: NewMintProps) => { const processQuote = async () => { if (quote && !hasProcessedQuote) { const invoice = await openExtension(quote.request); - if (invoice === undefined) { + if (invoice === undefined && now < quote.expiry) { await checkQuoteStatus(quote.quote); } - setCurrentState('paid'); - setHasProcessedQuote(true); + if (now > quote.expiry) { + handleClean(); + toast.error(intl.formatMessage(messages.expired)); + } else { + setCurrentState('paid'); + setHasProcessedQuote(true); + } } }; @@ -230,36 +236,31 @@ const NewMint = ({ onBack, list, onChange }: NewMintProps) => { }; const Balance = () => { + const api = useApi(); + const { wallet, getWallet } = useCashu(); const [amount, setAmount] = useState(0); const [mints, setMints] = useState([]); const { account } = useOwnAccount(); const [current, setCurrent] = useState('balance'); - const api = useApi(); - - const fetchWallet = async () => { - try { - const response = await api.get('/api/v1/ditto/cashu/wallet'); - const data: WalletData = await response.json(); - if (data) { - const normalizedData = baseWalletSchema.parse(data); - setMints([...normalizedData.mints]); - setAmount(normalizedData.balance); - } - - } catch (error) { - toast.error('Wallet not found'); - } - }; const items = { balance: setCurrent('newMint')} />, - newMint: setCurrent('balance')} list={mints} onChange={fetchWallet} />, + newMint: setCurrent('balance')} list={mints} />, }; useEffect(() => { - fetchWallet(); + getWallet(api); }, []); + useEffect( + () => { + if (wallet){ + setMints([...wallet.mints]); + setAmount(wallet.balance); + } + }, [wallet], + ); + if (!account) { return null; } From f5a6d10e05809fc1a83c6264b603e10c9e61065e Mon Sep 17 00:00:00 2001 From: danidfra Date: Thu, 20 Mar 2025 20:31:16 -0300 Subject: [PATCH 36/62] Typo "quoteSchema" --- src/features/wallet/components/balance.tsx | 4 ++-- src/schemas/wallet.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/features/wallet/components/balance.tsx b/src/features/wallet/components/balance.tsx index 602b30251..4cdb57bf7 100644 --- a/src/features/wallet/components/balance.tsx +++ b/src/features/wallet/components/balance.tsx @@ -22,7 +22,7 @@ import { SelectDropdown } from 'soapbox/features/forms/index.tsx'; import { useCashu } from 'soapbox/features/zap/hooks/useCashu.ts'; import { useApi } from 'soapbox/hooks/useApi.ts'; import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts'; -import { Quote, quoteShema } from 'soapbox/schemas/wallet.ts'; +import { Quote, quoteSchema } from 'soapbox/schemas/wallet.ts'; import toast from 'soapbox/toast.tsx'; @@ -128,7 +128,7 @@ const NewMint = ({ onBack, list }: NewMintProps) => { if (!quote) { try { const response = await api.post('/api/v1/ditto/cashu/quote', { mint: mintName, amount: Number(mintAmount) }); - const newQuote = quoteShema.parse(await response.json()); + const newQuote = quoteSchema.parse(await response.json()); localStorage.setItem('soapbox:wallet:quote', JSON.stringify(newQuote)); setQuote(newQuote); setHasProcessedQuote(true); diff --git a/src/schemas/wallet.ts b/src/schemas/wallet.ts index 61c045674..7b645ff09 100644 --- a/src/schemas/wallet.ts +++ b/src/schemas/wallet.ts @@ -8,7 +8,7 @@ const baseWalletSchema = z.object({ balance: z.number(), }); -const quoteShema = z.object({ +const quoteSchema = z.object({ expiry: z.number(), paid: z.boolean(), quote: z.string(), @@ -16,8 +16,8 @@ const quoteShema = z.object({ state: z.enum(['UNPAID', 'PAID', 'ISSUED']), }); -type Quote = z.infer +type Quote = z.infer type WalletData = z.infer; -export { baseWalletSchema, quoteShema, type WalletData, type Quote }; \ No newline at end of file +export { baseWalletSchema, quoteSchema, type WalletData, type Quote }; \ No newline at end of file From e04f7086f8654f4a047fc3e2182e5e57dcfef4a4 Mon Sep 17 00:00:00 2001 From: danidfra Date: Thu, 20 Mar 2025 23:11:53 -0300 Subject: [PATCH 37/62] Add transactions in wallet schema --- src/schemas/wallet.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/schemas/wallet.ts b/src/schemas/wallet.ts index 7b645ff09..5800443d3 100644 --- a/src/schemas/wallet.ts +++ b/src/schemas/wallet.ts @@ -16,8 +16,18 @@ const quoteSchema = z.object({ state: z.enum(['UNPAID', 'PAID', 'ISSUED']), }); +const transactionSchema = z.object({ + amount: z.number(), + created_at: z.number(), + direction: z.enum(['in', 'out']), +}); + +const transactionsSchema = z.array(transactionSchema); + +type Transactions = z.infer + type Quote = z.infer type WalletData = z.infer; -export { baseWalletSchema, quoteSchema, type WalletData, type Quote }; \ No newline at end of file +export { baseWalletSchema, quoteSchema, transactionsSchema, type WalletData, type Quote, type Transactions }; \ No newline at end of file From c3c87c8451b6073cb18704480dc0fdf07b2b1f29 Mon Sep 17 00:00:00 2001 From: danidfra Date: Fri, 21 Mar 2025 00:19:16 -0300 Subject: [PATCH 38/62] Implement transactions --- src/features/wallet/components/balance.tsx | 2 +- .../wallet/components/transactions.tsx | 125 ++++++------------ src/features/zap/hooks/useCashu.ts | 19 ++- src/locales/en.json | 3 +- 4 files changed, 64 insertions(+), 85 deletions(-) diff --git a/src/features/wallet/components/balance.tsx b/src/features/wallet/components/balance.tsx index 4cdb57bf7..85a2d9690 100644 --- a/src/features/wallet/components/balance.tsx +++ b/src/features/wallet/components/balance.tsx @@ -28,7 +28,7 @@ import toast from 'soapbox/toast.tsx'; const messages = defineMessages({ - balance: { id: 'wallet.balance.cashu', defaultMessage: '{amount} sats' }, + balance: { id: 'wallet.sats', defaultMessage: '{amount} sats' }, withdraw: { id: 'wallet.balance.withdraw_button', defaultMessage: 'Withdraw' }, exchange: { id: 'wallet.balance.exchange_button', defaultMessage: 'Exchange' }, mint: { id: 'wallet.balance.mint_button', defaultMessage: 'Mint' }, diff --git a/src/features/wallet/components/transactions.tsx b/src/features/wallet/components/transactions.tsx index 1dbd82846..ca29aee84 100644 --- a/src/features/wallet/components/transactions.tsx +++ b/src/features/wallet/components/transactions.tsx @@ -1,19 +1,19 @@ -import withdrawIcon from '@tabler/icons/outline/cash.svg'; +import arrowBarDownIcon from '@tabler/icons/outline/arrow-bar-down.svg'; +import arrowBarUpIcon from '@tabler/icons/outline/arrow-bar-up.svg'; import moreIcon from '@tabler/icons/outline/dots-circle-horizontal.svg'; -import infoIcon from '@tabler/icons/outline/info-circle.svg'; import questionIcon from '@tabler/icons/outline/question-mark.svg'; -import exchangeIcon from '@tabler/icons/outline/transfer.svg'; -import trendingDown from '@tabler/icons/outline/trending-down.svg'; -import trendingUp from '@tabler/icons/outline/trending-up.svg'; +import { useEffect } from 'react'; +import { FormattedDate, defineMessages, useIntl } from 'react-intl'; -// import IconButton from 'soapbox/components/ui/icon-button.tsx'; import Button from 'soapbox/components/ui/button.tsx'; import Divider from 'soapbox/components/ui/divider.tsx'; import HStack from 'soapbox/components/ui/hstack.tsx'; -import IconButton from 'soapbox/components/ui/icon-button.tsx'; +import Spinner from 'soapbox/components/ui/spinner.tsx'; import Stack from 'soapbox/components/ui/stack.tsx'; import SvgIcon from 'soapbox/components/ui/svg-icon.tsx'; import Text from 'soapbox/components/ui/text.tsx'; +import { useCashu } from 'soapbox/features/zap/hooks/useCashu.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts'; const themes = { @@ -25,38 +25,36 @@ const themes = { success: '!text-success-600', inherit: '!text-inherit', white: '!text-white', - exchange: '!text-blue-600 dark:!text-blue-300', withdraw: '!text-orange-600 dark:!text-orange-300', }; -const capitaliza = (str: string) => { - return str.charAt(0).toLocaleUpperCase() + str.slice(1); -}; - -const transaction = (e: { type: string; amount: number; from?: string | null; to?: string | null; mint?: string | null; date: string }, lastElementIndex: number, index: number) => { +const messages = defineMessages({ + amount: { id: 'wallet.sats', defaultMessage: '{amount} sats' }, + more: { id: 'wallet.transactions.more', defaultMessage: 'More' }, +}); +const TransactionItem = (e: { amount: number; created_at: number ; direction: 'in' | 'out' }, lastElementIndex: number, index: number) => { + const intl = useIntl(); const isLastElement = index === lastElementIndex; - let icon, sla, messageColor: typeof themes[keyof typeof themes] | undefined ; - const { type, amount, date } = e; - switch (type) { - case 'received': - icon = trendingUp; - sla = e.from; + let icon, type, messageColor; + const { direction, amount, created_at } = e; + + const formattedDate = ; + + switch (direction) { + case 'in': + icon = arrowBarDownIcon; + type = 'Received'; messageColor = themes.success; break; - case 'sent': - icon = trendingDown; - sla = e.to; + case 'out': + icon = arrowBarUpIcon; + type = 'Sent'; messageColor = themes.danger; break; - case 'exchanged': - case 'withdrawn': - icon = e.type === 'exchanged' ? exchangeIcon : withdrawIcon; - sla = e.mint || 'MoonFileMoney'; - messageColor = e.type === 'exchanged' ? themes.exchange : themes.withdraw; - break; default: messageColor = 'default'; + type = 'Unknown'; icon = questionIcon; } @@ -71,30 +69,22 @@ const transaction = (e: { type: string; amount: number; from?: string | null; t - {sla} - - - {capitaliza(type)} + {type} - - - {amount} - - - {/* sats */} - - + + {intl.formatMessage(messages.amount, { amount: amount })} + - {date} + {formattedDate} - + {/* */} @@ -103,60 +93,31 @@ const transaction = (e: { type: string; amount: number; from?: string | null; t ); }; -const eG = [ - { - to: null, - from: 'Patrick', - type: 'received', - mint: null, - amount: 100, - date: '02/11/24', - }, - { - to: 'Alex', - from: null, - type: 'sent', - mint: null, - amount: 50, - date: '02/09/24', - }, - { - to: null, - from: null, - type: 'exchanged', - mint: 'MoonFileMoney', - amount: 200, - date: '02/07/24', - }, - { - to: null, - from: null, - type: 'withdrawn', - mint: 'MoonFileMoney', - amount: 300, - date: '02/05/24', - }, -]; const Transactions = () => { + const intl = useIntl(); + const api = useApi(); const { account } = useOwnAccount(); + const { transactions, getTransactions } = useCashu(); + + useEffect( + () => { + getTransactions(api); + } + , []); if (!account) { return null; } - const lastItem = eG.length - 1; - return ( - {eG.map((item, index) => { - return transaction(item, lastItem, index); - })} + {transactions ? transactions.slice(0, 4).map((item, index) => TransactionItem(item, 3, index)) : } diff --git a/src/features/zap/hooks/useCashu.ts b/src/features/zap/hooks/useCashu.ts index bb1353564..dbbe63144 100644 --- a/src/features/zap/hooks/useCashu.ts +++ b/src/features/zap/hooks/useCashu.ts @@ -2,7 +2,7 @@ import { produce } from 'immer'; import { create } from 'zustand'; import { MastodonClient } from 'soapbox/api/MastodonClient.ts'; -import { WalletData, baseWalletSchema } from 'soapbox/schemas/wallet.ts'; +import { Transactions, WalletData, baseWalletSchema, 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'; @@ -17,17 +17,21 @@ interface ICashuState { isLoading: boolean; error: string | null; wallet: WalletData | null; + transactions: Transactions | null; createWallet: (api: MastodonClient, walletInfo: IWalletInfo) => Promise; + getTransactions: (api: MastodonClient) => Promise; getWallet: (api: MastodonClient, hasMessage?: boolean) => Promise; nutzapRequest: (api: MastodonClient, account: AccountEntity, amount: number, comment: string, status?: StatusEntity) => void; } +// TODO: Convert it into a custom hook after the backend is finished export const useCashu = create((set) => ({ nutzapsList: {}, isLoading: false, error: null, wallet: null, + transactions: null, createWallet: async (api, walletInfo) => { try { @@ -56,6 +60,19 @@ export const useCashu = create((set) => ({ } }, + getTransactions: async (api) => { + try { + const response = await api.get('/api/v1/ditto/cashu/transactions'); + const data: WalletData = await response.json(); + if (data) { + const normalizedData = transactionsSchema.parse(data); + set({ transactions: normalizedData }); + } + } catch (error) { + toast.error('Transactions not found'); + } + }, + nutzapRequest: async (api, account, amount, comment, status) => { set((state) => ({ ...state, isLoading: true, error: null })); diff --git a/src/locales/en.json b/src/locales/en.json index 1836cc499..92e964454 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1713,7 +1713,7 @@ "video.play": "Play", "video.unmute": "Unmute sound", "wallet": "Wallet", - "wallet.balance.cashu": "{amount} sats", + "wallet.sats": "{amount} sats", "wallet.balance.exchange_button": "Exchange", "wallet.balance.expired": "Expired", "wallet.balance.mint.paid_message": "Your mint was successful, and your sats are now in your balance. Enjoy!", @@ -1737,6 +1737,7 @@ "wallet.relays.error": "Failed to update mints.", "wallet.relays.sucess": "Relays updated with success!", "wallet.transactions": "Transactions", + "wallet.transactions.more": "More", "who_to_follow.title": "People To Follow", "zap.button.text.rounded": "{method} {amount}K sats", "zap.finish": "Finish", From 6ad87ed3852d7177f06a4f4ebb540cd8dcb24549 Mon Sep 17 00:00:00 2001 From: danidfra Date: Fri, 21 Mar 2025 11:54:22 -0300 Subject: [PATCH 39/62] i18n --- src/locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locales/en.json b/src/locales/en.json index 92e964454..cf4d7ad09 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1713,7 +1713,6 @@ "video.play": "Play", "video.unmute": "Unmute sound", "wallet": "Wallet", - "wallet.sats": "{amount} sats", "wallet.balance.exchange_button": "Exchange", "wallet.balance.expired": "Expired", "wallet.balance.mint.paid_message": "Your mint was successful, and your sats are now in your balance. Enjoy!", @@ -1736,6 +1735,7 @@ "wallet.relays.empty": "At least one relay is required.", "wallet.relays.error": "Failed to update mints.", "wallet.relays.sucess": "Relays updated with success!", + "wallet.sats": "{amount} sats", "wallet.transactions": "Transactions", "wallet.transactions.more": "More", "who_to_follow.title": "People To Follow", From 085d98f7b8e5fe8474fee4a50637f00b476e3f61 Mon Sep 17 00:00:00 2001 From: danidfra Date: Fri, 21 Mar 2025 12:43:53 -0300 Subject: [PATCH 40/62] Refactor wallet-relays and remove unnecessary toast --- .../wallet/components/wallet-mints.tsx | 1 - .../wallet/components/wallet-relays.tsx | 31 ++++++++----------- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/src/features/wallet/components/wallet-mints.tsx b/src/features/wallet/components/wallet-mints.tsx index 5a30df0a6..de7ffbb86 100644 --- a/src/features/wallet/components/wallet-mints.tsx +++ b/src/features/wallet/components/wallet-mints.tsx @@ -56,7 +56,6 @@ const WalletMints = () => { useEffect(() => { getWallet(api, false); - toast.error(intl.formatMessage(messages.loadingError)); }, []); useEffect( diff --git a/src/features/wallet/components/wallet-relays.tsx b/src/features/wallet/components/wallet-relays.tsx index 0e362bafb..f39ac1b5d 100644 --- a/src/features/wallet/components/wallet-relays.tsx +++ b/src/features/wallet/components/wallet-relays.tsx @@ -5,8 +5,8 @@ import Button from 'soapbox/components/ui/button.tsx'; import { Column } from 'soapbox/components/ui/column.tsx'; import Stack from 'soapbox/components/ui/stack.tsx'; import { RelayEditor } from 'soapbox/features/wallet/components/editable-lists.tsx'; +import { useCashu } from 'soapbox/features/zap/hooks/useCashu.ts'; import { useApi } from 'soapbox/hooks/useApi.ts'; -import { WalletData, baseWalletSchema } from 'soapbox/schemas/wallet.ts'; import toast from 'soapbox/toast.tsx'; import { isURL } from 'soapbox/utils/auth.ts'; @@ -23,27 +23,12 @@ const messages = defineMessages({ const WalletRelays = () => { const intl = useIntl(); const api = useApi(); + const { wallet, getWallet } = useCashu(); const [relays, setRelays] = useState([]); const [initialRelays, setInitialRelays] = useState([]); const [mints, setMints] = useState([]); - const fetchWallet = async () => { - try { - const response = await api.get('/api/v1/ditto/cashu/wallet'); - const data: WalletData = await response.json(); - if (data) { - const normalizedData = baseWalletSchema.parse(data); - setMints(normalizedData.mints); - setRelays(normalizedData.relays); - setInitialRelays(normalizedData.relays); - } - - } catch (error) { - toast.error(intl.formatMessage(messages.loadingError)); - } - }; - const handleClick = async () =>{ if (relays.length <= 0) { toast.error(intl.formatMessage(messages.empty)); @@ -70,9 +55,19 @@ const WalletRelays = () => { }; useEffect(() => { - fetchWallet(); + getWallet(api, false); }, []); + useEffect( + () => { + if (wallet) { + setMints(wallet.mints ?? []); + setInitialRelays(wallet.relays ?? []); + setRelays(wallet.relays ?? []); + } + }, [wallet], + ); + return ( From 3307ae57fbab228e66930528c1333bbe075e6f3b Mon Sep 17 00:00:00 2001 From: danidfra Date: Fri, 21 Mar 2025 13:00:21 -0300 Subject: [PATCH 41/62] Update wallet-relays and wallet-mints --- src/features/wallet/components/wallet-mints.tsx | 4 +++- src/features/wallet/components/wallet-relays.tsx | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/features/wallet/components/wallet-mints.tsx b/src/features/wallet/components/wallet-mints.tsx index de7ffbb86..8521dd771 100644 --- a/src/features/wallet/components/wallet-mints.tsx +++ b/src/features/wallet/components/wallet-mints.tsx @@ -30,7 +30,7 @@ const WalletMints = () => { const [mints, setMints] = useState([]); const handleClick = async () =>{ - if (mints.length <= 0) { + if (mints.length === 0) { toast.error(intl.formatMessage(messages.empty)); return; } @@ -46,6 +46,8 @@ const WalletMints = () => { try { await api.put('/api/v1/ditto/cashu/wallet', { mints: mints, relays: relays }); + setInitialMints(mints); + toast.success(intl.formatMessage(messages.sucess)); } catch (error) { const errorMessage = error instanceof Error ? error.message : intl.formatMessage(messages.error); diff --git a/src/features/wallet/components/wallet-relays.tsx b/src/features/wallet/components/wallet-relays.tsx index f39ac1b5d..cd8685c21 100644 --- a/src/features/wallet/components/wallet-relays.tsx +++ b/src/features/wallet/components/wallet-relays.tsx @@ -30,7 +30,7 @@ const WalletRelays = () => { const [mints, setMints] = useState([]); const handleClick = async () =>{ - if (relays.length <= 0) { + if (relays.length === 0) { toast.error(intl.formatMessage(messages.empty)); return; } @@ -46,6 +46,7 @@ const WalletRelays = () => { try { await api.put('/api/v1/ditto/cashu/wallet', { mints: mints, relays: relays }); + setInitialRelays(relays); toast.success(intl.formatMessage(messages.sucess)); } catch (error) { const errorMessage = error instanceof Error ? error.message : intl.formatMessage(messages.error); From 27aa66d279fe60fc429c63e0d2f50b5a024d29d2 Mon Sep 17 00:00:00 2001 From: danidfra Date: Fri, 21 Mar 2025 16:51:17 -0300 Subject: [PATCH 42/62] Change hook logic --- src/components/pure-status-action-bar.tsx | 16 +- src/components/status-action-bar.tsx | 14 +- src/features/account/components/header.tsx | 14 +- src/features/wallet/components/balance.tsx | 13 +- .../wallet/components/create-wallet.tsx | 8 +- .../wallet/components/transactions.tsx | 13 +- .../wallet/components/wallet-mints.tsx | 8 +- .../wallet/components/wallet-relays.tsx | 8 +- src/features/wallet/index.tsx | 15 +- .../zap/components/pay-request-form.tsx | 8 +- src/features/zap/hooks/useCashu.ts | 100 ------------- src/features/zap/hooks/useHooks.ts | 138 ++++++++++++++++++ 12 files changed, 166 insertions(+), 189 deletions(-) delete mode 100644 src/features/zap/hooks/useCashu.ts create mode 100644 src/features/zap/hooks/useHooks.ts diff --git a/src/components/pure-status-action-bar.tsx b/src/components/pure-status-action-bar.tsx index 1a527e82c..ed90e79f5 100644 --- a/src/components/pure-status-action-bar.tsx +++ b/src/components/pure-status-action-bar.tsx @@ -29,7 +29,6 @@ import thumbUpIcon from '@tabler/icons/outline/thumb-up.svg'; import trashIcon from '@tabler/icons/outline/trash.svg'; import uploadIcon from '@tabler/icons/outline/upload.svg'; import volume3Icon from '@tabler/icons/outline/volume-3.svg'; -import { useEffect } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { useHistory, useRouteMatch } from 'react-router-dom'; @@ -49,8 +48,7 @@ import DropdownMenu from 'soapbox/components/dropdown-menu/index.ts'; import PureStatusReactionWrapper from 'soapbox/components/pure-status-reaction-wrapper.tsx'; import StatusActionButton from 'soapbox/components/status-action-button.tsx'; import HStack from 'soapbox/components/ui/hstack.tsx'; -import { useCashu } from 'soapbox/features/zap/hooks/useCashu.ts'; -import { useApi } from 'soapbox/hooks/useApi.ts'; +import { useNutzapRequest, useWallet } from 'soapbox/features/zap/hooks/useHooks.ts'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; import { useDislike } from 'soapbox/hooks/useDislike.ts'; @@ -166,7 +164,6 @@ const PureStatusActionBar: React.FC = ({ statusActionButtonTheme = 'default', }) => { const intl = useIntl(); - const api = useApi(); // TODO: Remove this part after patrick implement in backend const history = useHistory(); const dispatch = useAppDispatch(); const match = useRouteMatch<{ groupSlug: string }>('/group/:groupSlug'); @@ -183,8 +180,9 @@ const PureStatusActionBar: React.FC = ({ const features = useFeatures(); const { boostModal, deleteModal } = useSettings(); - const { wallet, getWallet, nutzapsList } = useCashu(); - const isNutzapped = Object.keys(nutzapsList).some((nutzap)=> nutzap === status.id); // TODO: Remove "getWallet" to + const { wallet } = useWallet(); + const { nutzapsList } = useNutzapRequest(); + const isNutzapped = Object.keys(nutzapsList).some((nutzap)=> nutzap === status.id); // TODO: Remove "getWallet" after been in backend const { account } = useOwnAccount(); const isStaff = account ? account.staff : false; @@ -200,12 +198,6 @@ const PureStatusActionBar: React.FC = ({ const { unpinFromGroup, pinToGroup } = usePinGroup(); const { initReport } = useInitReport(); - useEffect( - () => { - getWallet(api, false); - } // TODO: Remove - , []); - if (!status) { return null; } diff --git a/src/components/status-action-bar.tsx b/src/components/status-action-bar.tsx index 0d5ef1181..ff72a3664 100644 --- a/src/components/status-action-bar.tsx +++ b/src/components/status-action-bar.tsx @@ -29,7 +29,6 @@ import thumbUpIcon from '@tabler/icons/outline/thumb-up.svg'; import trashIcon from '@tabler/icons/outline/trash.svg'; import uploadIcon from '@tabler/icons/outline/upload.svg'; import volume3Icon from '@tabler/icons/outline/volume-3.svg'; -import { useEffect } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { useHistory, useRouteMatch } from 'react-router-dom'; @@ -50,8 +49,7 @@ import DropdownMenu from 'soapbox/components/dropdown-menu/index.ts'; import StatusActionButton from 'soapbox/components/status-action-button.tsx'; import StatusReactionWrapper from 'soapbox/components/status-reaction-wrapper.tsx'; import HStack from 'soapbox/components/ui/hstack.tsx'; -import { useCashu } from 'soapbox/features/zap/hooks/useCashu.ts'; -import { useApi } from 'soapbox/hooks/useApi.ts'; +import { useNutzapRequest, useWallet } from 'soapbox/features/zap/hooks/useHooks.ts'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; import { useFeatures } from 'soapbox/hooks/useFeatures.ts'; @@ -162,7 +160,6 @@ const StatusActionBar: React.FC = ({ }) => { const intl = useIntl(); const history = useHistory(); - const api = useApi(); // TODO: Remove this part after patrick implement in backend const dispatch = useAppDispatch(); const match = useRouteMatch<{ groupSlug: string }>('/group/:groupSlug'); @@ -178,7 +175,8 @@ const StatusActionBar: React.FC = ({ const features = useFeatures(); const { boostModal, deleteModal } = useSettings(); - const { wallet, getWallet, nutzapsList } = useCashu(); + const { wallet } = useWallet(); + const { nutzapsList } = useNutzapRequest(); const isNutzapped = Object.keys(nutzapsList).some((nutzap)=> nutzap === status.id); const { account } = useOwnAccount(); @@ -188,12 +186,6 @@ const StatusActionBar: React.FC = ({ const { toggleReblog } = useReblog(); const { bookmark, unbookmark } = useBookmark(); - useEffect( - () => { - getWallet(api, false); - } // TODO: remove - , []); - if (!status) { return null; } diff --git a/src/features/account/components/header.tsx b/src/features/account/components/header.tsx index 355328517..c909f8f86 100644 --- a/src/features/account/components/header.tsx +++ b/src/features/account/components/header.tsx @@ -21,7 +21,6 @@ import userIcon from '@tabler/icons/outline/user.svg'; import { useMutation } from '@tanstack/react-query'; import { List as ImmutableList } from 'immutable'; import { nip19 } from 'nostr-tools'; -import { useEffect } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { useHistory } from 'react-router-dom'; @@ -45,9 +44,8 @@ import VerificationBadge from 'soapbox/components/verification-badge.tsx'; import MovedNote from 'soapbox/features/account-timeline/components/moved-note.tsx'; import ActionButton from 'soapbox/features/ui/components/action-button.tsx'; import SubscriptionButton from 'soapbox/features/ui/components/subscription-button.tsx'; -import { useCashu } from 'soapbox/features/zap/hooks/useCashu.ts'; +import { useWallet } from 'soapbox/features/zap/hooks/useHooks.ts'; import { usePaymentMethod } from 'soapbox/features/zap/usePaymentMethod.ts'; -import { useApi } from 'soapbox/hooks/useApi.ts'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; import { useFeatures } from 'soapbox/hooks/useFeatures.ts'; @@ -113,7 +111,6 @@ interface IHeader { const Header: React.FC = ({ account }) => { const intl = useIntl(); const history = useHistory(); - const api = useApi(); // TODO: Remove this part after patrick implement in backend const dispatch = useAppDispatch(); const features = useFeatures(); @@ -124,7 +121,7 @@ const Header: React.FC = ({ account }) => { const { software } = useAppSelector((state) => parseVersion(state.instance.version)); - const { wallet, getWallet } = useCashu(); + const { wallet } = useWallet(); const { getOrCreateChatByAccountId } = useChats(); @@ -145,13 +142,6 @@ const Header: React.FC = ({ account }) => { }, }); - - useEffect( - () => { - getWallet(api, false); - } // TODO: remove - , []); - if (!account) { return (
diff --git a/src/features/wallet/components/balance.tsx b/src/features/wallet/components/balance.tsx index 85a2d9690..dd14d883c 100644 --- a/src/features/wallet/components/balance.tsx +++ b/src/features/wallet/components/balance.tsx @@ -19,7 +19,7 @@ import Input from 'soapbox/components/ui/input.tsx'; import Stack from 'soapbox/components/ui/stack.tsx'; import Text from 'soapbox/components/ui/text.tsx'; import { SelectDropdown } from 'soapbox/features/forms/index.tsx'; -import { useCashu } from 'soapbox/features/zap/hooks/useCashu.ts'; +import { useWallet } from 'soapbox/features/zap/hooks/useHooks.ts'; import { useApi } from 'soapbox/hooks/useApi.ts'; import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts'; import { Quote, quoteSchema } from 'soapbox/schemas/wallet.ts'; @@ -90,7 +90,7 @@ const NewMint = ({ onBack, list }: NewMintProps) => { const [currentState, setCurrentState] = useState<'loading' | 'paid' | 'default'>('default'); const api = useApi(); const intl = useIntl(); - const { getWallet } = useCashu(); + const { getWallet } = useWallet(); const now = Math.floor(Date.now() / 1000); @@ -112,7 +112,7 @@ const NewMint = ({ onBack, list }: NewMintProps) => { toast.success(intl.formatMessage(messages.paidMessage)); onBack(); // onChange(); - getWallet(api); + getWallet(); // TODO: Remove handleClean(); setCurrentState('default'); } @@ -236,8 +236,7 @@ const NewMint = ({ onBack, list }: NewMintProps) => { }; const Balance = () => { - const api = useApi(); - const { wallet, getWallet } = useCashu(); + const { wallet } = useWallet(); const [amount, setAmount] = useState(0); const [mints, setMints] = useState([]); const { account } = useOwnAccount(); @@ -248,10 +247,6 @@ const Balance = () => { newMint: setCurrent('balance')} list={mints} />, }; - useEffect(() => { - getWallet(api); - }, []); - useEffect( () => { if (wallet){ diff --git a/src/features/wallet/components/create-wallet.tsx b/src/features/wallet/components/create-wallet.tsx index e8088041b..7c1a51e1a 100644 --- a/src/features/wallet/components/create-wallet.tsx +++ b/src/features/wallet/components/create-wallet.tsx @@ -11,8 +11,7 @@ import Stack from 'soapbox/components/ui/stack.tsx'; import SvgIcon from 'soapbox/components/ui/svg-icon.tsx'; import Text from 'soapbox/components/ui/text.tsx'; import { MintEditor } from 'soapbox/features/wallet/components/editable-lists.tsx'; -import { useCashu } from 'soapbox/features/zap/hooks/useCashu.ts'; -import { useApi } from 'soapbox/hooks/useApi.ts'; +import { useWallet } from 'soapbox/features/zap/hooks/useHooks.ts'; import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts'; const messages = defineMessages({ @@ -23,13 +22,12 @@ const messages = defineMessages({ }); const CreateWallet = () => { - const api = useApi(); const intl = useIntl(); const { account } = useOwnAccount(); const [formActive, setFormActive] = useState(false); const [isLoading, setIsLoading] = useState(false); const [mints, setMints] = useState([]); - const { createWallet } = useCashu(); + const { createWallet } = useWallet(); const handleSubmit = async () => { setIsLoading(true); @@ -39,7 +37,7 @@ const CreateWallet = () => { relays: [], }; - await createWallet(api, walletInfo); + await createWallet(walletInfo); setIsLoading(false); }; diff --git a/src/features/wallet/components/transactions.tsx b/src/features/wallet/components/transactions.tsx index ca29aee84..9b50ab9f5 100644 --- a/src/features/wallet/components/transactions.tsx +++ b/src/features/wallet/components/transactions.tsx @@ -2,7 +2,6 @@ import arrowBarDownIcon from '@tabler/icons/outline/arrow-bar-down.svg'; import arrowBarUpIcon from '@tabler/icons/outline/arrow-bar-up.svg'; import moreIcon from '@tabler/icons/outline/dots-circle-horizontal.svg'; import questionIcon from '@tabler/icons/outline/question-mark.svg'; -import { useEffect } from 'react'; import { FormattedDate, defineMessages, useIntl } from 'react-intl'; import Button from 'soapbox/components/ui/button.tsx'; @@ -12,8 +11,7 @@ import Spinner from 'soapbox/components/ui/spinner.tsx'; import Stack from 'soapbox/components/ui/stack.tsx'; import SvgIcon from 'soapbox/components/ui/svg-icon.tsx'; import Text from 'soapbox/components/ui/text.tsx'; -import { useCashu } from 'soapbox/features/zap/hooks/useCashu.ts'; -import { useApi } from 'soapbox/hooks/useApi.ts'; +import { useTransactions } from 'soapbox/features/zap/hooks/useHooks.ts'; import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts'; const themes = { @@ -95,15 +93,8 @@ const TransactionItem = (e: { amount: number; created_at: number ; direction: ' const Transactions = () => { const intl = useIntl(); - const api = useApi(); const { account } = useOwnAccount(); - const { transactions, getTransactions } = useCashu(); - - useEffect( - () => { - getTransactions(api); - } - , []); + const { transactions } = useTransactions(); if (!account) { return null; diff --git a/src/features/wallet/components/wallet-mints.tsx b/src/features/wallet/components/wallet-mints.tsx index 8521dd771..c9ca48c5b 100644 --- a/src/features/wallet/components/wallet-mints.tsx +++ b/src/features/wallet/components/wallet-mints.tsx @@ -5,7 +5,7 @@ import Button from 'soapbox/components/ui/button.tsx'; import { Column } from 'soapbox/components/ui/column.tsx'; import Stack from 'soapbox/components/ui/stack.tsx'; import { MintEditor } from 'soapbox/features/wallet/components/editable-lists.tsx'; -import { useCashu } from 'soapbox/features/zap/hooks/useCashu.ts'; +import { useWallet } from 'soapbox/features/zap/hooks/useHooks.ts'; import { useApi } from 'soapbox/hooks/useApi.ts'; import toast from 'soapbox/toast.tsx'; import { isURL } from 'soapbox/utils/auth.ts'; @@ -23,7 +23,7 @@ const messages = defineMessages({ const WalletMints = () => { const intl = useIntl(); const api = useApi(); - const { wallet, getWallet } = useCashu(); + const { wallet } = useWallet(); const [relays, setRelays] = useState([]); const [initialMints, setInitialMints] = useState([]); @@ -56,10 +56,6 @@ const WalletMints = () => { } }; - useEffect(() => { - getWallet(api, false); - }, []); - useEffect( () => { if (wallet) { diff --git a/src/features/wallet/components/wallet-relays.tsx b/src/features/wallet/components/wallet-relays.tsx index cd8685c21..74c4bcf79 100644 --- a/src/features/wallet/components/wallet-relays.tsx +++ b/src/features/wallet/components/wallet-relays.tsx @@ -5,7 +5,7 @@ import Button from 'soapbox/components/ui/button.tsx'; import { Column } from 'soapbox/components/ui/column.tsx'; import Stack from 'soapbox/components/ui/stack.tsx'; import { RelayEditor } from 'soapbox/features/wallet/components/editable-lists.tsx'; -import { useCashu } from 'soapbox/features/zap/hooks/useCashu.ts'; +import { useWallet } from 'soapbox/features/zap/hooks/useHooks.ts'; import { useApi } from 'soapbox/hooks/useApi.ts'; import toast from 'soapbox/toast.tsx'; import { isURL } from 'soapbox/utils/auth.ts'; @@ -23,7 +23,7 @@ const messages = defineMessages({ const WalletRelays = () => { const intl = useIntl(); const api = useApi(); - const { wallet, getWallet } = useCashu(); + const { wallet } = useWallet(); const [relays, setRelays] = useState([]); const [initialRelays, setInitialRelays] = useState([]); @@ -55,10 +55,6 @@ const WalletRelays = () => { } }; - useEffect(() => { - getWallet(api, false); - }, []); - useEffect( () => { if (wallet) { diff --git a/src/features/wallet/index.tsx b/src/features/wallet/index.tsx index 7f972d3d5..b61ae56b5 100644 --- a/src/features/wallet/index.tsx +++ b/src/features/wallet/index.tsx @@ -1,4 +1,3 @@ -import { useEffect, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import List, { ListItem } from 'soapbox/components/list.tsx'; @@ -10,15 +9,14 @@ import { SelectDropdown } from 'soapbox/features/forms/index.tsx'; import Balance from 'soapbox/features/wallet/components/balance.tsx'; import CreateWallet from 'soapbox/features/wallet/components/create-wallet.tsx'; import Transactions from 'soapbox/features/wallet/components/transactions.tsx'; -import { useCashu } from 'soapbox/features/zap/hooks/useCashu.ts'; +import { useWallet } from 'soapbox/features/zap/hooks/useHooks.ts'; import { usePaymentMethod } from 'soapbox/features/zap/usePaymentMethod.ts'; -import { useApi } from 'soapbox/hooks/useApi.ts'; import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts'; const messages = defineMessages({ payment: { id: 'wallet.payment', defaultMessage: 'Payment Method' }, - relays: { id: 'wallet.relays', defaultMessage: 'Relays' }, + relays: { id: 'wallet.relays', defaultMessage: 'Wallet Relays' }, transactions: { id: 'wallet.transactions', defaultMessage: 'Transactions' }, wallet: { id: 'wallet', defaultMessage: 'Wallet' }, management: { id: 'wallet.management', defaultMessage: 'Wallet Management' }, @@ -32,19 +30,12 @@ const paymentMethods = { /** User Wallet page. */ const Wallet = () => { - const api = useApi(); const intl = useIntl(); const { account } = useOwnAccount(); - const { wallet: walletData, getWallet } = useCashu(); - const [isLoading, setIsLoading] = useState(true); + const { wallet: walletData, isLoading } = useWallet(); const { method, changeMethod } = usePaymentMethod(); - useEffect(() => { - getWallet(api); - setIsLoading(false); - }, []); - if (!account) return null; return ( diff --git a/src/features/zap/components/pay-request-form.tsx b/src/features/zap/components/pay-request-form.tsx index d57d2fddf..f05f4551b 100644 --- a/src/features/zap/components/pay-request-form.tsx +++ b/src/features/zap/components/pay-request-form.tsx @@ -21,9 +21,8 @@ import Input from 'soapbox/components/ui/input.tsx'; import Stack from 'soapbox/components/ui/stack.tsx'; import SvgIcon from 'soapbox/components/ui/svg-icon.tsx'; import Text from 'soapbox/components/ui/text.tsx'; -import { useCashu } from 'soapbox/features/zap/hooks/useCashu.ts'; +import { useNutzapRequest } from 'soapbox/features/zap/hooks/useHooks.ts'; import { usePaymentMethod } from 'soapbox/features/zap/usePaymentMethod.ts'; -import { useApi } from 'soapbox/hooks/useApi.ts'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; import { emojifyText } from 'soapbox/utils/emojify.tsx'; import { capitalize } from 'soapbox/utils/strings.ts'; @@ -57,7 +56,6 @@ const messages = defineMessages({ const PayRequestForm = ({ account, status, onClose }: IPayRequestForm) => { const intl = useIntl(); - const api = useApi(); const dispatch = useAppDispatch(); const [zapComment, setZapComment] = useState(''); const [amount, setAmount] = useState(50); @@ -66,7 +64,7 @@ const PayRequestForm = ({ account, status, onClose }: IPayRequestForm) => { const { method: paymentMethod } = usePaymentMethod(); const isCashu = paymentMethod === 'cashu'; const hasZapSplit = zapArrays.length > 0 && !isCashu; - const { nutzapRequest } = useCashu(); + const { nutzapRequest } = useNutzapRequest(); const handleSubmit = async (e?: React.FormEvent) => { e?.preventDefault(); @@ -74,7 +72,7 @@ const PayRequestForm = ({ account, status, onClose }: IPayRequestForm) => { const splitData = { hasZapSplit, zapSplitAccounts, splitValues }; if (isCashu) { - nutzapRequest(api, account, amount, zapComment, status); + nutzapRequest(account, amount, zapComment, status); dispatch(closeModal('PAY_REQUEST')); return; } diff --git a/src/features/zap/hooks/useCashu.ts b/src/features/zap/hooks/useCashu.ts deleted file mode 100644 index dbbe63144..000000000 --- a/src/features/zap/hooks/useCashu.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { produce } from 'immer'; -import { create } from 'zustand'; - -import { MastodonClient } from 'soapbox/api/MastodonClient.ts'; -import { Transactions, WalletData, baseWalletSchema, 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 IWalletInfo { - mints: string[]; - relays: string[]; -} - -interface ICashuState { - nutzapsList: Record; - isLoading: boolean; - error: string | null; - wallet: WalletData | null; - transactions: Transactions | null; - - createWallet: (api: MastodonClient, walletInfo: IWalletInfo) => Promise; - getTransactions: (api: MastodonClient) => Promise; - getWallet: (api: MastodonClient, hasMessage?: boolean) => Promise; - nutzapRequest: (api: MastodonClient, account: AccountEntity, amount: number, comment: string, status?: StatusEntity) => void; -} - -// TODO: Convert it into a custom hook after the backend is finished -export const useCashu = create((set) => ({ - nutzapsList: {}, - isLoading: false, - error: null, - wallet: null, - transactions: null, - - createWallet: async (api, walletInfo) => { - 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'); // TO DO: create translated text - set({ wallet: normalizedData }); - } - } catch (e) { - toast.error('An error has occurred'); // TO DO: create translated text - } - }, - - getWallet: async (api, hasMessage = true) => { - try { - const response = await api.get('/api/v1/ditto/cashu/wallet'); - const data: WalletData = await response.json(); - if (data) { - const normalizedData = baseWalletSchema.parse(data); - set({ wallet: normalizedData }); - } - } catch (error) { - if (hasMessage) toast.error('Wallet not found'); - } - }, - - getTransactions: async (api) => { - try { - const response = await api.get('/api/v1/ditto/cashu/transactions'); - const data: WalletData = await response.json(); - if (data) { - const normalizedData = transactionsSchema.parse(data); - set({ transactions: normalizedData }); - } - } catch (error) { - toast.error('Transactions not found'); - } - }, - - nutzapRequest: async (api, account, amount, comment, status) => { - set((state) => ({ ...state, isLoading: true, error: null })); - - try { - const response = await api.post('/api/v1/ditto/cashu/nutzap', { - amount, - comment, - account_id: account.id, - status_id: status?.id, - }); - - const data = await response.json(); - - set(produce((state) => { - if (status) state.nutzapsList[status.id] = { status, amount, comment }; - state.isLoading = false; - })); - - toast.success(data.message || 'Nutzap sent successfully!'); - } catch (e) { - set((state) => ({ ...state, isLoading: false, error: e instanceof Error ? e.message : 'An unexpected error occurred' })); - toast.error(e instanceof Error ? e.message : 'An unexpected error occurred'); - } - }, -})); \ No newline at end of file diff --git a/src/features/zap/hooks/useHooks.ts b/src/features/zap/hooks/useHooks.ts new file mode 100644 index 000000000..320a724d1 --- /dev/null +++ b/src/features/zap/hooks/useHooks.ts @@ -0,0 +1,138 @@ +import { useEffect, useState } from 'react'; + +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { Transactions, WalletData, baseWalletSchema, 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 IWalletInfo { + mints: string[]; + relays: string[]; +} + +const useWallet = () => { + const api = useApi(); + const [wallet, setWallet] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(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: WalletData = await response.json(); + if (data) { + const normalizedData = baseWalletSchema.parse(data); + setWallet(normalizedData); + } + } catch (err) { + const messageError = err instanceof Error ? err.message : 'Wallet not found'; + if (hasMessage) toast.error(messageError); + setError(messageError); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + if (!wallet) { + getWallet(false); + } + }, [wallet]); + + return { wallet, isLoading, error, createWallet, getWallet }; +}; + +const useTransactions = () => { + const api = useApi(); + const [transactions, setTransactions] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const getTransactions = async () => { + setIsLoading(true); + try { + const response = await api.get('/api/v1/ditto/cashu/transactions'); + const data: Transactions = await response.json(); + if (data) { + const normalizedData = transactionsSchema.parse(data); + setTransactions(normalizedData); + } + } catch (err) { + const messageError = err instanceof Error ? err.message : 'Transactions not found'; + toast.error(messageError); + setError(messageError); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + if (!transactions) { + getTransactions(); + } + }, [transactions]); + + return { transactions, isLoading, error, getTransactions }; +}; + +const useNutzapRequest = () => { + const api = useApi(); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [nutzapsList, setNutzapsList] = useState>({}); + + const nutzapRequest = async (account: AccountEntity, amount: number, comment: string, status?: StatusEntity) => { + setIsLoading(true); + setError(null); + try { + const response = await api.post('/api/v1/ditto/cashu/nutzap', { + amount, + comment, + account_id: account.id, + status_id: status?.id, + }); + + const data = await response.json(); + + if (status) { + setNutzapsList((prevState) => ({ + ...prevState, + [status.id]: { status, amount, comment }, + })); + } + + toast.success(data.message || 'Nutzap sent successfully!'); + } catch (err) { + const messageError = err instanceof Error ? err.message : 'An unexpected error occurred'; + setError(messageError); + toast.error(messageError); + } finally { + setIsLoading(false); + } + }; + + return { nutzapsList, isLoading, error, nutzapRequest }; +}; + +export { useWallet, useTransactions, useNutzapRequest }; \ No newline at end of file From f7bdd066b89916edde8a0041d1d7dd10dec55dab Mon Sep 17 00:00:00 2001 From: danidfra Date: Sun, 23 Mar 2025 17:17:44 -0300 Subject: [PATCH 43/62] Update hook --- src/features/zap/hooks/useHooks.ts | 46 +++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/src/features/zap/hooks/useHooks.ts b/src/features/zap/hooks/useHooks.ts index 320a724d1..5600782ad 100644 --- a/src/features/zap/hooks/useHooks.ts +++ b/src/features/zap/hooks/useHooks.ts @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react'; +import { create } from 'zustand'; import { useApi } from 'soapbox/hooks/useApi.ts'; import { Transactions, WalletData, baseWalletSchema, transactionsSchema } from 'soapbox/schemas/wallet.ts'; @@ -6,14 +7,40 @@ import toast from 'soapbox/toast.tsx'; import type { Account as AccountEntity, Status as StatusEntity } from 'soapbox/types/entities.ts'; +interface WalletState { + wallet: WalletData | null; + transactions: Transactions | null; + nutzapsList: Record; // TODO: remove + + setWallet: (wallet: WalletData | null) => void; + setTransactions: (transactions: Transactions | null) => void; + addNutzap: (statusId: string, data: { status: StatusEntity; amount: number; comment: string }) => void; +} + interface IWalletInfo { mints: string[]; relays: string[]; } +const useWalletStore = create((set) => ({ + wallet: null, + transactions: null, + nutzapsList: {}, + + setWallet: (wallet) => set({ wallet }), + setTransactions: (transactions) => set({ transactions }), + addNutzap: (statusId, data) => + set((state) => ({ + nutzapsList: { + ...state.nutzapsList, + [statusId]: data, + }, + })), +})); + const useWallet = () => { const api = useApi(); - const [wallet, setWallet] = useState(null); + const { wallet, setWallet } = useWalletStore(); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); @@ -40,7 +67,7 @@ const useWallet = () => { setIsLoading(true); try { const response = await api.get('/api/v1/ditto/cashu/wallet'); - const data: WalletData = await response.json(); + const data = await response.json(); if (data) { const normalizedData = baseWalletSchema.parse(data); setWallet(normalizedData); @@ -58,14 +85,14 @@ const useWallet = () => { if (!wallet) { getWallet(false); } - }, [wallet]); + }, []); return { wallet, isLoading, error, createWallet, getWallet }; }; const useTransactions = () => { const api = useApi(); - const [transactions, setTransactions] = useState(null); + const { transactions, setTransactions } = useWalletStore(); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); @@ -73,7 +100,7 @@ const useTransactions = () => { setIsLoading(true); try { const response = await api.get('/api/v1/ditto/cashu/transactions'); - const data: Transactions = await response.json(); + const data = await response.json(); if (data) { const normalizedData = transactionsSchema.parse(data); setTransactions(normalizedData); @@ -91,16 +118,16 @@ const useTransactions = () => { if (!transactions) { getTransactions(); } - }, [transactions]); + }, []); return { transactions, isLoading, error, getTransactions }; }; const useNutzapRequest = () => { const api = useApi(); + const { nutzapsList, addNutzap } = useWalletStore(); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); - const [nutzapsList, setNutzapsList] = useState>({}); const nutzapRequest = async (account: AccountEntity, amount: number, comment: string, status?: StatusEntity) => { setIsLoading(true); @@ -116,10 +143,7 @@ const useNutzapRequest = () => { const data = await response.json(); if (status) { - setNutzapsList((prevState) => ({ - ...prevState, - [status.id]: { status, amount, comment }, - })); + addNutzap(status.id, { status, amount, comment }); } toast.success(data.message || 'Nutzap sent successfully!'); From fcb3057b01168751840bd2ff21f9dbdb476b2f95 Mon Sep 17 00:00:00 2001 From: danidfra Date: Sun, 23 Mar 2025 17:19:42 -0300 Subject: [PATCH 44/62] Update transactions --- src/features/ui/index.tsx | 8 +- src/features/ui/util/async-components.ts | 1 + .../wallet/components/transactions.tsx | 79 +++++++++++-------- .../wallet/components/wallet-transactions.tsx | 28 +++++++ src/features/wallet/index.tsx | 13 ++- src/locales/en.json | 1 + 6 files changed, 90 insertions(+), 40 deletions(-) create mode 100644 src/features/wallet/components/wallet-transactions.tsx diff --git a/src/features/ui/index.tsx b/src/features/ui/index.tsx index fbaa6c9f6..f60504db0 100644 --- a/src/features/ui/index.tsx +++ b/src/features/ui/index.tsx @@ -75,6 +75,7 @@ import { Wallet, WalletRelays, WalletMints, + WalletTransactions, EditProfile, EditEmail, EditPassword, @@ -333,9 +334,10 @@ const SwitchingColumnsArea: React.FC = ({ children }) => - - - + + + + diff --git a/src/features/ui/util/async-components.ts b/src/features/ui/util/async-components.ts index 0e407442c..280a9c57e 100644 --- a/src/features/ui/util/async-components.ts +++ b/src/features/ui/util/async-components.ts @@ -183,6 +183,7 @@ export const NostrBunkerLogin = lazy(() => import('soapbox/features/nostr/nostr- export const Wallet = lazy(() => import('soapbox/features/wallet/index.tsx')); export const WalletRelays = lazy(() => import('soapbox/features/wallet/components/wallet-relays.tsx')); export const WalletMints = lazy(() => import('soapbox/features/wallet/components/wallet-mints.tsx')); +export const WalletTransactions = lazy(() => import('soapbox/features/wallet/components/wallet-transactions.tsx')); export const StreakModal = lazy(() => import('soapbox/features/ui/components/modals/streak-modal.tsx')); export const FollowsTimeline = lazy(() => import('soapbox/features/home-timeline/follows-timeline.tsx')); export const CommunityTimeline = lazy(() => import('soapbox/features/home-timeline/community-timeline.tsx')); \ No newline at end of file diff --git a/src/features/wallet/components/transactions.tsx b/src/features/wallet/components/transactions.tsx index 9b50ab9f5..a612a5cf9 100644 --- a/src/features/wallet/components/transactions.tsx +++ b/src/features/wallet/components/transactions.tsx @@ -1,10 +1,8 @@ import arrowBarDownIcon from '@tabler/icons/outline/arrow-bar-down.svg'; import arrowBarUpIcon from '@tabler/icons/outline/arrow-bar-up.svg'; -import moreIcon from '@tabler/icons/outline/dots-circle-horizontal.svg'; import questionIcon from '@tabler/icons/outline/question-mark.svg'; import { FormattedDate, defineMessages, useIntl } from 'react-intl'; -import Button from 'soapbox/components/ui/button.tsx'; import Divider from 'soapbox/components/ui/divider.tsx'; import HStack from 'soapbox/components/ui/hstack.tsx'; import Spinner from 'soapbox/components/ui/spinner.tsx'; @@ -28,16 +26,27 @@ const themes = { const messages = defineMessages({ amount: { id: 'wallet.sats', defaultMessage: '{amount} sats' }, - more: { id: 'wallet.transactions.more', defaultMessage: 'More' }, }); -const TransactionItem = (e: { amount: number; created_at: number ; direction: 'in' | 'out' }, lastElementIndex: number, index: number) => { - const intl = useIntl(); - const isLastElement = index === lastElementIndex; - let icon, type, messageColor; - const { direction, amount, created_at } = e; +const groupByDate = (transactions: { amount: number; created_at: number; direction: 'in' | 'out' }[]) => { + return transactions.reduce((acc, transaction) => { + const dateKey = new Date(transaction.created_at * 1000).toDateString(); // Agrupa pelo dia + if (!acc[dateKey]) { + acc[dateKey] = []; + } + acc[dateKey].push(transaction); + return acc; + }, {} as Record); +}; - const formattedDate = ; +const TransactionItem = ({ transaction, hasDivider = true }: { transaction: { amount: number; created_at: number; direction: 'in' | 'out' }; hasDivider?: boolean}) => { + const intl = useIntl(); + let icon, type, messageColor; + const { direction, amount, created_at } = transaction; + + const formattedTime = ( + + ); switch (direction) { case 'in': @@ -59,40 +68,33 @@ const TransactionItem = (e: { amount: number; created_at: number ; direction: ' return ( -
- - {type} - + {type}
- - {intl.formatMessage(messages.amount, { amount: amount })} - - - {formattedDate} - + {intl.formatMessage(messages.amount, { amount })} + {formattedTime} - - {/* */} -
- {!isLastElement && () } + {hasDivider && }
); }; -const Transactions = () => { - const intl = useIntl(); +interface ITransactions { + limit?: number; +} + +const Transactions = ({ limit = 6 }: ITransactions) => { const { account } = useOwnAccount(); const { transactions } = useTransactions(); @@ -100,17 +102,26 @@ const Transactions = () => { return null; } + if (!transactions) { + return ; + } + + const groupedTransactions = groupByDate(transactions.slice(0, limit)); + return ( - - - - {transactions ? transactions.slice(0, 4).map((item, index) => TransactionItem(item, 3, index)) : } + + + {Object.entries(groupedTransactions).map(([date, transactions]) => ( + + + + + {transactions.map((transaction, index) => ( + + ))} + + ))} - - - ); }; diff --git a/src/features/wallet/components/wallet-transactions.tsx b/src/features/wallet/components/wallet-transactions.tsx new file mode 100644 index 000000000..a69cbcb28 --- /dev/null +++ b/src/features/wallet/components/wallet-transactions.tsx @@ -0,0 +1,28 @@ +import moreIcon from '@tabler/icons/outline/dots-circle-horizontal.svg'; +import { defineMessages, useIntl } from 'react-intl'; + +import Button from 'soapbox/components/ui/button.tsx'; +import { Column } from 'soapbox/components/ui/column.tsx'; +import Transactions from 'soapbox/features/wallet/components/transactions.tsx'; + +const messages = defineMessages({ + title: { id: 'wallet.transactions', defaultMessage: 'Transactions' }, + more: { id: 'wallet.transactions.show_more', defaultMessage: 'Show More' }, +}); + +const WalletTransactions = () => { + const intl = useIntl(); + + return ( + + +
+ +
+
+ ); +}; + +export default WalletTransactions; diff --git a/src/features/wallet/index.tsx b/src/features/wallet/index.tsx index b61ae56b5..accf11859 100644 --- a/src/features/wallet/index.tsx +++ b/src/features/wallet/index.tsx @@ -1,6 +1,8 @@ +import moreIcon from '@tabler/icons/outline/dots-circle-horizontal.svg'; import { defineMessages, useIntl } from 'react-intl'; import List, { ListItem } from 'soapbox/components/list.tsx'; +import Button from 'soapbox/components/ui/button.tsx'; import { Card, CardBody, CardHeader, CardTitle } from 'soapbox/components/ui/card.tsx'; import { Column } from 'soapbox/components/ui/column.tsx'; import Spinner from 'soapbox/components/ui/spinner.tsx'; @@ -13,7 +15,6 @@ import { useWallet } from 'soapbox/features/zap/hooks/useHooks.ts'; import { usePaymentMethod } from 'soapbox/features/zap/usePaymentMethod.ts'; import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts'; - const messages = defineMessages({ payment: { id: 'wallet.payment', defaultMessage: 'Payment Method' }, relays: { id: 'wallet.relays', defaultMessage: 'Wallet Relays' }, @@ -21,6 +22,7 @@ const messages = defineMessages({ wallet: { id: 'wallet', defaultMessage: 'Wallet' }, management: { id: 'wallet.management', defaultMessage: 'Wallet Management' }, mints: { id: 'wallet.mints', defaultMessage: 'Mints' }, + more: { id: 'wallet.transactions.more', defaultMessage: 'More' }, }); const paymentMethods = { @@ -64,6 +66,11 @@ const Wallet = () => { +
+ +
@@ -72,8 +79,8 @@ const Wallet = () => { - - + + Date: Sun, 23 Mar 2025 17:40:49 -0300 Subject: [PATCH 45/62] Add message when "no transactions" --- src/features/wallet/components/transactions.tsx | 15 ++++++++------- src/features/wallet/index.tsx | 7 ++++--- src/locales/en.json | 1 + 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/features/wallet/components/transactions.tsx b/src/features/wallet/components/transactions.tsx index a612a5cf9..633df7633 100644 --- a/src/features/wallet/components/transactions.tsx +++ b/src/features/wallet/components/transactions.tsx @@ -1,7 +1,7 @@ import arrowBarDownIcon from '@tabler/icons/outline/arrow-bar-down.svg'; import arrowBarUpIcon from '@tabler/icons/outline/arrow-bar-up.svg'; import questionIcon from '@tabler/icons/outline/question-mark.svg'; -import { FormattedDate, defineMessages, useIntl } from 'react-intl'; +import { FormattedDate, FormattedMessage } from 'react-intl'; import Divider from 'soapbox/components/ui/divider.tsx'; import HStack from 'soapbox/components/ui/hstack.tsx'; @@ -24,10 +24,6 @@ const themes = { withdraw: '!text-orange-600 dark:!text-orange-300', }; -const messages = defineMessages({ - amount: { id: 'wallet.sats', defaultMessage: '{amount} sats' }, -}); - const groupByDate = (transactions: { amount: number; created_at: number; direction: 'in' | 'out' }[]) => { return transactions.reduce((acc, transaction) => { const dateKey = new Date(transaction.created_at * 1000).toDateString(); // Agrupa pelo dia @@ -40,7 +36,6 @@ const groupByDate = (transactions: { amount: number; created_at: number; directi }; const TransactionItem = ({ transaction, hasDivider = true }: { transaction: { amount: number; created_at: number; direction: 'in' | 'out' }; hasDivider?: boolean}) => { - const intl = useIntl(); let icon, type, messageColor; const { direction, amount, created_at } = transaction; @@ -80,7 +75,7 @@ const TransactionItem = ({ transaction, hasDivider = true }: { transaction: { am - {intl.formatMessage(messages.amount, { amount })} + {formattedTime} @@ -106,6 +101,12 @@ const Transactions = ({ limit = 6 }: ITransactions) => { return ; } + if (transactions.length === 0) { + return ( + + ); + } + const groupedTransactions = groupByDate(transactions.slice(0, limit)); return ( diff --git a/src/features/wallet/index.tsx b/src/features/wallet/index.tsx index accf11859..15fc3cee5 100644 --- a/src/features/wallet/index.tsx +++ b/src/features/wallet/index.tsx @@ -11,7 +11,7 @@ import { SelectDropdown } from 'soapbox/features/forms/index.tsx'; import Balance from 'soapbox/features/wallet/components/balance.tsx'; import CreateWallet from 'soapbox/features/wallet/components/create-wallet.tsx'; import Transactions from 'soapbox/features/wallet/components/transactions.tsx'; -import { useWallet } from 'soapbox/features/zap/hooks/useHooks.ts'; +import { useTransactions, useWallet } from 'soapbox/features/zap/hooks/useHooks.ts'; import { usePaymentMethod } from 'soapbox/features/zap/usePaymentMethod.ts'; import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts'; @@ -37,6 +37,7 @@ const Wallet = () => { const { account } = useOwnAccount(); const { wallet: walletData, isLoading } = useWallet(); const { method, changeMethod } = usePaymentMethod(); + const { transactions } = useTransactions(); if (!account) return null; @@ -66,11 +67,11 @@ const Wallet = () => { -
+ {!transactions &&
-
+
}
diff --git a/src/locales/en.json b/src/locales/en.json index 69c25b17a..b0b9b0993 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1738,6 +1738,7 @@ "wallet.sats": "{amount} sats", "wallet.transactions": "Transactions", "wallet.transactions.more": "More", + "wallet.transactions.no_transactions": "No transactions available yet.", "wallet.transactions.show_more": "Show More", "who_to_follow.title": "People To Follow", "zap.button.text.rounded": "{method} {amount}K sats", From a6e18421d3cede72d32883818c2ef0a51d81eca8 Mon Sep 17 00:00:00 2001 From: danidfra Date: Mon, 24 Mar 2025 20:13:36 -0300 Subject: [PATCH 46/62] Refactor code and rename translated texts --- src/features/wallet/components/balance.tsx | 34 ++++++++----------- .../wallet/components/create-wallet.tsx | 2 +- .../wallet/components/editable-lists.tsx | 2 ++ .../wallet/components/transactions.tsx | 4 +-- src/features/wallet/index.tsx | 9 +++-- .../zap/components/pay-request-form.tsx | 2 +- src/locales/en.json | 13 +++---- 7 files changed, 34 insertions(+), 32 deletions(-) diff --git a/src/features/wallet/components/balance.tsx b/src/features/wallet/components/balance.tsx index dd14d883c..45dcbbf10 100644 --- a/src/features/wallet/components/balance.tsx +++ b/src/features/wallet/components/balance.tsx @@ -1,5 +1,3 @@ -// import IconButton from 'soapbox/components/ui/icon-button.tsx'; -import arrowBackIcon from '@tabler/icons/outline/arrow-back-up.svg'; import cancelIcon from '@tabler/icons/outline/cancel.svg'; import withdrawIcon from '@tabler/icons/outline/cash.svg'; import mIcon from '@tabler/icons/outline/circle-dotted-letter-m.svg'; @@ -19,7 +17,7 @@ import Input from 'soapbox/components/ui/input.tsx'; import Stack from 'soapbox/components/ui/stack.tsx'; import Text from 'soapbox/components/ui/text.tsx'; import { SelectDropdown } from 'soapbox/features/forms/index.tsx'; -import { useWallet } from 'soapbox/features/zap/hooks/useHooks.ts'; +import { useTransactions, useWallet } from 'soapbox/features/zap/hooks/useHooks.ts'; import { useApi } from 'soapbox/hooks/useApi.ts'; import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts'; import { Quote, quoteSchema } from 'soapbox/schemas/wallet.ts'; @@ -28,10 +26,11 @@ import toast from 'soapbox/toast.tsx'; const messages = defineMessages({ + amount: { id: 'wallet.balance.mint.amount', defaultMessage: 'Amount in sats' }, + cancel: { id: 'wallet.button.cancel', defaultMessage: 'Cancel' }, balance: { id: 'wallet.sats', defaultMessage: '{amount} sats' }, - withdraw: { id: 'wallet.balance.withdraw_button', defaultMessage: 'Withdraw' }, - exchange: { id: 'wallet.balance.exchange_button', defaultMessage: 'Exchange' }, - mint: { id: 'wallet.balance.mint_button', defaultMessage: 'Mint' }, + withdraw: { id: 'wallet.button.withdraw', defaultMessage: 'Withdraw' }, + mint: { id: 'wallet.button.mint', defaultMessage: 'Mint' }, payment: { id: 'wallet.balance.mint.payment', defaultMessage: 'Make the payment to complete:' }, paidMessage: { id: 'wallet.balance.mint.paid_message', defaultMessage: 'Your mint was successful, and your sats are now in your balance. Enjoy!' }, unpaidMessage: { id: 'wallet.balance.mint.unpaid_message', defaultMessage: 'Your mint is still unpaid. Complete the payment to receive your sats.' }, @@ -91,10 +90,12 @@ const NewMint = ({ onBack, list }: NewMintProps) => { const api = useApi(); const intl = useIntl(); const { getWallet } = useWallet(); + const { getTransactions } = useTransactions(); const now = Math.floor(Date.now() / 1000); const handleClean = useCallback(() => { + onBack(); setQuote(undefined); setMintAmount(''); setCurrentState('default'); @@ -111,8 +112,8 @@ const NewMint = ({ onBack, list }: NewMintProps) => { } else { toast.success(intl.formatMessage(messages.paidMessage)); onBack(); - // onChange(); - getWallet(); // TODO: Remove + getWallet(); + getTransactions(); handleClean(); setCurrentState('default'); } @@ -196,15 +197,13 @@ const NewMint = ({ onBack, list }: NewMintProps) => {
{!quote ? - + - + /^[0-9]*$/.test(e.target.value) && setMintAmount(e.target.value)} autoCapitalize='off' @@ -213,7 +212,6 @@ const NewMint = ({ onBack, list }: NewMintProps) => { : - {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} {intl.formatMessage(messages.payment)} @@ -226,8 +224,7 @@ const NewMint = ({ onBack, list }: NewMintProps) => { } - diff --git a/src/features/zap/components/pay-request-form.tsx b/src/features/zap/components/pay-request-form.tsx index f05f4551b..1c316f0fd 100644 --- a/src/features/zap/components/pay-request-form.tsx +++ b/src/features/zap/components/pay-request-form.tsx @@ -72,7 +72,7 @@ const PayRequestForm = ({ account, status, onClose }: IPayRequestForm) => { const splitData = { hasZapSplit, zapSplitAccounts, splitValues }; if (isCashu) { - nutzapRequest(account, amount, zapComment, status); + await nutzapRequest(account, amount, zapComment, status); dispatch(closeModal('PAY_REQUEST')); return; } diff --git a/src/locales/en.json b/src/locales/en.json index b0b9b0993..496d567a8 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1712,15 +1712,15 @@ "video.pause": "Pause", "video.play": "Play", "video.unmute": "Unmute sound", - "wallet": "Wallet", - "wallet.balance.exchange_button": "Exchange", "wallet.balance.expired": "Expired", + "wallet.balance.mint.amount": "Amount in sats", "wallet.balance.mint.paid_message": "Your mint was successful, and your sats are now in your balance. Enjoy!", "wallet.balance.mint.payment": "Make the payment to complete:", "wallet.balance.mint.unpaid_message": "Your mint is still unpaid. Complete the payment to receive your sats.", - "wallet.balance.mint_button": "Mint", - "wallet.balance.withdraw_button": "Withdraw", - "wallet.create_wallet.button": "Create wallet", + "wallet.button.cancel": "Cancel", + "wallet.button.create_wallet": "Create wallet", + "wallet.button.mint": "Mint", + "wallet.button.withdraw": "Withdraw", "wallet.create_wallet.question": "Do you want create one?", "wallet.create_wallet.title": "You don't have a wallet", "wallet.invalid_url": "All strings must be valid URLs.", @@ -1736,9 +1736,10 @@ "wallet.relays.error": "Failed to update mints.", "wallet.relays.sucess": "Relays updated with success!", "wallet.sats": "{amount} sats", + "wallet.title": "Wallet", "wallet.transactions": "Transactions", "wallet.transactions.more": "More", - "wallet.transactions.no_transactions": "No transactions available yet.", + "wallet.transactions.no_transactions": "You don't have transactions yet.", "wallet.transactions.show_more": "Show More", "who_to_follow.title": "People To Follow", "zap.button.text.rounded": "{method} {amount}K sats", From 8b097aeef2592411456b5ea6589973e7186824ad Mon Sep 17 00:00:00 2001 From: danidfra Date: Tue, 25 Mar 2025 13:29:40 -0300 Subject: [PATCH 47/62] Create pocketWallet --- src/features/ui/components/pocket-wallet.tsx | 85 ++++++++++++++++++++ src/features/ui/util/async-components.ts | 3 +- src/features/wallet/index.tsx | 2 +- src/pages/default-page.tsx | 10 +++ src/pages/home-page.tsx | 6 ++ src/pages/profile-page.tsx | 7 ++ src/pages/search-page.tsx | 7 ++ 7 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 src/features/ui/components/pocket-wallet.tsx diff --git a/src/features/ui/components/pocket-wallet.tsx b/src/features/ui/components/pocket-wallet.tsx new file mode 100644 index 000000000..ddb4b1b44 --- /dev/null +++ b/src/features/ui/components/pocket-wallet.tsx @@ -0,0 +1,85 @@ +import arrowsDiagonalIcon from '@tabler/icons/outline/arrows-diagonal-2.svg'; +import arrowsDiagonalMinimizeIcon from '@tabler/icons/outline/arrows-diagonal-minimize.svg'; +import eyeClosedIcon from '@tabler/icons/outline/eye-closed.svg'; +import eyeIcon from '@tabler/icons/outline/eye.svg'; +import walletIcon from '@tabler/icons/outline/wallet.svg'; +import { useEffect, useState } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + +import Button from 'soapbox/components/ui/button.tsx'; +import HStack from 'soapbox/components/ui/hstack.tsx'; +import Icon from 'soapbox/components/ui/icon.tsx'; +import Stack from 'soapbox/components/ui/stack.tsx'; +import Text from 'soapbox/components/ui/text.tsx'; +import Balance from 'soapbox/features/wallet/components/balance.tsx'; +import { useWallet } from 'soapbox/features/zap/hooks/useHooks.ts'; + +const messages = defineMessages({ + wallet: { id: 'wallet.title', defaultMessage: 'Wallet' }, + balance: { id: 'wallet.sats', defaultMessage: '{amount} sats' }, + withdraw: { id: 'wallet.button.withdraw', defaultMessage: 'Withdraw' }, + mint: { id: 'wallet.button.mint', defaultMessage: 'Mint' }, +}); + +const PocketWallet = () => { + const intl = useIntl(); + const { wallet } = useWallet(); + + const [expanded, setExpanded] = useState(false); + const [eyeClosed, setEyeClosed] = useState(() => { + const storedData = localStorage.getItem('soapbox:wallet:eye'); + return storedData ? JSON.parse(storedData) : false; + }); + + useEffect(() => { + localStorage.setItem('soapbox:wallet:eye', JSON.stringify(eyeClosed)); + }, [eyeClosed]); + + return ( + + + + + + {intl.formatMessage(messages.wallet)} + + + + + {!expanded && <> + { eyeClosed ? + + + + + + + + + : + {intl.formatMessage(messages.balance, { amount: wallet?.balance })} + } + + + } + + + + + + + + {expanded && + + } + + + ); + +}; + +export default PocketWallet; \ No newline at end of file diff --git a/src/features/ui/util/async-components.ts b/src/features/ui/util/async-components.ts index 280a9c57e..1db53d6a1 100644 --- a/src/features/ui/util/async-components.ts +++ b/src/features/ui/util/async-components.ts @@ -186,4 +186,5 @@ export const WalletMints = lazy(() => import('soapbox/features/wallet/components export const WalletTransactions = lazy(() => import('soapbox/features/wallet/components/wallet-transactions.tsx')); export const StreakModal = lazy(() => import('soapbox/features/ui/components/modals/streak-modal.tsx')); export const FollowsTimeline = lazy(() => import('soapbox/features/home-timeline/follows-timeline.tsx')); -export const CommunityTimeline = lazy(() => import('soapbox/features/home-timeline/community-timeline.tsx')); \ No newline at end of file +export const CommunityTimeline = lazy(() => import('soapbox/features/home-timeline/community-timeline.tsx')); +export const PocketWallet = lazy(() => import('soapbox/features/ui/components/pocket-wallet.tsx')); \ No newline at end of file diff --git a/src/features/wallet/index.tsx b/src/features/wallet/index.tsx index b3b14badc..5648c5c61 100644 --- a/src/features/wallet/index.tsx +++ b/src/features/wallet/index.tsx @@ -69,7 +69,7 @@ const Wallet = () => { - + {hasTransactions &&
diff --git a/src/features/zap/hooks/useHooks.ts b/src/features/zap/hooks/useHooks.ts index 5600782ad..a92bc95c2 100644 --- a/src/features/zap/hooks/useHooks.ts +++ b/src/features/zap/hooks/useHooks.ts @@ -11,9 +11,11 @@ interface WalletState { wallet: WalletData | null; transactions: Transactions | null; nutzapsList: Record; // TODO: remove + prevTransaction?: string | null; + nextTransaction?: string | null; setWallet: (wallet: WalletData | null) => void; - setTransactions: (transactions: Transactions | null) => void; + setTransactions: (transactions: Transactions | null, prevTransaction?: string | null, nextTransaction?: string | null) => void; addNutzap: (statusId: string, data: { status: StatusEntity; amount: number; comment: string }) => void; } @@ -25,10 +27,12 @@ interface IWalletInfo { const useWalletStore = create((set) => ({ wallet: null, transactions: null, + prevTransaction: null, + nextTransaction: null, nutzapsList: {}, setWallet: (wallet) => set({ wallet }), - setTransactions: (transactions) => set({ transactions }), + setTransactions: (transactions, prevTransaction, nextTransaction) => set({ transactions, prevTransaction, nextTransaction }), addNutzap: (statusId, data) => set((state) => ({ nutzapsList: { @@ -92,7 +96,7 @@ const useWallet = () => { const useTransactions = () => { const api = useApi(); - const { transactions, setTransactions } = useWalletStore(); + const { transactions, nextTransaction, setTransactions } = useWalletStore(); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); @@ -100,10 +104,11 @@ const useTransactions = () => { 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); + setTransactions(normalizedData, prev, next); } } catch (err) { const messageError = err instanceof Error ? err.message : 'Transactions not found'; @@ -114,13 +119,37 @@ const useTransactions = () => { } }; + const expandTransactions = async () => { + if (!nextTransaction || !transactions) { + toast.info('You reached the end of transactions'); + return; + } + 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); + } catch (err) { + const messageError = err instanceof Error ? err.message : 'Error expanding transactions'; + toast.error(messageError); + setError(messageError); + } finally { + setIsLoading(false); + } + }; + useEffect(() => { if (!transactions) { getTransactions(); } }, []); - return { transactions, isLoading, error, getTransactions }; + return { transactions, isLoading, error, getTransactions, expandTransactions }; }; const useNutzapRequest = () => { @@ -128,6 +157,8 @@ const useNutzapRequest = () => { const { nutzapsList, addNutzap } = useWalletStore(); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); + const { getWallet } = useWallet(); + const { getTransactions } = useTransactions(); const nutzapRequest = async (account: AccountEntity, amount: number, comment: string, status?: StatusEntity) => { setIsLoading(true); @@ -147,6 +178,8 @@ const useNutzapRequest = () => { } toast.success(data.message || 'Nutzap sent successfully!'); + getWallet(); + getTransactions(); } catch (err) { const messageError = err instanceof Error ? err.message : 'An unexpected error occurred'; setError(messageError); diff --git a/src/locales/en.json b/src/locales/en.json index 496d567a8..8131253e1 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1724,6 +1724,7 @@ "wallet.create_wallet.question": "Do you want create one?", "wallet.create_wallet.title": "You don't have a wallet", "wallet.invalid_url": "All strings must be valid URLs.", + "wallet.loading": "Loading…", "wallet.loading_error": "An unexpected error occurred while loading your wallet data.", "wallet.management": "Wallet Management", "wallet.mints": "Mints", From 575c81cde97ed53813af20e3ad074f751e5f7d86 Mon Sep 17 00:00:00 2001 From: danidfra Date: Tue, 25 Mar 2025 14:51:38 -0300 Subject: [PATCH 49/62] Refactor code --- src/actions/interactions.ts | 63 ------------------- .../zap/components/pay-request-form.tsx | 9 ++- src/features/zap/hooks/useHooks.ts | 2 +- src/locales/en.json | 4 +- src/locales/es.json | 2 +- src/locales/ga.json | 4 +- src/locales/pt-BR.json | 2 +- src/reducers/statuses.ts | 7 --- 8 files changed, 11 insertions(+), 82 deletions(-) diff --git a/src/actions/interactions.ts b/src/actions/interactions.ts index 74edd3f26..dd8655554 100644 --- a/src/actions/interactions.ts +++ b/src/actions/interactions.ts @@ -1,4 +1,3 @@ -import toast from 'soapbox/toast.tsx'; import { isLoggedIn } from 'soapbox/utils/auth.ts'; import api from '../api/index.ts'; @@ -74,10 +73,6 @@ const FAVOURITES_EXPAND_FAIL = 'FAVOURITES_EXPAND_FAIL'; const REBLOGS_EXPAND_SUCCESS = 'REBLOGS_EXPAND_SUCCESS'; const REBLOGS_EXPAND_FAIL = 'REBLOGS_EXPAND_FAIL'; -const NUTZAP_REQUEST = 'NUTZAP_REQUEST'; -const NUTZAP_SUCCESS = 'NUTZAP_SUCCESS'; -const NUTZAP_FAIL = 'NUTZAP_FAIL'; - const ZAP_REQUEST = 'ZAP_REQUEST'; const ZAP_SUCCESS = 'ZAP_SUCCESS'; const ZAP_FAIL = 'ZAP_FAIL'; @@ -362,57 +357,6 @@ const zapFail = (status: StatusEntity, error: unknown) => ({ skipLoading: true, }); -const nutzap = (account: AccountEntity, status: StatusEntity | undefined, amount: number, comment: string) => - async (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - - try { - const response = await api(getState).post('/api/v1/ditto/cashu/nutzap', { - amount, - comment, - account_id: account.id, - status_id: status?.id, - }); - - const data = await response.json(); - - if (data) { - toast.success(data.message); - } else { - toast.success('Nutzap sent successfully!'); - } - - if (status) dispatch(nutzapSuccess(status)); - - } catch (e) { - if (e instanceof Error) { - toast.error(e.message); - } else { - toast.error('An unexpected error occurred'); - } - if (status) dispatch(nutzapFail(status, e)); - } - }; - -const nutzapRequest = (status: StatusEntity) => ({ - type: NUTZAP_REQUEST, - status: status, - skipLoading: true, -}); - -const nutzapSuccess = (status: StatusEntity) => ({ - type: NUTZAP_SUCCESS, - status: status, - skipLoading: true, -}); - -const nutzapFail = (status: StatusEntity, error: unknown) => ({ - type: NUTZAP_FAIL, - status: status, - error: error, - skipLoading: true, -}); - const bookmarkRequest = (status: StatusEntity) => ({ type: BOOKMARK_REQUEST, status: status, @@ -849,9 +793,6 @@ export { FAVOURITES_EXPAND_FAIL, REBLOGS_EXPAND_SUCCESS, REBLOGS_EXPAND_FAIL, - NUTZAP_REQUEST, - NUTZAP_SUCCESS, - NUTZAP_FAIL, ZAP_REQUEST, ZAP_FAIL, ZAPS_FETCH_REQUEST, @@ -923,10 +864,6 @@ export { remoteInteractionRequest, remoteInteractionSuccess, remoteInteractionFail, - nutzapRequest, - nutzapSuccess, - nutzapFail, - nutzap, zap, fetchZaps, expandZaps, diff --git a/src/features/zap/components/pay-request-form.tsx b/src/features/zap/components/pay-request-form.tsx index 1c316f0fd..f822ac1e8 100644 --- a/src/features/zap/components/pay-request-form.tsx +++ b/src/features/zap/components/pay-request-form.tsx @@ -25,7 +25,6 @@ import { useNutzapRequest } from 'soapbox/features/zap/hooks/useHooks.ts'; import { usePaymentMethod } from 'soapbox/features/zap/usePaymentMethod.ts'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; import { emojifyText } from 'soapbox/utils/emojify.tsx'; -import { capitalize } from 'soapbox/utils/strings.ts'; import PaymentButton from './zap-button/payment-button.tsx'; @@ -48,8 +47,8 @@ interface IPayRequestForm { const closeIcon = xIcon; const messages = defineMessages({ - zap_button_rounded: { id: 'zap.button.text.rounded', defaultMessage: '{method} {amount}K sats' }, - zap_button: { id: 'payment_method.button.text.raw', defaultMessage: '{method} {amount} sats' }, + zap_button_rounded: { id: 'zap.button.text.rounded', defaultMessage: 'Zap {amount}K sats' }, + zap_button: { id: 'payment_method.button.text.raw', defaultMessage: 'Zap {amount} sats' }, zap_commentPlaceholder: { id: 'payment_method.comment_input.placeholder', defaultMessage: 'Optional comment' }, }); @@ -108,9 +107,9 @@ const PayRequestForm = ({ account, status, onClose }: IPayRequestForm) => { const renderPaymentButtonText = () => { if (amount >= 1000) { - return intl.formatMessage(messages.zap_button_rounded, { amount: Math.round((amount / 1000) * 10) / 10, method: isCashu ? 'Nutzap' : capitalize(paymentMethod) }); + return intl.formatMessage(messages.zap_button_rounded, { amount: Math.round((amount / 1000) * 10) / 10 }); } - return intl.formatMessage(messages.zap_button, { amount: amount, method: isCashu ? 'Nutzap' : capitalize(paymentMethod) }); + return intl.formatMessage(messages.zap_button, { amount: amount }); }; useEffect(() => { diff --git a/src/features/zap/hooks/useHooks.ts b/src/features/zap/hooks/useHooks.ts index a92bc95c2..5d74b75df 100644 --- a/src/features/zap/hooks/useHooks.ts +++ b/src/features/zap/hooks/useHooks.ts @@ -177,7 +177,7 @@ const useNutzapRequest = () => { addNutzap(status.id, { status, amount, comment }); } - toast.success(data.message || 'Nutzap sent successfully!'); + toast.success(data.message || 'Zap sent successfully!'); getWallet(); getTransactions(); } catch (err) { diff --git a/src/locales/en.json b/src/locales/en.json index 8131253e1..6d91785be 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1295,7 +1295,7 @@ "password_reset.reset": "Reset password", "patron.donate": "Donate", "patron.title": "Funding Goal", - "payment_method.button.text.raw": "{method} {amount} sats", + "payment_method.button.text.raw": "Zap {amount} sats", "payment_method.comment_input.placeholder": "Optional comment", "payment_method.send_to": "Send {method} to {target}", "payment_method.split_message.deducted": "{amountDeducted} sats will deducted*", @@ -1743,7 +1743,7 @@ "wallet.transactions.no_transactions": "You don't have transactions yet.", "wallet.transactions.show_more": "Show More", "who_to_follow.title": "People To Follow", - "zap.button.text.rounded": "{method} {amount}K sats", + "zap.button.text.rounded": "Zap {amount}K sats", "zap.finish": "Finish", "zap.next": "Next", "zap.open_wallet": "Open Wallet", diff --git a/src/locales/es.json b/src/locales/es.json index bac461ddb..6df0403ed 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -1626,7 +1626,7 @@ "video.play": "Reproducir", "video.unmute": "Dejar de silenciar sonido", "who_to_follow.title": "Personas a seguir", - "payment_method.button.text.raw": "{method} {amount} sats", + "payment_method.button.text.raw": "Zap {amount} sats", "payment_method.comment_input.placeholder": "Comentario opcional", "zap.open_wallet": "Abrir Wallet", "zap.send_to": "Enviar zaps a {target}", diff --git a/src/locales/ga.json b/src/locales/ga.json index d8d70e1e6..a7de77b12 100644 --- a/src/locales/ga.json +++ b/src/locales/ga.json @@ -1679,8 +1679,8 @@ "video.play": "Seinn", "video.unmute": "Fuaim gan iomrá", "who_to_follow.title": "Daoine le Leanúint", - "payment_method.button.text.raw": "Suíonn {method} {amount}", - "zap.button.text.rounded": "{method} {amount}K shuíonn", + "payment_method.button.text.raw": "Suíonn Zap {amount}", + "zap.button.text.rounded": "Zap {amount}K shuíonn", "payment_method.comment_input.placeholder": "Nóta roghnach", "zap.finish": "Críochnaigh", "zap.next": "Ar Aghaidh", diff --git a/src/locales/pt-BR.json b/src/locales/pt-BR.json index 298539676..1657c2da3 100644 --- a/src/locales/pt-BR.json +++ b/src/locales/pt-BR.json @@ -1635,7 +1635,7 @@ "video.play": "Reproduzir", "video.unmute": "Reativar som", "who_to_follow.title": "Quem seguir", - "payment_method.button.text.raw": "{method} {amount} sats", + "payment_method.button.text.raw": "Zap {amount} sats", "payment_method.comment_input.placeholder": "Comentário opcional", "zap.open_wallet": "Abrir Carteira", "zap.send_to": "Enviar zaps para {target}", diff --git a/src/reducers/statuses.ts b/src/reducers/statuses.ts index 663fbc381..69696387b 100644 --- a/src/reducers/statuses.ts +++ b/src/reducers/statuses.ts @@ -28,8 +28,6 @@ import { DISLIKE_REQUEST, UNDISLIKE_REQUEST, DISLIKE_FAIL, - NUTZAP_FAIL, - NUTZAP_SUCCESS, ZAP_REQUEST, ZAP_FAIL, } from '../actions/interactions.ts'; @@ -239,7 +237,6 @@ const simulatePayment = (state: State, statusId: string, paid: boolean, method: }); } - return state.set(statusId, updatedStatus); }; @@ -301,10 +298,6 @@ export default function statuses(state = initialState, action: AnyAction): State return simulatePayment(state, action.status.id, true, 'zap'); case ZAP_FAIL: return simulatePayment(state, action.status.id, false, 'zap'); - case NUTZAP_SUCCESS: - return simulatePayment(state, action.status.id, true, 'cashu'); - case NUTZAP_FAIL: - return simulatePayment(state, action.status.id, false, 'cashu'); case REBLOG_REQUEST: return state.setIn([action.status.id, 'reblogged'], true); case REBLOG_FAIL: From fc7a8c849c43ea56e1d488230427c57d9cee5c25 Mon Sep 17 00:00:00 2001 From: danidfra Date: Tue, 25 Mar 2025 18:14:13 -0300 Subject: [PATCH 50/62] Update loading more transactions --- .../wallet/components/wallet-transactions.tsx | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/features/wallet/components/wallet-transactions.tsx b/src/features/wallet/components/wallet-transactions.tsx index d0301745a..af9ec6116 100644 --- a/src/features/wallet/components/wallet-transactions.tsx +++ b/src/features/wallet/components/wallet-transactions.tsx @@ -1,8 +1,8 @@ -import moreIcon from '@tabler/icons/outline/dots-circle-horizontal.svg'; +import { useEffect, useRef, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import Button from 'soapbox/components/ui/button.tsx'; import { Column } from 'soapbox/components/ui/column.tsx'; +import Spinner from 'soapbox/components/ui/spinner.tsx'; import Transactions from 'soapbox/features/wallet/components/transactions.tsx'; import { useTransactions } from 'soapbox/features/zap/hooks/useHooks.ts'; @@ -15,17 +15,35 @@ const messages = defineMessages({ const WalletTransactions = () => { const intl = useIntl(); const { isLoading, expandTransactions } = useTransactions(); + const observerRef = useRef(null); + const [showSpinner, setShowSpinner] = useState(false); + + useEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setShowSpinner(true); + if (!isLoading) expandTransactions(); + } + }, + { rootMargin: '100px' }, + ); + + if (observerRef.current) { + observer.observe(observerRef.current); + } + + return () => observer.disconnect(); + }, [expandTransactions, isLoading]); return ( - + -
- +
+ {showSpinner && isLoading && }
); }; -export default WalletTransactions; +export default WalletTransactions; \ No newline at end of file From 92aa009de9446a00974b3b879615b69d58339605 Mon Sep 17 00:00:00 2001 From: danidfra Date: Wed, 26 Mar 2025 12:42:56 -0300 Subject: [PATCH 51/62] Refactor wallet logic to handle Zap Cashu data --- src/components/pure-status-action-bar.tsx | 10 +++---- src/components/status-action-bar.tsx | 10 +++---- .../components/status-interaction-bar.tsx | 6 ++-- .../zap/components/pay-request-form.tsx | 6 ++-- src/features/zap/hooks/useHooks.ts | 28 +++++++++---------- src/normalizers/status.ts | 4 +-- src/reducers/statuses.ts | 19 ++++--------- src/schemas/status.ts | 4 +-- 8 files changed, 40 insertions(+), 47 deletions(-) diff --git a/src/components/pure-status-action-bar.tsx b/src/components/pure-status-action-bar.tsx index ed90e79f5..075fc00cf 100644 --- a/src/components/pure-status-action-bar.tsx +++ b/src/components/pure-status-action-bar.tsx @@ -48,7 +48,7 @@ import DropdownMenu from 'soapbox/components/dropdown-menu/index.ts'; import PureStatusReactionWrapper from 'soapbox/components/pure-status-reaction-wrapper.tsx'; import StatusActionButton from 'soapbox/components/status-action-button.tsx'; import HStack from 'soapbox/components/ui/hstack.tsx'; -import { useNutzapRequest, useWallet } from 'soapbox/features/zap/hooks/useHooks.ts'; +import { useZapCashuRequest, useWallet } from 'soapbox/features/zap/hooks/useHooks.ts'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; import { useDislike } from 'soapbox/hooks/useDislike.ts'; @@ -181,8 +181,8 @@ const PureStatusActionBar: React.FC = ({ const { boostModal, deleteModal } = useSettings(); const { wallet } = useWallet(); - const { nutzapsList } = useNutzapRequest(); - const isNutzapped = Object.keys(nutzapsList).some((nutzap)=> nutzap === status.id); // TODO: Remove "getWallet" after been in backend + const { zapCashuList } = useZapCashuRequest(); + const isZappedCashu = zapCashuList.some((zapCashu)=> zapCashu === status.id); const { account } = useOwnAccount(); const isStaff = account ? account.staff : false; @@ -855,9 +855,9 @@ const PureStatusActionBar: React.FC = ({ color='accent' filled onClick={handleZapClick} - active={status.nutzapped || status.zapped || isNutzapped} + active={status.zapped_cashu || status.zapped || isZappedCashu} theme={statusActionButtonTheme} - count={status?.zaps_amount ? status.zaps_amount / 1000 : 0} + count={(status?.zaps_amount ?? 0) / 1000 + (status?.zaps_amount_cashu ?? 0)} /> )} diff --git a/src/components/status-action-bar.tsx b/src/components/status-action-bar.tsx index ff72a3664..38526dd3f 100644 --- a/src/components/status-action-bar.tsx +++ b/src/components/status-action-bar.tsx @@ -49,7 +49,7 @@ import DropdownMenu from 'soapbox/components/dropdown-menu/index.ts'; import StatusActionButton from 'soapbox/components/status-action-button.tsx'; import StatusReactionWrapper from 'soapbox/components/status-reaction-wrapper.tsx'; import HStack from 'soapbox/components/ui/hstack.tsx'; -import { useNutzapRequest, useWallet } from 'soapbox/features/zap/hooks/useHooks.ts'; +import { useZapCashuRequest, useWallet } from 'soapbox/features/zap/hooks/useHooks.ts'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; import { useFeatures } from 'soapbox/hooks/useFeatures.ts'; @@ -176,8 +176,8 @@ const StatusActionBar: React.FC = ({ const { boostModal, deleteModal } = useSettings(); const { wallet } = useWallet(); - const { nutzapsList } = useNutzapRequest(); - const isNutzapped = Object.keys(nutzapsList).some((nutzap)=> nutzap === status.id); + const { zapCashuList } = useZapCashuRequest(); + const isZappedCashu = zapCashuList.some((zapCashu)=> zapCashu === status.id); const { account } = useOwnAccount(); const isStaff = account ? account.staff : false; @@ -845,9 +845,9 @@ const StatusActionBar: React.FC = ({ color='accent' filled onClick={handleZapClick} - active={status.nutzapped || status.zapped || isNutzapped} + active={status.zapped_cashu || status.zapped || isZappedCashu} theme={statusActionButtonTheme} - count={status?.zaps_amount ? status.zaps_amount / 1000 : 0} + count={(status?.zaps_amount ?? 0) / 1000 + (status?.zaps_amount_cashu ?? 0)} /> )} diff --git a/src/features/status/components/status-interaction-bar.tsx b/src/features/status/components/status-interaction-bar.tsx index 62ee17cf7..9f43a85bb 100644 --- a/src/features/status/components/status-interaction-bar.tsx +++ b/src/features/status/components/status-interaction-bar.tsx @@ -206,13 +206,13 @@ const StatusInteractionBar: React.FC = ({ status }): JSX. }; const getZaps = () => { - if (status.zaps_amount) { + if (status.zaps_amount || status.zaps_amount_cashu) { return ( - + ); diff --git a/src/features/zap/components/pay-request-form.tsx b/src/features/zap/components/pay-request-form.tsx index f822ac1e8..f95d813a0 100644 --- a/src/features/zap/components/pay-request-form.tsx +++ b/src/features/zap/components/pay-request-form.tsx @@ -21,7 +21,7 @@ import Input from 'soapbox/components/ui/input.tsx'; import Stack from 'soapbox/components/ui/stack.tsx'; import SvgIcon from 'soapbox/components/ui/svg-icon.tsx'; import Text from 'soapbox/components/ui/text.tsx'; -import { useNutzapRequest } from 'soapbox/features/zap/hooks/useHooks.ts'; +import { useZapCashuRequest } from 'soapbox/features/zap/hooks/useHooks.ts'; import { usePaymentMethod } from 'soapbox/features/zap/usePaymentMethod.ts'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; import { emojifyText } from 'soapbox/utils/emojify.tsx'; @@ -63,7 +63,7 @@ const PayRequestForm = ({ account, status, onClose }: IPayRequestForm) => { const { method: paymentMethod } = usePaymentMethod(); const isCashu = paymentMethod === 'cashu'; const hasZapSplit = zapArrays.length > 0 && !isCashu; - const { nutzapRequest } = useNutzapRequest(); + const { zapCashuRequest } = useZapCashuRequest(); const handleSubmit = async (e?: React.FormEvent) => { e?.preventDefault(); @@ -71,7 +71,7 @@ const PayRequestForm = ({ account, status, onClose }: IPayRequestForm) => { const splitData = { hasZapSplit, zapSplitAccounts, splitValues }; if (isCashu) { - await nutzapRequest(account, amount, zapComment, status); + await zapCashuRequest(account, amount, zapComment, status); dispatch(closeModal('PAY_REQUEST')); return; } diff --git a/src/features/zap/hooks/useHooks.ts b/src/features/zap/hooks/useHooks.ts index 5d74b75df..496449834 100644 --- a/src/features/zap/hooks/useHooks.ts +++ b/src/features/zap/hooks/useHooks.ts @@ -10,13 +10,13 @@ import type { Account as AccountEntity, Status as StatusEntity } from 'soapbox/t interface WalletState { wallet: WalletData | null; transactions: Transactions | null; - nutzapsList: Record; // TODO: remove + zapCashuList: string[]; prevTransaction?: string | null; nextTransaction?: string | null; setWallet: (wallet: WalletData | null) => void; setTransactions: (transactions: Transactions | null, prevTransaction?: string | null, nextTransaction?: string | null) => void; - addNutzap: (statusId: string, data: { status: StatusEntity; amount: number; comment: string }) => void; + addZapCashu: (statusId: string) => void; } interface IWalletInfo { @@ -29,16 +29,16 @@ const useWalletStore = create((set) => ({ transactions: null, prevTransaction: null, nextTransaction: null, - nutzapsList: {}, + zapCashuList: [], setWallet: (wallet) => set({ wallet }), setTransactions: (transactions, prevTransaction, nextTransaction) => set({ transactions, prevTransaction, nextTransaction }), - addNutzap: (statusId, data) => + addZapCashu: (statusId) => set((state) => ({ - nutzapsList: { - ...state.nutzapsList, - [statusId]: data, - }, + zapCashuList: [ + ...state.zapCashuList, + statusId, + ], })), })); @@ -152,15 +152,15 @@ const useTransactions = () => { return { transactions, isLoading, error, getTransactions, expandTransactions }; }; -const useNutzapRequest = () => { +const useZapCashuRequest = () => { const api = useApi(); - const { nutzapsList, addNutzap } = useWalletStore(); + const { zapCashuList, addZapCashu } = useWalletStore(); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const { getWallet } = useWallet(); const { getTransactions } = useTransactions(); - const nutzapRequest = async (account: AccountEntity, amount: number, comment: string, status?: StatusEntity) => { + const zapCashuRequest = async (account: AccountEntity, amount: number, comment: string, status?: StatusEntity) => { setIsLoading(true); setError(null); try { @@ -174,7 +174,7 @@ const useNutzapRequest = () => { const data = await response.json(); if (status) { - addNutzap(status.id, { status, amount, comment }); + addZapCashu(status.id); } toast.success(data.message || 'Zap sent successfully!'); @@ -189,7 +189,7 @@ const useNutzapRequest = () => { } }; - return { nutzapsList, isLoading, error, nutzapRequest }; + return { zapCashuList, isLoading, error, zapCashuRequest }; }; -export { useWallet, useTransactions, useNutzapRequest }; \ No newline at end of file +export { useWallet, useTransactions, useZapCashuRequest }; \ No newline at end of file diff --git a/src/normalizers/status.ts b/src/normalizers/status.ts index 32945ea31..5535050f2 100644 --- a/src/normalizers/status.ts +++ b/src/normalizers/status.ts @@ -77,7 +77,7 @@ export const StatusRecord = ImmutableRecord({ reblogs_count: 0, replies_count: 0, zaps_amount: 0, - nutzaps_amount: 0, + zaps_amount_cashu: 0, sensitive: false, spoiler_text: '', tags: ImmutableList>(), @@ -86,7 +86,7 @@ export const StatusRecord = ImmutableRecord({ url: '', visibility: 'public' as StatusVisibility, zapped: false, - nutzapped: false, + zapped_cashu: false, event: null as ReturnType | null, // Internal fields diff --git a/src/reducers/statuses.ts b/src/reducers/statuses.ts index 69696387b..24c365e79 100644 --- a/src/reducers/statuses.ts +++ b/src/reducers/statuses.ts @@ -222,20 +222,13 @@ const simulateDislike = ( }; /** Simulate zap of status for optimistic interactions */ -const simulatePayment = (state: State, statusId: string, paid: boolean, method: 'cashu' | 'zap'): State => { +const simulatePayment = (state: State, statusId: string, zapped: boolean): State => { const status = state.get(statusId); if (!status) return state; - let updatedStatus; - if (method === 'zap') { - updatedStatus = status.merge({ - zapped: paid, - }); - } else { - updatedStatus = status.merge({ - nutzapped: paid, - }); - } + const updatedStatus = status.merge({ + zapped, + }); return state.set(statusId, updatedStatus); }; @@ -295,9 +288,9 @@ export default function statuses(state = initialState, action: AnyAction): State case DISLIKE_FAIL: return state.get(action.status.id) === undefined ? state : state.setIn([action.status.id, 'disliked'], false); case ZAP_REQUEST: - return simulatePayment(state, action.status.id, true, 'zap'); + return simulatePayment(state, action.status.id, true); case ZAP_FAIL: - return simulatePayment(state, action.status.id, false, 'zap'); + return simulatePayment(state, action.status.id, false); case REBLOG_REQUEST: return state.setIn([action.status.id, 'reblogged'], true); case REBLOG_FAIL: diff --git a/src/schemas/status.ts b/src/schemas/status.ts index 5a5d7280c..e16e74bc1 100644 --- a/src/schemas/status.ts +++ b/src/schemas/status.ts @@ -73,8 +73,8 @@ const baseStatusSchema = z.object({ visibility: z.string().catch('public'), zapped: z.coerce.boolean(), zaps_amount: z.number().catch(0), - nutzapped: z.coerce.boolean(), - nutzaps_amount: z.number().catch(0), + zapped_cashu: z.coerce.boolean(), + zaps_amount_cashu: z.number().catch(0), }); type BaseStatus = z.infer; From f3873b55bae51536969c9372899911ec538fcfba Mon Sep 17 00:00:00 2001 From: danidfra Date: Wed, 26 Mar 2025 17:59:36 -0300 Subject: [PATCH 52/62] Update fetch wallet data --- src/components/pure-status-action-bar.tsx | 7 +++--- src/components/status-action-bar.tsx | 7 +++--- src/features/zap/hooks/useHooks.ts | 27 ++++++++++++++++++----- src/pages/search-page.tsx | 2 +- 4 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/components/pure-status-action-bar.tsx b/src/components/pure-status-action-bar.tsx index 075fc00cf..2c47abfd9 100644 --- a/src/components/pure-status-action-bar.tsx +++ b/src/components/pure-status-action-bar.tsx @@ -48,7 +48,7 @@ import DropdownMenu from 'soapbox/components/dropdown-menu/index.ts'; import PureStatusReactionWrapper from 'soapbox/components/pure-status-reaction-wrapper.tsx'; import StatusActionButton from 'soapbox/components/status-action-button.tsx'; import HStack from 'soapbox/components/ui/hstack.tsx'; -import { useZapCashuRequest, useWallet } from 'soapbox/features/zap/hooks/useHooks.ts'; +import { useZapCashuRequest, useWalletStore } from 'soapbox/features/zap/hooks/useHooks.ts'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; import { useDislike } from 'soapbox/hooks/useDislike.ts'; @@ -180,7 +180,7 @@ const PureStatusActionBar: React.FC = ({ const features = useFeatures(); const { boostModal, deleteModal } = useSettings(); - const { wallet } = useWallet(); + const { acceptsZapsCashu } = useWalletStore(); const { zapCashuList } = useZapCashuRequest(); const isZappedCashu = zapCashuList.some((zapCashu)=> zapCashu === status.id); @@ -764,7 +764,6 @@ const PureStatusActionBar: React.FC = ({ const canShare = ('share' in navigator) && (status.visibility === 'public' || status.visibility === 'group'); const acceptsZaps = status.account.ditto.accepts_zaps === true; - const hasWallet = wallet !== null; const spacing: { [key: string]: React.ComponentProps['space']; @@ -848,7 +847,7 @@ const PureStatusActionBar: React.FC = ({ /> )} - {(acceptsZaps || hasWallet) && ( + {(acceptsZaps || acceptsZapsCashu) && ( = ({ const features = useFeatures(); const { boostModal, deleteModal } = useSettings(); - const { wallet } = useWallet(); + const { acceptsZapsCashu } = useWalletStore(); const { zapCashuList } = useZapCashuRequest(); const isZappedCashu = zapCashuList.some((zapCashu)=> zapCashu === status.id); @@ -754,7 +754,6 @@ const StatusActionBar: React.FC = ({ const canShare = ('share' in navigator) && (status.visibility === 'public' || status.visibility === 'group'); const acceptsZaps = status.account.ditto.accepts_zaps === true; - const hasWallet = wallet !== null; const spacing: { [key: string]: React.ComponentProps['space']; @@ -838,7 +837,7 @@ const StatusActionBar: React.FC = ({ /> )} - {(acceptsZaps || hasWallet) && ( + {(acceptsZaps || acceptsZapsCashu) && ( 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; } @@ -26,13 +32,19 @@ interface IWalletInfo { const useWalletStore = create((set) => ({ wallet: null, + acceptsZapsCashu: false, transactions: null, prevTransaction: null, nextTransaction: null, zapCashuList: [], + hasFetchedWallet: false, + hasFetchedTransactions: false, + 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: [ @@ -44,7 +56,7 @@ const useWalletStore = create((set) => ({ const useWallet = () => { const api = useApi(); - const { wallet, setWallet } = useWalletStore(); + const { wallet, setWallet, setAcceptsZapsCashu, hasFetchedWallet, setHasFetchedWallet } = useWalletStore(); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); @@ -74,6 +86,8 @@ const useWallet = () => { const data = await response.json(); if (data) { const normalizedData = baseWalletSchema.parse(data); + setAcceptsZapsCashu(true); + setWallet(normalizedData); } } catch (err) { @@ -82,11 +96,13 @@ const useWallet = () => { setError(messageError); } finally { setIsLoading(false); + setHasFetchedWallet(true); } }; useEffect(() => { - if (!wallet) { + if (!hasFetchedWallet) { + setHasFetchedWallet(true); getWallet(false); } }, []); @@ -96,7 +112,7 @@ const useWallet = () => { const useTransactions = () => { const api = useApi(); - const { transactions, nextTransaction, setTransactions } = useWalletStore(); + const { transactions, nextTransaction, setTransactions, hasFetchedTransactions, setHasFetchedTransactions } = useWalletStore(); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); @@ -144,7 +160,8 @@ const useTransactions = () => { }; useEffect(() => { - if (!transactions) { + if (!hasFetchedTransactions) { + setHasFetchedTransactions(true); getTransactions(); } }, []); @@ -192,4 +209,4 @@ const useZapCashuRequest = () => { return { zapCashuList, isLoading, error, zapCashuRequest }; }; -export { useWallet, useTransactions, useZapCashuRequest }; \ No newline at end of file +export { useWalletStore, useWallet, useTransactions, useZapCashuRequest }; \ No newline at end of file diff --git a/src/pages/search-page.tsx b/src/pages/search-page.tsx index 6b48177cc..e05df5441 100644 --- a/src/pages/search-page.tsx +++ b/src/pages/search-page.tsx @@ -23,7 +23,7 @@ const ExplorePage: React.FC = ({ children }) => { const me = useAppSelector(state => state.me); const features = useFeatures(); const accountsPath = useLocation().pathname === '/explore/accounts'; - const wallet = useWallet(); + const { wallet } = useWallet(); return ( <> From 1eec214a51c1ecff4e39e59e3aea92cc523fd2ea Mon Sep 17 00:00:00 2001 From: danidfra Date: Thu, 27 Mar 2025 14:26:27 -0300 Subject: [PATCH 53/62] Implement "nutzapped_by" --- .../ui/components/modals/zaps-modal.tsx | 23 +++++++---- src/features/zap/hooks/useHooks.ts | 41 ++++++++++++++++++- src/schemas/wallet.ts | 22 +++++++++- 3 files changed, 76 insertions(+), 10 deletions(-) diff --git a/src/features/ui/components/modals/zaps-modal.tsx b/src/features/ui/components/modals/zaps-modal.tsx index e6bd1fbbf..63a1d3f62 100644 --- a/src/features/ui/components/modals/zaps-modal.tsx +++ b/src/features/ui/components/modals/zaps-modal.tsx @@ -8,6 +8,7 @@ import Modal from 'soapbox/components/ui/modal.tsx'; import Spinner from 'soapbox/components/ui/spinner.tsx'; import Text from 'soapbox/components/ui/text.tsx'; import AccountContainer from 'soapbox/containers/account-container.tsx'; +import { useWalletStore, useZappedByCashu } from 'soapbox/features/zap/hooks/useHooks.ts'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; import { shortNumberFormat } from 'soapbox/utils/numbers.tsx'; @@ -16,6 +17,7 @@ interface IAccountWithZaps { id: string; comment: string; amount: number; + type: string; } interface IZapsModal { @@ -25,16 +27,22 @@ interface IZapsModal { const ZapsModal: React.FC = ({ onClose, statusId }) => { const dispatch = useAppDispatch(); + const zaps = useAppSelector((state) => state.user_lists.zapped_by.get(statusId)?.items); const next = useAppSelector((state) => state.user_lists.zapped_by.get(statusId)?.next); + const nutzaps = useWalletStore().nutzappedRecord[statusId]; + const { getNutzappedBy } = useZappedByCashu(); const accounts = useMemo((): ImmutableList | undefined => { - if (!zaps) return; + if (!zaps && !nutzaps) return; - return zaps - .map(({ account, amount, comment }) => ({ id: account, amount, comment })) - .flatten() as ImmutableList; - }, [zaps]); + const zappedAccounts = zaps?.map(({ account, amount, comment }) => ({ id: account, amount, comment, type: 'zap' })) || []; + const nutzappedAccounts = nutzaps?.map(({ account, amount, comment }) => ({ id: account.id, amount, comment, type: 'nutzap' })) || []; + + const combinedAccounts = [...zappedAccounts, ...nutzappedAccounts]; + + return ImmutableList(combinedAccounts); + }, [zaps, nutzaps]); const fetchData = () => { dispatch(fetchZaps(statusId)); @@ -42,6 +50,7 @@ const ZapsModal: React.FC = ({ onClose, statusId }) => { useEffect(() => { fetchData(); + getNutzappedBy(statusId); }, []); const onClickClose = () => { @@ -56,7 +65,7 @@ const ZapsModal: React.FC = ({ onClose, statusId }) => { let body; - if (!zaps || !accounts) { + if (!zaps || !nutzaps || !accounts) { body = ; } else { const emptyMessage = ; @@ -76,7 +85,7 @@ const ZapsModal: React.FC = ({ onClose, statusId }) => { return (
- {shortNumberFormat(account.amount / 1000)} + {account.type === 'zap' ? shortNumberFormat(account.amount / 1000) : shortNumberFormat(account.amount)}
diff --git a/src/features/zap/hooks/useHooks.ts b/src/features/zap/hooks/useHooks.ts index 99a4b6454..089a98f48 100644 --- a/src/features/zap/hooks/useHooks.ts +++ b/src/features/zap/hooks/useHooks.ts @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import { create } from 'zustand'; import { useApi } from 'soapbox/hooks/useApi.ts'; -import { Transactions, WalletData, baseWalletSchema, transactionsSchema } from 'soapbox/schemas/wallet.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'; @@ -12,11 +12,13 @@ interface WalletState { acceptsZapsCashu: boolean; transactions: Transactions | null; zapCashuList: string[]; + nutzappedRecord: NutzappedRecord; prevTransaction?: string | null; nextTransaction?: string | null; hasFetchedWallet: boolean; hasFetchedTransactions: boolean; + setNutzappedRecord: (statusId: string, nutzappedEntry: NutzappedEntry) => void; setAcceptsZapsCashu: (acceptsZapsCashu: boolean) => void; setWallet: (wallet: WalletData | null) => void; setHasFetchedWallet: (hasFetchedWallet: boolean) => void; @@ -37,9 +39,16 @@ const useWalletStore = create((set) => ({ prevTransaction: null, nextTransaction: null, zapCashuList: [], + nutzappedRecord: {}, hasFetchedWallet: false, hasFetchedTransactions: false, + setNutzappedRecord: (statusId, nutzappedEntry) => set((state)=> ({ + nutzappedRecord: { + ...state.nutzappedRecord, + [statusId]: nutzappedEntry, + }, + })), setAcceptsZapsCashu: (acceptsZapsCashu) => set({ acceptsZapsCashu }), setWallet: (wallet) => set({ wallet }), setHasFetchedWallet: (hasFetchedWallet) => set({ hasFetchedWallet }), @@ -209,4 +218,32 @@ const useZapCashuRequest = () => { return { zapCashuList, isLoading, error, zapCashuRequest }; }; -export { useWalletStore, useWallet, useTransactions, useZapCashuRequest }; \ No newline at end of file +const useZappedByCashu = () => { + const api = useApi(); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const { 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(); // TODO: pagination after Patrick finish + const data = await response.json(); + if (data) { + const normalizedData = nutzappedEntry.parse(data); + setNutzappedRecord(statusId, normalizedData); + } + } catch (err) { + const messageError = err instanceof Error ? err.message : 'Zaps not found'; + toast.error('Zaps not foud'); + setError(messageError); + } finally { + setIsLoading(false); + } + }; + + return { error, isLoading, getNutzappedBy }; +}; + +export { useWalletStore, useWallet, useTransactions, useZapCashuRequest, useZappedByCashu }; \ No newline at end of file diff --git a/src/schemas/wallet.ts b/src/schemas/wallet.ts index 5800443d3..84e12b0f0 100644 --- a/src/schemas/wallet.ts +++ b/src/schemas/wallet.ts @@ -1,6 +1,8 @@ import { NSchema as n } from '@nostrify/nostrify'; import { z } from 'zod'; +import { accountSchema } from 'soapbox/schemas/account.ts'; + const baseWalletSchema = z.object({ pubkey_p2pk: n.id(), mints: z.array(z.string().url()).nonempty(), @@ -22,12 +24,30 @@ const transactionSchema = z.object({ direction: z.enum(['in', 'out']), }); + +const nutzappedEntry = z.array( + z.object({ + comment: z.string(), + amount: z.number(), + account: accountSchema, + }), +); + +const nutzappedRecord = z.record( + z.string(), + nutzappedEntry, +); + const transactionsSchema = z.array(transactionSchema); +type NutzappedEntry = z.infer + +type NutzappedRecord = z.infer + type Transactions = z.infer type Quote = z.infer type WalletData = z.infer; -export { baseWalletSchema, quoteSchema, transactionsSchema, type WalletData, type Quote, type Transactions }; \ No newline at end of file +export { baseWalletSchema, quoteSchema, transactionsSchema, nutzappedRecord, nutzappedEntry, type WalletData, type Quote, type Transactions, type NutzappedRecord, type NutzappedEntry }; \ No newline at end of file From d22163b87cb7857e4c16813452f0a521f59bc14b Mon Sep 17 00:00:00 2001 From: danidfra Date: Thu, 27 Mar 2025 20:25:35 -0300 Subject: [PATCH 54/62] Update zaps-modal to include nutzapped_by details --- .../ui/components/modals/zaps-modal.tsx | 8 +++- src/features/zap/hooks/useHooks.ts | 44 ++++++++++++++++--- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/features/ui/components/modals/zaps-modal.tsx b/src/features/ui/components/modals/zaps-modal.tsx index 63a1d3f62..083602452 100644 --- a/src/features/ui/components/modals/zaps-modal.tsx +++ b/src/features/ui/components/modals/zaps-modal.tsx @@ -31,7 +31,8 @@ const ZapsModal: React.FC = ({ onClose, statusId }) => { const zaps = useAppSelector((state) => state.user_lists.zapped_by.get(statusId)?.items); const next = useAppSelector((state) => state.user_lists.zapped_by.get(statusId)?.next); const nutzaps = useWalletStore().nutzappedRecord[statusId]; - const { getNutzappedBy } = useZappedByCashu(); + const { nextZaps } = useWalletStore(); + const { getNutzappedBy, expandNutzappedBy } = useZappedByCashu(); const accounts = useMemo((): ImmutableList | undefined => { if (!zaps && !nutzaps) return; @@ -61,6 +62,9 @@ const ZapsModal: React.FC = ({ onClose, statusId }) => { if (next) { dispatch(expandZaps(statusId, next)); } + if (nextZaps) { + expandNutzappedBy(statusId); + } }; let body; @@ -79,7 +83,7 @@ const ZapsModal: React.FC = ({ onClose, statusId }) => { style={{ height: '80vh' }} useWindowScroll={false} onLoadMore={handleLoadMore} - hasMore={!!next} + hasMore={!!next || !!nextZaps} > {accounts.map((account, index) => { return ( diff --git a/src/features/zap/hooks/useHooks.ts b/src/features/zap/hooks/useHooks.ts index 089a98f48..51d241fbe 100644 --- a/src/features/zap/hooks/useHooks.ts +++ b/src/features/zap/hooks/useHooks.ts @@ -1,3 +1,4 @@ +import { debounce } from 'es-toolkit'; import { useEffect, useState } from 'react'; import { create } from 'zustand'; @@ -15,10 +16,12 @@ interface WalletState { nutzappedRecord: NutzappedRecord; prevTransaction?: string | null; nextTransaction?: string | null; + prevZaps?: string | null; + nextZaps?: string | null; hasFetchedWallet: boolean; hasFetchedTransactions: boolean; - setNutzappedRecord: (statusId: string, nutzappedEntry: NutzappedEntry) => void; + 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; @@ -38,16 +41,20 @@ const useWalletStore = create((set) => ({ transactions: null, prevTransaction: null, nextTransaction: null, + prevZaps: null, + nextZaps: null, zapCashuList: [], nutzappedRecord: {}, hasFetchedWallet: false, hasFetchedTransactions: false, - setNutzappedRecord: (statusId, nutzappedEntry) => set((state)=> ({ + setNutzappedRecord: (statusId, nutzappedEntry, prevZaps, nextZaps) => set((state)=> ({ nutzappedRecord: { ...state.nutzappedRecord, [statusId]: nutzappedEntry, }, + prevZaps, + nextZaps, })), setAcceptsZapsCashu: (acceptsZapsCashu) => set({ acceptsZapsCashu }), setWallet: (wallet) => set({ wallet }), @@ -222,17 +229,17 @@ const useZappedByCashu = () => { const api = useApi(); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); - const { setNutzappedRecord } = useWalletStore(); + 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(); // TODO: pagination after Patrick finish + const { prev, next } = response.pagination(); const data = await response.json(); if (data) { const normalizedData = nutzappedEntry.parse(data); - setNutzappedRecord(statusId, normalizedData); + setNutzappedRecord(statusId, normalizedData, prev, next); } } catch (err) { const messageError = err instanceof Error ? err.message : 'Zaps not found'; @@ -243,7 +250,32 @@ const useZappedByCashu = () => { } }; - return { error, isLoading, getNutzappedBy }; + 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 }; \ No newline at end of file From 7c9f740258091b226f3ad8cb85d5ae991be5851b Mon Sep 17 00:00:00 2001 From: danidfra Date: Fri, 28 Mar 2025 10:39:35 -0300 Subject: [PATCH 55/62] Implement a tooltip in create-wallet --- src/components/ui/tooltip.tsx | 2 +- src/features/wallet/components/create-wallet.tsx | 13 +++++++++---- src/locales/en.json | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx index cce1728a2..08663da25 100644 --- a/src/components/ui/tooltip.tsx +++ b/src/components/ui/tooltip.tsx @@ -78,7 +78,7 @@ const Tooltip: React.FC = (props) => { left: x ?? 0, ...styles, }} - className='pointer-events-none z-[100] whitespace-nowrap rounded bg-gray-800 px-2.5 py-1.5 text-xs font-medium text-gray-100 shadow dark:bg-gray-100 dark:text-gray-900' + className='pointer-events-none z-[100] max-w-[200px] whitespace-normal rounded bg-gray-800 px-2.5 py-1.5 text-xs font-medium text-gray-100 shadow dark:bg-gray-100 dark:text-gray-900 sm:max-w-[300px]' {...getFloatingProps()} > {text} diff --git a/src/features/wallet/components/create-wallet.tsx b/src/features/wallet/components/create-wallet.tsx index c7d19049e..af586c031 100644 --- a/src/features/wallet/components/create-wallet.tsx +++ b/src/features/wallet/components/create-wallet.tsx @@ -1,4 +1,4 @@ -import exclamationIcon from '@tabler/icons/outline/exclamation-circle.svg'; +import helpIcon from '@tabler/icons/outline/help-circle.svg'; import plusIcon from '@tabler/icons/outline/square-rounded-plus.svg'; import { useState } from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; @@ -7,9 +7,10 @@ import Button from 'soapbox/components/ui/button.tsx'; import FormActions from 'soapbox/components/ui/form-actions.tsx'; import Form from 'soapbox/components/ui/form.tsx'; import HStack from 'soapbox/components/ui/hstack.tsx'; +import Icon from 'soapbox/components/ui/icon.tsx'; import Stack from 'soapbox/components/ui/stack.tsx'; -import SvgIcon from 'soapbox/components/ui/svg-icon.tsx'; import Text from 'soapbox/components/ui/text.tsx'; +import Tooltip from 'soapbox/components/ui/tooltip.tsx'; import { MintEditor } from 'soapbox/features/wallet/components/editable-lists.tsx'; import { useWallet } from 'soapbox/features/zap/hooks/useHooks.ts'; import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts'; @@ -17,7 +18,7 @@ import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts'; const messages = defineMessages({ title: { id: 'wallet.create_wallet.title', defaultMessage: 'You don\'t have a wallet' }, question: { id: 'wallet.create_wallet.question', defaultMessage: 'Do you want create one?' }, - button: { id: 'wallet.button.create_wallet', defaultMessage: 'Create wallet' }, + button: { id: 'wallet.button.create_wallet', defaultMessage: 'Create' }, mints: { id: 'wallet.mints', defaultMessage: 'Mints' }, }); @@ -72,7 +73,11 @@ const CreateWallet = () => { {intl.formatMessage(messages.mints)} - + +
+ +
+
diff --git a/src/locales/en.json b/src/locales/en.json index 6d91785be..008f08701 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1718,7 +1718,7 @@ "wallet.balance.mint.payment": "Make the payment to complete:", "wallet.balance.mint.unpaid_message": "Your mint is still unpaid. Complete the payment to receive your sats.", "wallet.button.cancel": "Cancel", - "wallet.button.create_wallet": "Create wallet", + "wallet.button.create_wallet": "Create", "wallet.button.mint": "Mint", "wallet.button.withdraw": "Withdraw", "wallet.create_wallet.question": "Do you want create one?", From 0957c5b92e7d53d593186da082e8d9598d4afc1d Mon Sep 17 00:00:00 2001 From: danidfra Date: Fri, 28 Mar 2025 10:50:43 -0300 Subject: [PATCH 56/62] Refactor code to use accepts_zaps_cashu --- src/components/pure-status-action-bar.tsx | 4 ++-- src/components/status-action-bar.tsx | 4 ++-- src/features/account/components/header.tsx | 8 ++------ src/schemas/account.ts | 1 + 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/components/pure-status-action-bar.tsx b/src/components/pure-status-action-bar.tsx index 2c47abfd9..d1f646f23 100644 --- a/src/components/pure-status-action-bar.tsx +++ b/src/components/pure-status-action-bar.tsx @@ -48,7 +48,7 @@ import DropdownMenu from 'soapbox/components/dropdown-menu/index.ts'; import PureStatusReactionWrapper from 'soapbox/components/pure-status-reaction-wrapper.tsx'; import StatusActionButton from 'soapbox/components/status-action-button.tsx'; import HStack from 'soapbox/components/ui/hstack.tsx'; -import { useZapCashuRequest, useWalletStore } from 'soapbox/features/zap/hooks/useHooks.ts'; +import { useZapCashuRequest } from 'soapbox/features/zap/hooks/useHooks.ts'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; import { useDislike } from 'soapbox/hooks/useDislike.ts'; @@ -180,7 +180,6 @@ const PureStatusActionBar: React.FC = ({ const features = useFeatures(); const { boostModal, deleteModal } = useSettings(); - const { acceptsZapsCashu } = useWalletStore(); const { zapCashuList } = useZapCashuRequest(); const isZappedCashu = zapCashuList.some((zapCashu)=> zapCashu === status.id); @@ -764,6 +763,7 @@ const PureStatusActionBar: React.FC = ({ const canShare = ('share' in navigator) && (status.visibility === 'public' || status.visibility === 'group'); const acceptsZaps = status.account.ditto.accepts_zaps === true; + const acceptsZapsCashu = status.account.ditto.accepts_zaps_cashu === true; const spacing: { [key: string]: React.ComponentProps['space']; diff --git a/src/components/status-action-bar.tsx b/src/components/status-action-bar.tsx index 4e98628b8..c2c7a8e73 100644 --- a/src/components/status-action-bar.tsx +++ b/src/components/status-action-bar.tsx @@ -49,7 +49,7 @@ import DropdownMenu from 'soapbox/components/dropdown-menu/index.ts'; import StatusActionButton from 'soapbox/components/status-action-button.tsx'; import StatusReactionWrapper from 'soapbox/components/status-reaction-wrapper.tsx'; import HStack from 'soapbox/components/ui/hstack.tsx'; -import { useZapCashuRequest, useWalletStore } from 'soapbox/features/zap/hooks/useHooks.ts'; +import { useZapCashuRequest } from 'soapbox/features/zap/hooks/useHooks.ts'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; import { useFeatures } from 'soapbox/hooks/useFeatures.ts'; @@ -175,7 +175,6 @@ const StatusActionBar: React.FC = ({ const features = useFeatures(); const { boostModal, deleteModal } = useSettings(); - const { acceptsZapsCashu } = useWalletStore(); const { zapCashuList } = useZapCashuRequest(); const isZappedCashu = zapCashuList.some((zapCashu)=> zapCashu === status.id); @@ -754,6 +753,7 @@ const StatusActionBar: React.FC = ({ const canShare = ('share' in navigator) && (status.visibility === 'public' || status.visibility === 'group'); const acceptsZaps = status.account.ditto.accepts_zaps === true; + const acceptsZapsCashu = status.account.ditto.accepts_zaps_cashu === true; const spacing: { [key: string]: React.ComponentProps['space']; diff --git a/src/features/account/components/header.tsx b/src/features/account/components/header.tsx index c909f8f86..a26d1fc67 100644 --- a/src/features/account/components/header.tsx +++ b/src/features/account/components/header.tsx @@ -44,7 +44,6 @@ import VerificationBadge from 'soapbox/components/verification-badge.tsx'; import MovedNote from 'soapbox/features/account-timeline/components/moved-note.tsx'; import ActionButton from 'soapbox/features/ui/components/action-button.tsx'; import SubscriptionButton from 'soapbox/features/ui/components/subscription-button.tsx'; -import { useWallet } from 'soapbox/features/zap/hooks/useHooks.ts'; import { usePaymentMethod } from 'soapbox/features/zap/usePaymentMethod.ts'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; @@ -121,9 +120,6 @@ const Header: React.FC = ({ account }) => { const { software } = useAppSelector((state) => parseVersion(state.instance.version)); - const { wallet } = useWallet(); - - const { getOrCreateChatByAccountId } = useChats(); const createAndNavigateToChat = useMutation({ @@ -684,7 +680,7 @@ const Header: React.FC = ({ account }) => { const info = makeInfo(); const menu = makeMenu(); const acceptsZaps = account.ditto.accepts_zaps === true; - const hasWallet = wallet !== null; + const acceptsZapsCashu = account.ditto.accepts_zaps_cashu === true; return (
@@ -726,7 +722,7 @@ const Header: React.FC = ({ account }) => { {renderMessageButton()} {renderShareButton()} - {(acceptsZaps || hasWallet) && renderZapAccount()} + {(acceptsZaps || acceptsZapsCashu) && renderZapAccount()} {menu.length > 0 && ( diff --git a/src/schemas/account.ts b/src/schemas/account.ts index e95518527..0720aa04e 100644 --- a/src/schemas/account.ts +++ b/src/schemas/account.ts @@ -38,6 +38,7 @@ const baseAccountSchema = z.object({ display_name: z.string().catch(''), ditto: coerceObject({ accepts_zaps: z.boolean().catch(false), + accepts_zaps_cashu: z.boolean().catch(false), external_url: z.string().optional().catch(undefined), streak: coerceObject({ days: z.number().catch(0), From 21fc7c8ed9cd1659a192e2eb3808bdd47a49e71e Mon Sep 17 00:00:00 2001 From: danidfra Date: Tue, 1 Apr 2025 17:45:07 -0300 Subject: [PATCH 57/62] Update translated text and use Goose to update small things in wallet --- src/features/account/components/header.tsx | 2 +- .../ui/components/modals/zaps-modal.tsx | 2 +- .../wallet/components/create-wallet.tsx | 123 ++++++++++++------ src/features/wallet/index.tsx | 27 ++-- .../zap/components/pay-request-form.tsx | 4 +- src/features/zap/usePaymentMethod.ts | 8 +- src/locales/ar.json | 2 +- src/locales/en.json | 2 +- src/locales/es.json | 2 +- src/locales/ga.json | 2 +- src/locales/pt-BR.json | 2 +- src/locales/pt.json | 2 +- src/locales/zh-CN.json | 2 +- 13 files changed, 114 insertions(+), 66 deletions(-) diff --git a/src/features/account/components/header.tsx b/src/features/account/components/header.tsx index a26d1fc67..d76cfecef 100644 --- a/src/features/account/components/header.tsx +++ b/src/features/account/components/header.tsx @@ -100,7 +100,7 @@ const messages = defineMessages({ profileExternal: { id: 'account.profile_external', defaultMessage: 'View profile on {domain}' }, header: { id: 'account.header.alt', defaultMessage: 'Profile header' }, subscribeFeed: { id: 'account.rss_feed', defaultMessage: 'Subscribe to RSS feed' }, - method: { id: 'payment_method.send_to', defaultMessage: 'Send {method} to {target}' }, + method: { id: 'payment_method.send_to', defaultMessage: 'Send sats via {method} to {target}' }, }); interface IHeader { diff --git a/src/features/ui/components/modals/zaps-modal.tsx b/src/features/ui/components/modals/zaps-modal.tsx index 083602452..a6aaf23ea 100644 --- a/src/features/ui/components/modals/zaps-modal.tsx +++ b/src/features/ui/components/modals/zaps-modal.tsx @@ -89,7 +89,7 @@ const ZapsModal: React.FC = ({ onClose, statusId }) => { return (
- {account.type === 'zap' ? shortNumberFormat(account.amount / 1000) : shortNumberFormat(account.amount)} + {account.type === 'lightning' ? shortNumberFormat(account.amount / 1000) : shortNumberFormat(account.amount)}
diff --git a/src/features/wallet/components/create-wallet.tsx b/src/features/wallet/components/create-wallet.tsx index af586c031..759cf2969 100644 --- a/src/features/wallet/components/create-wallet.tsx +++ b/src/features/wallet/components/create-wallet.tsx @@ -47,54 +47,91 @@ const CreateWallet = () => { } return ( - + - {!formActive - ? - ( - <> - - {intl.formatMessage(messages.title)} + {!formActive ? ( + <> +
+ +
+ + {intl.formatMessage(messages.title)} + + + + {intl.formatMessage(messages.question)} - - - {intl.formatMessage(messages.question)} - - + + - - - -
- ) - } + + + +
+ )}
); diff --git a/src/features/wallet/index.tsx b/src/features/wallet/index.tsx index 5648c5c61..cdf1c3e96 100644 --- a/src/features/wallet/index.tsx +++ b/src/features/wallet/index.tsx @@ -26,7 +26,7 @@ const messages = defineMessages({ }); const paymentMethods = { - zap: 'zap', + lightning: 'lightning', cashu: 'cashu', }; @@ -45,13 +45,15 @@ const Wallet = () => { return ( <> {isLoading ? - - + +
+ +
: ( - + @@ -59,7 +61,11 @@ const Wallet = () => { {walletData ? ( <> - + @@ -70,8 +76,13 @@ const Wallet = () => { - {hasTransactions &&
-
} @@ -91,7 +102,7 @@ const Wallet = () => { items={paymentMethods} defaultValue={method} onChange={(event: React.ChangeEvent) => { - changeMethod((event.target.value as 'cashu' | 'zap')); + changeMethod((event.target.value as 'cashu' | 'lightning')); }} /> diff --git a/src/features/zap/components/pay-request-form.tsx b/src/features/zap/components/pay-request-form.tsx index f95d813a0..66d47faaa 100644 --- a/src/features/zap/components/pay-request-form.tsx +++ b/src/features/zap/components/pay-request-form.tsx @@ -128,8 +128,8 @@ const PayRequestForm = ({ account, status, onClose }: IPayRequestForm) => { 1 ? `${paymentMethod}s` : paymentMethod }} + defaultMessage='Send sats via {method} to {target}' + values={{ target: emojifyText(account.display_name, account.emojis), method: paymentMethod }} /> diff --git a/src/features/zap/usePaymentMethod.ts b/src/features/zap/usePaymentMethod.ts index e0f8df9aa..f82afdfa3 100644 --- a/src/features/zap/usePaymentMethod.ts +++ b/src/features/zap/usePaymentMethod.ts @@ -4,14 +4,14 @@ import { create } from 'zustand'; enableMapSet(); interface IPaymentMethod { - method: 'cashu' | 'zap'; - changeMethod: (method: 'cashu' | 'zap') => void; + method: 'cashu' | 'lightning'; + changeMethod: (method: 'cashu' | 'lightning') => void; } export const usePaymentMethod = create( (set) => ({ - method: localStorage.getItem('soapbox:payment_method') as 'cashu' | 'zap' || 'cashu', - changeMethod: (method: 'cashu' | 'zap') => { + method: localStorage.getItem('soapbox:payment_method') as 'cashu' | 'lightning' || 'cashu', + changeMethod: (method: 'cashu' | 'lightning') => { localStorage.setItem('soapbox:payment_method', method); set({ method }); }, diff --git a/src/locales/ar.json b/src/locales/ar.json index 67f8240c8..71deb73a9 100644 --- a/src/locales/ar.json +++ b/src/locales/ar.json @@ -1523,5 +1523,5 @@ "payment_method.comment_input.placeholder": "تعليق إختياري", "zap.open_wallet": "فتح المحفظة", "zap.send_to": "أرسل zaps إلى {target}", - "payment_method.send_to": "أرسل {method} إلى {target}" + "payment_method.send_to": "أرسل sats عبر {method} إلى {target}" } diff --git a/src/locales/en.json b/src/locales/en.json index 008f08701..86ee426c1 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1297,7 +1297,7 @@ "patron.title": "Funding Goal", "payment_method.button.text.raw": "Zap {amount} sats", "payment_method.comment_input.placeholder": "Optional comment", - "payment_method.send_to": "Send {method} to {target}", + "payment_method.send_to": "Send sats via {method} to {target}", "payment_method.split_message.deducted": "{amountDeducted} sats will deducted*", "pinned_accounts.title": "{name}’s choices", "pinned_statuses.none": "No pins to show.", diff --git a/src/locales/es.json b/src/locales/es.json index 6df0403ed..4189f115b 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -1630,5 +1630,5 @@ "payment_method.comment_input.placeholder": "Comentario opcional", "zap.open_wallet": "Abrir Wallet", "zap.send_to": "Enviar zaps a {target}", - "payment_method.send_to": "Enviar {method} a {target}" + "payment_method.send_to": "Enviar sats a través de {method} a {target}" } diff --git a/src/locales/ga.json b/src/locales/ga.json index a7de77b12..d33088d97 100644 --- a/src/locales/ga.json +++ b/src/locales/ga.json @@ -1686,7 +1686,7 @@ "zap.next": "Ar Aghaidh", "zap.open_wallet": "Oscail Sparán", "zap.send_to": "Seol zaps chuig {target}", - "payment_method.send_to": "Seol {method} chuig {target}", + "payment_method.send_to": "Seol sats trí {method} chuig {target}", "payment_method.split_message.deducted": "Asbhainfear {amountDeducted} sats*", "zap.split_message.receiver": "Gheobhaidh {receiver} {amountReceiver} sats*", "zap_split.question": "Cén fáth a bhfuil mé ag íoc seo?", diff --git a/src/locales/pt-BR.json b/src/locales/pt-BR.json index 1657c2da3..7d8c49339 100644 --- a/src/locales/pt-BR.json +++ b/src/locales/pt-BR.json @@ -1639,7 +1639,7 @@ "payment_method.comment_input.placeholder": "Comentário opcional", "zap.open_wallet": "Abrir Carteira", "zap.send_to": "Enviar zaps para {target}", - "payment_method.send_to": "Enviar {method} para {target}", + "payment_method.send_to": "Enviar sats via {method} para {target}", "payment_method.split_message.deducted": "{amountDeducted} sats serão deduzidos*", "zap.split_message.receiver": "{receiver} receberá {amountReceiver} sats*", "zap_split.question": "Por que estou pagando isso?", diff --git a/src/locales/pt.json b/src/locales/pt.json index 45a1850ff..d85f3855f 100644 --- a/src/locales/pt.json +++ b/src/locales/pt.json @@ -1618,6 +1618,6 @@ "who_to_follow.title": "Quem Seguir", "payment_method.comment_input.placeholder": "Comentário opcional", "zap.open_wallet": "Abrir Carteira", - "payment_method.send_to": "Enviar {method} para {target}", + "payment_method.send_to": "Enviar sats através de {method} para {target}", "zap.send_to": "Enviar zaps para {target}" } diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json index 40739cd3e..c8f0dfb3f 100644 --- a/src/locales/zh-CN.json +++ b/src/locales/zh-CN.json @@ -1623,6 +1623,6 @@ "payment_method.button.text.raw": "打闪 {amount} 聪", "payment_method.comment_input.placeholder": "可选评论", "zap.open_wallet": "打开钱包", - "payment_method.send_to": "发送打闪给 {target}", + "payment_method.send_to": "通过 {method} 发送 sats 到 {target}", "zap.send_to": "发送打闪给 {target}" } From 0c92375c42b95d12bb03a7a391dd29c3252e96e6 Mon Sep 17 00:00:00 2001 From: danidfra Date: Tue, 1 Apr 2025 18:05:07 -0300 Subject: [PATCH 58/62] Remove toast --- .../wallet/components/wallet-transactions.tsx | 16 +- src/features/wallet/index.tsx | 139 +++++++++--------- src/features/zap/hooks/useHooks.ts | 8 +- src/locales/en.json | 1 + 4 files changed, 83 insertions(+), 81 deletions(-) diff --git a/src/features/wallet/components/wallet-transactions.tsx b/src/features/wallet/components/wallet-transactions.tsx index af9ec6116..933e43ac5 100644 --- a/src/features/wallet/components/wallet-transactions.tsx +++ b/src/features/wallet/components/wallet-transactions.tsx @@ -10,6 +10,7 @@ const messages = defineMessages({ title: { id: 'wallet.transactions', defaultMessage: 'Transactions' }, more: { id: 'wallet.transactions.show_more', defaultMessage: 'Show More' }, loading: { id: 'wallet.loading', defaultMessage: 'Loading…' }, + noMoreTransactions: { id: 'wallet.transactions.no_more', defaultMessage: 'You reached the end of transactions' }, }); const WalletTransactions = () => { @@ -17,13 +18,15 @@ const WalletTransactions = () => { const { isLoading, expandTransactions } = useTransactions(); const observerRef = useRef(null); const [showSpinner, setShowSpinner] = useState(false); + const [hasMoreTransactions, setHasMoreTransactions] = useState(true); useEffect(() => { const observer = new IntersectionObserver( - ([entry]) => { - if (entry.isIntersecting) { + async ([entry]) => { + if (entry.isIntersecting && !isLoading && hasMoreTransactions) { setShowSpinner(true); - if (!isLoading) expandTransactions(); + const hasMore = await expandTransactions(); + setHasMoreTransactions(hasMore); } }, { rootMargin: '100px' }, @@ -34,13 +37,18 @@ const WalletTransactions = () => { } return () => observer.disconnect(); - }, [expandTransactions, isLoading]); + }, [expandTransactions, isLoading, hasMoreTransactions]); return (
{showSpinner && isLoading && } + {!hasMoreTransactions && ( +
+ {intl.formatMessage(messages.noMoreTransactions)} +
+ )}
); diff --git a/src/features/wallet/index.tsx b/src/features/wallet/index.tsx index cdf1c3e96..6efe77118 100644 --- a/src/features/wallet/index.tsx +++ b/src/features/wallet/index.tsx @@ -44,85 +44,78 @@ const Wallet = () => { return ( <> - {isLoading ? + {isLoading ? ( -
- -
+
- : - ( - - - - - + ) : ( + + + + + - {walletData ? ( - <> - - + + + + + + + + + + + + + {hasTransactions &&
+ +
} +
- - - + + + - - - {hasTransactions &&
- -
} -
- - - - - - - - - - - ) => { - changeMethod((event.target.value as 'cashu' | 'lightning')); - }} - /> - - - - - - ) - : - <> - - - - - - } -
-
- ) - } + + + + + + ) => { + changeMethod((event.target.value as 'cashu' | 'lightning')); + }} + /> + + + + + ) : ( + <> + + + + + )} +
+
+ )} ); }; diff --git a/src/features/zap/hooks/useHooks.ts b/src/features/zap/hooks/useHooks.ts index 51d241fbe..d9dcdbfd4 100644 --- a/src/features/zap/hooks/useHooks.ts +++ b/src/features/zap/hooks/useHooks.ts @@ -73,7 +73,7 @@ const useWalletStore = create((set) => ({ const useWallet = () => { const api = useApi(); const { wallet, setWallet, setAcceptsZapsCashu, hasFetchedWallet, setHasFetchedWallet } = useWalletStore(); - const [isLoading, setIsLoading] = useState(false); + const [isLoading, setIsLoading] = useState(!hasFetchedWallet); const [error, setError] = useState(null); const createWallet = async (walletInfo: IWalletInfo) => { @@ -118,7 +118,6 @@ const useWallet = () => { useEffect(() => { if (!hasFetchedWallet) { - setHasFetchedWallet(true); getWallet(false); } }, []); @@ -153,8 +152,7 @@ const useTransactions = () => { const expandTransactions = async () => { if (!nextTransaction || !transactions) { - toast.info('You reached the end of transactions'); - return; + return false; } try { setIsLoading(true); @@ -166,10 +164,12 @@ const useTransactions = () => { const newTransactions = [...(transactions ?? []), ...normalizedData ]; setTransactions(newTransactions, prev, next); + return true; // Return true to indicate successful expansion } catch (err) { const messageError = err instanceof Error ? err.message : 'Error expanding transactions'; toast.error(messageError); setError(messageError); + return false; } finally { setIsLoading(false); } diff --git a/src/locales/en.json b/src/locales/en.json index 86ee426c1..a686ec86d 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1740,6 +1740,7 @@ "wallet.title": "Wallet", "wallet.transactions": "Transactions", "wallet.transactions.more": "More", + "wallet.transactions.no_more": "You reached the end of transactions", "wallet.transactions.no_transactions": "You don't have transactions yet.", "wallet.transactions.show_more": "Show More", "who_to_follow.title": "People To Follow", From d8665fa7a51bba6435cf2d9a9f8073954d177f01 Mon Sep 17 00:00:00 2001 From: danidfra Date: Tue, 1 Apr 2025 22:59:38 -0300 Subject: [PATCH 59/62] Add toggle button in zap-modal to switch between Cashu and Lightning, and apply small updates using Goose --- .../components/status-interaction-bar.tsx | 2 +- src/features/ui/components/pocket-wallet.tsx | 13 +--- .../wallet/components/wallet-mints.tsx | 60 +++++++++++++++---- .../wallet/components/wallet-relays.tsx | 60 ++++++++++++++----- .../zap/components/pay-request-form.tsx | 59 ++++++++++++++---- src/features/zap/hooks/useHooks.ts | 10 ++-- src/locales/en.json | 7 ++- 7 files changed, 153 insertions(+), 58 deletions(-) diff --git a/src/features/status/components/status-interaction-bar.tsx b/src/features/status/components/status-interaction-bar.tsx index 9f43a85bb..8657d95e0 100644 --- a/src/features/status/components/status-interaction-bar.tsx +++ b/src/features/status/components/status-interaction-bar.tsx @@ -212,7 +212,7 @@ const StatusInteractionBar: React.FC = ({ status }): JSX. ); diff --git a/src/features/ui/components/pocket-wallet.tsx b/src/features/ui/components/pocket-wallet.tsx index ddb4b1b44..0398f5eaa 100644 --- a/src/features/ui/components/pocket-wallet.tsx +++ b/src/features/ui/components/pocket-wallet.tsx @@ -39,7 +39,7 @@ const PocketWallet = () => { - + {intl.formatMessage(messages.wallet)} @@ -47,16 +47,7 @@ const PocketWallet = () => { {!expanded && <> - { eyeClosed ? - - - - - - - - - : + { eyeClosed ? •••••• : {intl.formatMessage(messages.balance, { amount: wallet?.balance })} } diff --git a/src/features/wallet/components/wallet-mints.tsx b/src/features/wallet/components/wallet-mints.tsx index c9ca48c5b..941607be2 100644 --- a/src/features/wallet/components/wallet-mints.tsx +++ b/src/features/wallet/components/wallet-mints.tsx @@ -3,7 +3,9 @@ import { defineMessages, useIntl } from 'react-intl'; import Button from 'soapbox/components/ui/button.tsx'; import { Column } from 'soapbox/components/ui/column.tsx'; +import Spinner from 'soapbox/components/ui/spinner.tsx'; import Stack from 'soapbox/components/ui/stack.tsx'; +import Text from 'soapbox/components/ui/text.tsx'; import { MintEditor } from 'soapbox/features/wallet/components/editable-lists.tsx'; import { useWallet } from 'soapbox/features/zap/hooks/useHooks.ts'; import { useApi } from 'soapbox/hooks/useApi.ts'; @@ -16,7 +18,7 @@ const messages = defineMessages({ error: { id: 'wallet.mints.error', defaultMessage: 'Failed to update mints.' }, empty: { id: 'wallet.mints.empty', defaultMessage: 'At least one mint is required.' }, url: { id: 'wallet.invalid_url', defaultMessage: 'All strings must be valid URLs.' }, - sucess: { id: 'wallet.mints.sucess', defaultMessage: 'Mints updated with success!' }, + success: { id: 'wallet.mints.success', defaultMessage: 'Mints updated with success!' }, send: { id: 'common.send', defaultMessage: 'Send' }, }); @@ -28,6 +30,8 @@ const WalletMints = () => { const [relays, setRelays] = useState([]); const [initialMints, setInitialMints] = useState([]); const [mints, setMints] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [hasError, setHasError] = useState(false); const handleClick = async () =>{ if (mints.length === 0) { @@ -48,7 +52,7 @@ const WalletMints = () => { setInitialMints(mints); - toast.success(intl.formatMessage(messages.sucess)); + toast.success(intl.formatMessage(messages.success)); } catch (error) { const errorMessage = error instanceof Error ? error.message : intl.formatMessage(messages.error); toast.error(errorMessage); @@ -58,23 +62,53 @@ const WalletMints = () => { useEffect( () => { + setIsLoading(true); + setHasError(false); + if (wallet) { - setMints(wallet.mints ?? []); - setInitialMints(wallet.mints ?? []); - setRelays(wallet.relays ?? []); + try { + setMints(wallet.mints ?? []); + setInitialMints(wallet.mints ?? []); + setRelays(wallet.relays ?? []); + } catch (error) { + console.error('Error setting wallet data:', error); + setHasError(true); + toast.error(intl.formatMessage(messages.loadingError)); + } finally { + setIsLoading(false); + } + } else { + // Handle the case when wallet is null or undefined + setIsLoading(false); + if (wallet === undefined) { // wallet is still loading + // Keep loading state true + setIsLoading(true); + } else if (wallet === null) { // wallet failed to load + setHasError(true); + toast.error(intl.formatMessage(messages.loadingError)); + } } - }, [wallet], + }, [wallet, intl], ); return ( - - - - - + {isLoading ? ( + + + + ) : hasError ? ( + + {intl.formatMessage(messages.loadingError)} + + ) : ( + + + + + )} ); }; diff --git a/src/features/wallet/components/wallet-relays.tsx b/src/features/wallet/components/wallet-relays.tsx index 74c4bcf79..d6c0ab18f 100644 --- a/src/features/wallet/components/wallet-relays.tsx +++ b/src/features/wallet/components/wallet-relays.tsx @@ -3,7 +3,9 @@ import { defineMessages, useIntl } from 'react-intl'; import Button from 'soapbox/components/ui/button.tsx'; import { Column } from 'soapbox/components/ui/column.tsx'; +import Spinner from 'soapbox/components/ui/spinner.tsx'; import Stack from 'soapbox/components/ui/stack.tsx'; +import Text from 'soapbox/components/ui/text.tsx'; import { RelayEditor } from 'soapbox/features/wallet/components/editable-lists.tsx'; import { useWallet } from 'soapbox/features/zap/hooks/useHooks.ts'; import { useApi } from 'soapbox/hooks/useApi.ts'; @@ -13,10 +15,10 @@ import { isURL } from 'soapbox/utils/auth.ts'; const messages = defineMessages({ title: { id: 'wallet.relays', defaultMessage: 'Wallet Relays' }, loadingError: { id: 'wallet.loading_error', defaultMessage: 'An unexpected error occurred while loading your wallet data.' }, - error: { id: 'wallet.relays.error', defaultMessage: 'Failed to update mints.' }, + error: { id: 'wallet.relays.error', defaultMessage: 'Failed to update relays.' }, empty: { id: 'wallet.relays.empty', defaultMessage: 'At least one relay is required.' }, url: { id: 'wallet.invalid_url', defaultMessage: 'All strings must be valid URLs.' }, - sucess: { id: 'wallet.relays.sucess', defaultMessage: 'Relays updated with success!' }, + success: { id: 'wallet.relays.success', defaultMessage: 'Relays updated with success!' }, send: { id: 'common.send', defaultMessage: 'Send' }, }); @@ -28,6 +30,8 @@ const WalletRelays = () => { const [relays, setRelays] = useState([]); const [initialRelays, setInitialRelays] = useState([]); const [mints, setMints] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [hasError, setHasError] = useState(false); const handleClick = async () =>{ if (relays.length === 0) { @@ -47,7 +51,7 @@ const WalletRelays = () => { try { await api.put('/api/v1/ditto/cashu/wallet', { mints: mints, relays: relays }); setInitialRelays(relays); - toast.success(intl.formatMessage(messages.sucess)); + toast.success(intl.formatMessage(messages.success)); } catch (error) { const errorMessage = error instanceof Error ? error.message : intl.formatMessage(messages.error); toast.error(errorMessage); @@ -57,23 +61,51 @@ const WalletRelays = () => { useEffect( () => { + setIsLoading(true); + setHasError(false); + if (wallet) { - setMints(wallet.mints ?? []); - setInitialRelays(wallet.relays ?? []); - setRelays(wallet.relays ?? []); + try { + setMints(wallet.mints ?? []); + setInitialRelays(wallet.relays ?? []); + setRelays(wallet.relays ?? []); + } catch (error) { + console.error('Error setting wallet data:', error); + setHasError(true); + toast.error(intl.formatMessage(messages.loadingError)); + } finally { + setIsLoading(false); + } + } else { + setIsLoading(false); + if (wallet === undefined) { + setIsLoading(true); + } else if (wallet === null) { + setHasError(true); + toast.error(intl.formatMessage(messages.loadingError)); + } } - }, [wallet], + }, [wallet, intl], ); return ( - - - - - + {isLoading ? ( + + + + ) : hasError ? ( + + {intl.formatMessage(messages.loadingError)} + + ) : ( + + + + + )} ); }; diff --git a/src/features/zap/components/pay-request-form.tsx b/src/features/zap/components/pay-request-form.tsx index 66d47faaa..815f451ac 100644 --- a/src/features/zap/components/pay-request-form.tsx +++ b/src/features/zap/components/pay-request-form.tsx @@ -29,6 +29,7 @@ import { emojifyText } from 'soapbox/utils/emojify.tsx'; import PaymentButton from './zap-button/payment-button.tsx'; import type { Account as AccountEntity, Status as StatusEntity } from 'soapbox/types/entities.ts'; +import HStack from 'soapbox/components/ui/hstack.tsx'; const ZAP_PRESETS = [ { buttonAmount: 50, icon: coinIcon }, @@ -44,8 +45,6 @@ interface IPayRequestForm { onClose?(): void; } -const closeIcon = xIcon; - const messages = defineMessages({ zap_button_rounded: { id: 'zap.button.text.rounded', defaultMessage: 'Zap {amount}K sats' }, zap_button: { id: 'payment_method.button.text.raw', defaultMessage: 'Zap {amount} sats' }, @@ -60,7 +59,7 @@ const PayRequestForm = ({ account, status, onClose }: IPayRequestForm) => { const [amount, setAmount] = useState(50); const { zapArrays, zapSplitData, receiveAmount } = useZapSplit(status, account); const splitValues = zapSplitData.splitValues; - const { method: paymentMethod } = usePaymentMethod(); + const { method: paymentMethod, changeMethod } = usePaymentMethod(); const isCashu = paymentMethod === 'cashu'; const hasZapSplit = zapArrays.length > 0 && !isCashu; const { zapCashuRequest } = useZapCashuRequest(); @@ -118,20 +117,60 @@ const PayRequestForm = ({ account, status, onClose }: IPayRequestForm) => { return ( - - + + +
+ + +
+ + +
+ {zapArrays.length > 0 && isCashu && ( + + + + )}
diff --git a/src/features/zap/hooks/useHooks.ts b/src/features/zap/hooks/useHooks.ts index d9dcdbfd4..d264ae21e 100644 --- a/src/features/zap/hooks/useHooks.ts +++ b/src/features/zap/hooks/useHooks.ts @@ -164,7 +164,7 @@ const useTransactions = () => { const newTransactions = [...(transactions ?? []), ...normalizedData ]; setTransactions(newTransactions, prev, next); - return true; // Return true to indicate successful expansion + return true; } catch (err) { const messageError = err instanceof Error ? err.message : 'Error expanding transactions'; toast.error(messageError); @@ -197,26 +197,24 @@ const useZapCashuRequest = () => { setIsLoading(true); setError(null); try { - const response = await api.post('/api/v1/ditto/cashu/nutzap', { + await api.post('/api/v1/ditto/cashu/nutzap', { amount, comment, account_id: account.id, status_id: status?.id, }); - const data = await response.json(); - if (status) { addZapCashu(status.id); } - toast.success(data.message || 'Zap sent successfully!'); + toast.success('Sats sent successfully!'); getWallet(); getTransactions(); } catch (err) { const messageError = err instanceof Error ? err.message : 'An unexpected error occurred'; setError(messageError); - toast.error(messageError); + toast.error('An unexpected error occurred'); } finally { setIsLoading(false); } diff --git a/src/locales/en.json b/src/locales/en.json index a686ec86d..090d4cee8 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1296,6 +1296,7 @@ "patron.donate": "Donate", "patron.title": "Funding Goal", "payment_method.button.text.raw": "Zap {amount} sats", + "payment_method.cashu.split_disabled": "Switch to ⚡ for splits", "payment_method.comment_input.placeholder": "Optional comment", "payment_method.send_to": "Send sats via {method} to {target}", "payment_method.split_message.deducted": "{amountDeducted} sats will deducted*", @@ -1730,12 +1731,12 @@ "wallet.mints": "Mints", "wallet.mints.empty": "At least one mint is required.", "wallet.mints.error": "Failed to update mints.", - "wallet.mints.sucess": "Mints updated with success!", + "wallet.mints.success": "Mints updated with success!", "wallet.payment": "Payment Method", "wallet.relays": "Wallet Relays", "wallet.relays.empty": "At least one relay is required.", - "wallet.relays.error": "Failed to update mints.", - "wallet.relays.sucess": "Relays updated with success!", + "wallet.relays.error": "Failed to update relays.", + "wallet.relays.success": "Relays updated with success!", "wallet.sats": "{amount} sats", "wallet.title": "Wallet", "wallet.transactions": "Transactions", From 1ea2ceb048fcabd67804169cfcf0f0d7d6636e7b Mon Sep 17 00:00:00 2001 From: danidfra Date: Tue, 1 Apr 2025 22:59:52 -0300 Subject: [PATCH 60/62] Fix linting issues in wallet and payment components --- src/features/ui/components/pocket-wallet.tsx | 2 +- .../wallet/components/wallet-mints.tsx | 42 ++++++++------ .../wallet/components/wallet-relays.tsx | 42 ++++++++------ .../zap/components/pay-request-form.tsx | 58 ++++++++++--------- 4 files changed, 81 insertions(+), 63 deletions(-) diff --git a/src/features/ui/components/pocket-wallet.tsx b/src/features/ui/components/pocket-wallet.tsx index 0398f5eaa..b7dd6485c 100644 --- a/src/features/ui/components/pocket-wallet.tsx +++ b/src/features/ui/components/pocket-wallet.tsx @@ -47,7 +47,7 @@ const PocketWallet = () => { {!expanded && <> - { eyeClosed ? •••••• : + { eyeClosed ? {intl.formatMessage({ id: 'wallet.hidden.balance', defaultMessage: '••••••' })} : {intl.formatMessage(messages.balance, { amount: wallet?.balance })} } diff --git a/src/features/wallet/components/wallet-mints.tsx b/src/features/wallet/components/wallet-mints.tsx index 941607be2..0ffabf0b2 100644 --- a/src/features/wallet/components/wallet-mints.tsx +++ b/src/features/wallet/components/wallet-mints.tsx @@ -64,7 +64,7 @@ const WalletMints = () => { () => { setIsLoading(true); setHasError(false); - + if (wallet) { try { setMints(wallet.mints ?? []); @@ -93,22 +93,30 @@ const WalletMints = () => { return ( - {isLoading ? ( - - - - ) : hasError ? ( - - {intl.formatMessage(messages.loadingError)} - - ) : ( - - - - - )} + {(() => { + if (isLoading) { + return ( + + + + ); + } else if (hasError) { + return ( + + {intl.formatMessage(messages.loadingError)} + + ); + } else { + return ( + + + + + ); + } + })()} ); }; diff --git a/src/features/wallet/components/wallet-relays.tsx b/src/features/wallet/components/wallet-relays.tsx index d6c0ab18f..2b6b1e4a5 100644 --- a/src/features/wallet/components/wallet-relays.tsx +++ b/src/features/wallet/components/wallet-relays.tsx @@ -63,7 +63,7 @@ const WalletRelays = () => { () => { setIsLoading(true); setHasError(false); - + if (wallet) { try { setMints(wallet.mints ?? []); @@ -90,22 +90,30 @@ const WalletRelays = () => { return ( - {isLoading ? ( - - - - ) : hasError ? ( - - {intl.formatMessage(messages.loadingError)} - - ) : ( - - - - - )} + {(() => { + if (isLoading) { + return ( + + + + ); + } else if (hasError) { + return ( + + {intl.formatMessage(messages.loadingError)} + + ); + } else { + return ( + + + + + ); + } + })()} ); }; diff --git a/src/features/zap/components/pay-request-form.tsx b/src/features/zap/components/pay-request-form.tsx index 815f451ac..bf0d5c4a9 100644 --- a/src/features/zap/components/pay-request-form.tsx +++ b/src/features/zap/components/pay-request-form.tsx @@ -16,6 +16,7 @@ import pileCoin from 'soapbox/assets/icons/pile-coin.png'; import DisplayNameInline from 'soapbox/components/display-name-inline.tsx'; import Avatar from 'soapbox/components/ui/avatar.tsx'; import Button from 'soapbox/components/ui/button.tsx'; +import HStack from 'soapbox/components/ui/hstack.tsx'; import IconButton from 'soapbox/components/ui/icon-button.tsx'; import Input from 'soapbox/components/ui/input.tsx'; import Stack from 'soapbox/components/ui/stack.tsx'; @@ -29,7 +30,6 @@ import { emojifyText } from 'soapbox/utils/emojify.tsx'; import PaymentButton from './zap-button/payment-button.tsx'; import type { Account as AccountEntity, Status as StatusEntity } from 'soapbox/types/entities.ts'; -import HStack from 'soapbox/components/ui/hstack.tsx'; const ZAP_PRESETS = [ { buttonAmount: 50, icon: coinIcon }, @@ -118,56 +118,56 @@ const PayRequestForm = ({ account, status, onClose }: IPayRequestForm) => { return ( - -
+ +
- + -
+ {zapArrays.length > 0 && isCashu && ( - - + )} @@ -194,7 +194,9 @@ const PayRequestForm = ({ account, status, onClose }: IPayRequestForm) => { className='max-w-20 rounded-none border-0 border-b-4 p-0 text-center !text-2xl font-bold shadow-none !ring-0 dark:bg-transparent sm:max-w-28 sm:p-0.5 sm:!text-4xl' /> {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} - {hasZapSplit &&

sats

} + {hasZapSplit &&

+ {intl.formatMessage({ id: 'payment_method.unit', defaultMessage: 'sats' })} +

}
{hasZapSplit && ( @@ -215,7 +217,7 @@ const PayRequestForm = ({ account, status, onClose }: IPayRequestForm) => { {hasZapSplit ? - + + )} + {!isLoading && !error && ( diff --git a/src/locales/en.json b/src/locales/en.json index 090d4cee8..16878fc5c 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -714,6 +714,8 @@ "edit_profile.success": "Your profile has been successfully saved!", "email_confirmation.success": "Your email has been confirmed!", "embed.instructions": "Embed this post on your website by copying the code below.", + "emoji.cashu": "Cashu", + "emoji.lightning": "Lightning", "emoji_button.activity": "Activity", "emoji_button.add_custom": "Add custom emoji", "emoji_button.custom": "Custom", @@ -1296,10 +1298,14 @@ "patron.donate": "Donate", "patron.title": "Funding Goal", "payment_method.button.text.raw": "Zap {amount} sats", + "payment_method.button.zap_sats": "Zap sats", + "payment_method.cashu": "Cashu", "payment_method.cashu.split_disabled": "Switch to ⚡ for splits", "payment_method.comment_input.placeholder": "Optional comment", + "payment_method.lightning": "Lightning", "payment_method.send_to": "Send sats via {method} to {target}", "payment_method.split_message.deducted": "{amountDeducted} sats will deducted*", + "payment_method.unit": "sats", "pinned_accounts.title": "{name}’s choices", "pinned_statuses.none": "No pins to show.", "poll.choose_multiple": "Choose as many as you'd like.", @@ -1724,6 +1730,7 @@ "wallet.button.withdraw": "Withdraw", "wallet.create_wallet.question": "Do you want create one?", "wallet.create_wallet.title": "You don't have a wallet", + "wallet.hidden.balance": "••••••", "wallet.invalid_url": "All strings must be valid URLs.", "wallet.loading": "Loading…", "wallet.loading_error": "An unexpected error occurred while loading your wallet data.", @@ -1737,6 +1744,8 @@ "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.retrying": "Retrying in {seconds}s…", "wallet.sats": "{amount} sats", "wallet.title": "Wallet", "wallet.transactions": "Transactions", From b547283707bfc5ef067b197587141cf8be14b241 Mon Sep 17 00:00:00 2001 From: danidfra Date: Thu, 3 Apr 2025 17:22:47 -0300 Subject: [PATCH 62/62] Wrong quote --- src/features/zap/components/pay-request-form.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/zap/components/pay-request-form.tsx b/src/features/zap/components/pay-request-form.tsx index bf0d5c4a9..cddaa1626 100644 --- a/src/features/zap/components/pay-request-form.tsx +++ b/src/features/zap/components/pay-request-form.tsx @@ -130,7 +130,7 @@ const PayRequestForm = ({ account, status, onClose }: IPayRequestForm) => { onClick={() => changeMethod('lightning')} title={intl.formatMessage({ id: 'payment_method.lightning', defaultMessage: 'Lightning' })} > - + {/* eslint-disable-line formatjs/no-literal-string-in-jsx */}