diff --git a/app/features/receive/receive-input.tsx b/app/features/receive/receive-input.tsx index 82145c390..def83c7d1 100644 --- a/app/features/receive/receive-input.tsx +++ b/app/features/receive/receive-input.tsx @@ -169,7 +169,9 @@ export default function ReceiveInput() { diff --git a/app/features/receive/receive-scanner.tsx b/app/features/receive/receive-scanner.tsx deleted file mode 100644 index ed067536d..000000000 --- a/app/features/receive/receive-scanner.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { - ClosePageButton, - PageContent, - PageHeader, - PageHeaderTitle, -} from '~/components/page'; -import { QRScanner } from '~/components/qr-scanner'; -import { useBuildLinkWithSearchParams } from '~/hooks/use-search-params-link'; -import { useToast } from '~/hooks/use-toast'; -import { extractCashuToken } from '~/lib/cashu'; -import { useNavigateWithViewTransition } from '~/lib/transitions'; -import { useReceiveStore } from './receive-provider'; - -export default function ReceiveScanner() { - const { toast } = useToast(); - const navigate = useNavigateWithViewTransition(); - const buildLinkWithSearchParams = useBuildLinkWithSearchParams(); - const receiveAccountId = useReceiveStore((s) => s.accountId); - - return ( - <> - - - Scan - - - { - const encodedToken = extractCashuToken(scannedContent)?.encoded; - if (!encodedToken) { - toast({ - title: 'Invalid input', - description: 'Please scan a valid cashu token', - variant: 'destructive', - }); - return; - } - - const hash = `#${encodedToken}`; - - // The hash needs to be set manually before navigating or clientLoader of the destination route won't see it - // See https://github.com/remix-run/remix/discussions/10721 - window.history.replaceState(null, '', hash); - navigate( - { - ...buildLinkWithSearchParams('/receive/cashu/token', { - selectedAccountId: receiveAccountId, - }), - hash, - }, - { transition: 'slideLeft', applyTo: 'newView' }, - ); - }} - /> - - - ); -} diff --git a/app/features/send/resolve-destination.ts b/app/features/send/resolve-destination.ts index 2eef18fa6..5d6a2450c 100644 --- a/app/features/send/resolve-destination.ts +++ b/app/features/send/resolve-destination.ts @@ -1,6 +1,5 @@ import { type DecodedBolt11, parseBolt11Invoice } from '~/lib/bolt11'; import { parseCashuPaymentRequest } from '~/lib/cashu'; -import { isValidLightningAddress } from '~/lib/lnurl'; import type { Money } from '~/lib/money'; import { type Contact, isContact } from '../contacts/contact'; import { validateBolt11, validateLightningAddressFormat } from './validation'; @@ -33,10 +32,10 @@ type ResolveResult = | { success: true; data: SendDestination } | { success: false; error: string }; -export async function resolveSendDestination( +export function resolveSendDestination( input: string | Contact, { allowZeroAmountBolt11 = false }: { allowZeroAmountBolt11?: boolean } = {}, -): Promise { +): ResolveResult { if (isContact(input)) { return { success: true, @@ -50,10 +49,6 @@ export async function resolveSendDestination( } if (validateLightningAddressFormat(input) === true) { - const isValid = await isValidLightningAddress(input); - if (!isValid) { - return { success: false, error: 'Invalid lightning address' }; - } return { success: true, data: { diff --git a/app/features/send/send-input.tsx b/app/features/send/send-input.tsx index fbd6c280c..94574396c 100644 --- a/app/features/send/send-input.tsx +++ b/app/features/send/send-input.tsx @@ -137,7 +137,7 @@ export function SendInput() { }; const handleSelectDestination = async (destination: string | Contact) => { - const result = await selectDestination(destination); + const result = selectDestination(destination); if (!result.success) { toast({ title: 'Invalid destination', @@ -241,7 +241,9 @@ export function SendInput() { diff --git a/app/features/send/send-scanner.tsx b/app/features/send/send-scanner.tsx deleted file mode 100644 index cd3c1f77d..000000000 --- a/app/features/send/send-scanner.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { - ClosePageButton, - PageContent, - PageHeader, - PageHeaderTitle, -} from '~/components/page'; -import { QRScanner } from '~/components/qr-scanner'; -import { useExchangeRate } from '~/hooks/use-exchange-rate'; -import { useBuildLinkWithSearchParams } from '~/hooks/use-search-params-link'; -import { useToast } from '~/hooks/use-toast'; -import type { Money } from '~/lib/money'; -import { useNavigateWithViewTransition } from '~/lib/transitions/view-transition'; -import type { Account } from '../accounts/account'; -import { DomainError, getErrorMessage } from '../shared/error'; -import { useSendStore } from './send-provider'; - -/** - * Converts an amount to the send account currency. - * If the amount is already in the send account currency, returns the amount. - * - * @throws if the exchange rate fails to load - */ -const useConverter = (sendAccount: Account) => { - const otherCurrency = sendAccount.currency === 'BTC' ? 'USD' : 'BTC'; - - const { data: rate } = useExchangeRate( - `${otherCurrency}-${sendAccount.currency}`, - ); - - return (amount: Money) => { - if (!rate) throw new Error('Exchange rate not found'); - - return amount.convert(sendAccount.currency, rate); - }; -}; - -export default function SendScanner() { - const { toast } = useToast(); - const navigate = useNavigateWithViewTransition(); - const buildLinkWithSearchParams = useBuildLinkWithSearchParams(); - - const sendAccount = useSendStore((state) => state.getSourceAccount()); - const selectDestination = useSendStore((state) => state.selectDestination); - const continueSend = useSendStore((state) => state.proceedWithSend); - - const convert = useConverter(sendAccount); - - const handleDecode = async (input: string) => { - const selectDestinationResult = await selectDestination(input); - if (!selectDestinationResult.success) { - toast({ - title: 'Invalid input', - description: selectDestinationResult.error, - variant: 'destructive', - }); - return; - } - - const { amount } = selectDestinationResult.data; - - if (!amount) { - // Navigate to send input to enter the amount - return navigate(buildLinkWithSearchParams('/send'), { - applyTo: 'oldView', - transition: 'slideDown', - }); - } - - const convertedAmount = - amount.currency !== sendAccount.currency ? convert(amount) : undefined; - const result = await continueSend(amount, convertedAmount); - - if (!result.success) { - const toastOptions = - result.error instanceof DomainError - ? { description: result.error.message } - : { - title: 'Error', - description: getErrorMessage( - result.error, - 'Failed to get a send quote. Please try again', - ), - variant: 'destructive' as const, - }; - - toast(toastOptions); - return; - } - - if (result.next !== 'confirmQuote') { - return; - } - - navigate(buildLinkWithSearchParams('/send/confirm'), { - applyTo: 'newView', - transition: 'slideUp', - }); - }; - - return ( - <> - - - Scan - - - - - - ); -} diff --git a/app/features/send/send-store.ts b/app/features/send/send-store.ts index 5a63b0878..031f46480 100644 --- a/app/features/send/send-store.ts +++ b/app/features/send/send-store.ts @@ -124,10 +124,9 @@ type Actions = { getSourceAccount: () => Account; selectDestination: ( destination: string | Contact, - ) => Promise< + ) => | { success: true; data: DecodedDestination } - | { success: false; error: string } - >; + | { success: false; error: string }; clearDestination: () => void; hasRequiredDestination: () => boolean; proceedWithSend: ( @@ -256,9 +255,9 @@ export const createSendStore = ({ } as Partial); }, - selectDestination: async (input) => { + selectDestination: (input) => { const account = get().getSourceAccount(); - const result = await resolveSendDestination(input, { + const result = resolveSendDestination(input, { allowZeroAmountBolt11: account.type === 'spark', }); if (!result.success) { @@ -282,6 +281,9 @@ export const createSendStore = ({ : null; const accountId = matched?.id ?? account.id; + const amount = + result.data.sendType === 'BOLT11_INVOICE' ? result.data.amount : null; + set({ accountId, sendType, @@ -294,10 +296,7 @@ export const createSendStore = ({ success: true, data: { type: result.data.sendType, - amount: - result.data.sendType === 'BOLT11_INVOICE' - ? result.data.amount - : null, + amount, }, }; }, diff --git a/app/routes/_protected.receive.scan.tsx b/app/routes/_protected.receive.scan.tsx deleted file mode 100644 index 5f61a35d3..000000000 --- a/app/routes/_protected.receive.scan.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { Page } from '~/components/page'; -import ReceiveScanner from '~/features/receive/receive-scanner'; - -export default function ReceiveScan() { - return ( - - - - ); -} diff --git a/app/routes/_protected.scan.tsx b/app/routes/_protected.scan.tsx index 5d55eda8f..37f2ef304 100644 --- a/app/routes/_protected.scan.tsx +++ b/app/routes/_protected.scan.tsx @@ -11,6 +11,7 @@ import { Button } from '~/components/ui/button'; import { classifyInput } from '~/features/scan'; import { validateBolt11 } from '~/features/send/validation'; import useIsPwa from '~/hooks/use-is-pwa'; +import { useBuildLinkWithSearchParams } from '~/hooks/use-search-params-link'; import { useToast } from '~/hooks/use-toast'; import { readClipboard } from '~/lib/read-clipboard'; import { useNavigateWithViewTransition } from '~/lib/transitions'; @@ -20,6 +21,7 @@ export default function ScanPage() { const navigate = useNavigateWithViewTransition(); const { toast } = useToast(); const isPwa = useIsPwa(); + const buildLinkWithSearchParams = useBuildLinkWithSearchParams(); const handleInput = (raw: string) => { const result = classifyInput(raw); @@ -40,8 +42,8 @@ export default function ScanPage() { // See https://github.com/remix-run/remix/discussions/10721 window.history.replaceState(null, '', hash); navigate( - { pathname: '/receive/cashu/token', hash }, - { transition: 'slideLeft', applyTo: 'newView' }, + { ...buildLinkWithSearchParams('/receive/cashu/token'), hash }, + { transition: 'slideDown', applyTo: 'oldView' }, ); return; } @@ -65,8 +67,8 @@ export default function ScanPage() { // See https://github.com/remix-run/remix/discussions/10721 window.history.replaceState(null, '', hash); navigate( - { pathname: '/send', hash }, - { transition: 'slideLeft', applyTo: 'newView' }, + { ...buildLinkWithSearchParams('/send'), hash }, + { transition: 'slideDown', applyTo: 'oldView' }, ); }; diff --git a/app/routes/_protected.send.scan.tsx b/app/routes/_protected.send.scan.tsx deleted file mode 100644 index 16c8f1399..000000000 --- a/app/routes/_protected.send.scan.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { Page } from '~/components/page'; -import SendScanner from '~/features/send/send-scanner'; - -export default function SendScanPage() { - return ( - - - - ); -} diff --git a/app/routes/_protected.send.tsx b/app/routes/_protected.send.tsx index e633e655a..e00c22790 100644 --- a/app/routes/_protected.send.tsx +++ b/app/routes/_protected.send.tsx @@ -14,10 +14,10 @@ import { getQueryClient } from '~/features/shared/query-client'; import { toast } from '~/hooks/use-toast'; import type { Route } from './+types/_protected.send'; -export async function clientLoader(): Promise<{ +export function clientLoader(): { initialDestination: SendDestination | null; initialAccountId: string | null; -}> { +} { const hash = window.location.hash.slice(1); if (!hash) { return { initialDestination: null, initialAccountId: null }; @@ -31,7 +31,7 @@ export async function clientLoader(): Promise<{ window.location.pathname + window.location.search, ); - const result = await resolveSendDestination(hash, { + const result = resolveSendDestination(hash, { allowZeroAmountBolt11: true, });