From 342267ea6070163f509b3e27cd039fea3cd331f9 Mon Sep 17 00:00:00 2001 From: gudnuf Date: Wed, 4 Mar 2026 15:43:18 -0800 Subject: [PATCH] Buy bitcoin with Cash App --- app/components/ui/button.tsx | 2 +- app/features/buy/buy-checkout.tsx | 264 ++++++++++++++++++ app/features/buy/buy-faq-drawer.tsx | 71 +++++ app/features/buy/buy-input.tsx | 179 ++++++++++++ app/features/buy/buy-provider.tsx | 44 +++ app/features/buy/buy-store.ts | 105 +++++++ app/features/buy/cash-app.tsx | 17 ++ app/features/buy/index.ts | 3 + app/features/receive/receive-input.tsx | 39 +-- .../receive/spark-receive-quote-hooks.ts | 6 + app/features/send/send-input.tsx | 45 +-- app/features/send/send-store.ts | 4 +- .../shared/converted-money-switcher.tsx | 35 +++ app/routes/_protected._index.tsx | 29 +- app/routes/_protected.buy._index.tsx | 10 + app/routes/_protected.buy.checkout.tsx | 34 +++ app/routes/_protected.buy.tsx | 21 ++ 17 files changed, 823 insertions(+), 85 deletions(-) create mode 100644 app/features/buy/buy-checkout.tsx create mode 100644 app/features/buy/buy-faq-drawer.tsx create mode 100644 app/features/buy/buy-input.tsx create mode 100644 app/features/buy/buy-provider.tsx create mode 100644 app/features/buy/buy-store.ts create mode 100644 app/features/buy/cash-app.tsx create mode 100644 app/features/buy/index.ts create mode 100644 app/features/shared/converted-money-switcher.tsx create mode 100644 app/routes/_protected.buy._index.tsx create mode 100644 app/routes/_protected.buy.checkout.tsx create mode 100644 app/routes/_protected.buy.tsx diff --git a/app/components/ui/button.tsx b/app/components/ui/button.tsx index 9675a1d6..cdfcec9b 100644 --- a/app/components/ui/button.tsx +++ b/app/components/ui/button.tsx @@ -16,7 +16,7 @@ const buttonVariants = cva( outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', secondary: - 'bg-secondary text-secondary-foreground hover:bg-secondary/80', + 'border border-border bg-card text-card-foreground hover:bg-card/80', ghost: 'hover:bg-accent hover:text-accent-foreground', link: 'text-primary underline-offset-4 hover:underline', }, diff --git a/app/features/buy/buy-checkout.tsx b/app/features/buy/buy-checkout.tsx new file mode 100644 index 00000000..6de32393 --- /dev/null +++ b/app/features/buy/buy-checkout.tsx @@ -0,0 +1,264 @@ +import { AlertCircle } from 'lucide-react'; +import { MoneyDisplay } from '~/components/money-display'; +import { + PageBackButton, + PageContent, + PageFooter, + PageHeader, + PageHeaderTitle, +} from '~/components/page'; +import { QRCode } from '~/components/qr-code'; +import { Button } from '~/components/ui/button'; +import { Card, CardContent } from '~/components/ui/card'; +import { useBuildLinkWithSearchParams } from '~/hooks/use-search-params-link'; +import useUserAgent from '~/hooks/use-user-agent'; +import type { Money } from '~/lib/money'; +import { useNavigateWithViewTransition } from '~/lib/transitions'; +import { useCashuReceiveQuote } from '../receive/cashu-receive-quote-hooks'; +import { useSparkReceiveQuote } from '../receive/spark-receive-quote-hooks'; +import { getDefaultUnit } from '../shared/currencies'; +import { MoneyWithConvertedAmount } from '../shared/money-with-converted-amount'; +import type { BuyQuote } from './buy-store'; +import { buildCashAppDeepLink } from './cash-app'; + +const getErrorMessageFromQuoteStatus = (status: string) => { + if (status === 'EXPIRED') { + return 'This invoice has expired. Please create a new one.'; + } + if (status === 'FAILED') { + return 'Something went wrong. Please try again.'; + } + return undefined; +}; + +const useNavigateToTransaction = () => { + const navigate = useNavigateWithViewTransition(); + const buildLinkWithSearchParams = useBuildLinkWithSearchParams(); + + return (transactionId: string) => { + navigate( + buildLinkWithSearchParams(`/transactions/${transactionId}`, { + showOkButton: 'true', + }), + { transition: 'fade', applyTo: 'newView' }, + ); + }; +}; + +const ConfirmationRow = ({ + label, + value, +}: { label: string; value: React.ReactNode }) => { + return ( +
+

{label}

+
{value}
+
+ ); +}; + +function ErrorCard({ message }: { message: string }) { + return ( + + + +

{message}

+
+
+ ); +} + +function ConfirmationDetails({ + accountName, + fee, +}: { accountName: string; fee?: Money }) { + return ( + + + + + {fee && ( + + } + /> + )} + + + ); +} + +function MobileCheckoutContent({ + errorMessage, + accountName, + fee, +}: { + errorMessage: string | undefined; + accountName: string; + fee?: Money; +}) { + return ( +
+
+ {errorMessage ? ( + + ) : ( + + )} +
+
+ ); +} + +function DesktopCheckoutContent({ + errorMessage, + paymentRequest, + accountName, + fee, +}: { + errorMessage: string | undefined; + paymentRequest: string; + accountName: string; + fee?: Money; +}) { + if (errorMessage) { + return ( +
+ +
+ ); + } + + const deepLinkUrl = buildCashAppDeepLink(paymentRequest); + + return ( + <> + +
+ +
+ + ); +} + +function CashAppCheckout({ + paymentRequest, + amount, + accountName, + errorMessage, + fee, +}: { + paymentRequest: string; + amount: Money; + accountName: string; + errorMessage: string | undefined; + fee?: Money; +}) { + const buildLinkWithSearchParams = useBuildLinkWithSearchParams(); + const { isMobile } = useUserAgent(); + const deepLinkUrl = buildCashAppDeepLink(paymentRequest); + + return ( + <> + + + Buy + + + + {isMobile ? ( + + ) : ( + + )} + + {isMobile && !errorMessage && ( + + + + )} + + ); +} + +export function BuyCheckoutCashu({ + quote, + amount, + accountName, +}: { + quote: BuyQuote; + amount: Money; + accountName: string; +}) { + const navigateToTransaction = useNavigateToTransaction(); + + const { status: quotePaymentStatus } = useCashuReceiveQuote({ + quoteId: quote.id, + onPaid: (cashuQuote) => { + navigateToTransaction(cashuQuote.transactionId); + }, + }); + + return ( + + ); +} + +export function BuyCheckoutSpark({ + quote, + amount, + accountName, +}: { + quote: BuyQuote; + amount: Money; + accountName: string; +}) { + const navigateToTransaction = useNavigateToTransaction(); + + const { status: quotePaymentStatus } = useSparkReceiveQuote({ + quoteId: quote.id, + onPaid: (sparkQuote) => { + navigateToTransaction(sparkQuote.transactionId); + }, + }); + + return ( + + ); +} diff --git a/app/features/buy/buy-faq-drawer.tsx b/app/features/buy/buy-faq-drawer.tsx new file mode 100644 index 00000000..a896a355 --- /dev/null +++ b/app/features/buy/buy-faq-drawer.tsx @@ -0,0 +1,71 @@ +import { Info } from 'lucide-react'; +import { + Drawer, + DrawerContent, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from '~/components/ui/drawer'; + +const faqItems = [ + { + question: 'Why Cash App?', + answer: + 'Cash App is the first supported payment method because it natively supports Bitcoin Lightning payments.', + }, + { + question: "What if I don't have Cash App?", + answer: + "Don't worry, we're launching more payment options soon. In the meantime, you can receive bitcoin from any Bitcoin Lightning wallet by tapping the Receive button.", + }, + { + question: "Why isn't my Cash App loading?", + answer: "Make sure you've downloaded the latest version.", + }, + { + question: 'What are the fees?', + answer: + 'None. Agicash charges zero fees. Cash App charges zero fees. Your transaction executes at the mid-market rate, making this the cheapest way to buy bitcoin.', + }, + { + question: 'Is there a purchase limit?', + answer: + 'Cash App has a $999/week limit on Lightning payments. This is a Cash App limit, not an Agicash limit.', + }, + { + question: 'How fast is it?', + answer: + 'Instant. Your purchase and settlement happen in seconds over the Bitcoin Lightning Network.', + }, +]; + +export function BuyFaqDrawer() { + return ( + + + + + +
+ + Frequently Asked Questions + +
+
+ {faqItems.map((item) => ( +
+

{item.question}

+

+ {item.answer} +

+
+ ))} +
+
+
+
+
+ ); +} diff --git a/app/features/buy/buy-input.tsx b/app/features/buy/buy-input.tsx new file mode 100644 index 00000000..fe7fb36c --- /dev/null +++ b/app/features/buy/buy-input.tsx @@ -0,0 +1,179 @@ +import { MoneyInputDisplay } from '~/components/money-display'; +import { Numpad } from '~/components/numpad'; +import { + ClosePageButton, + PageContent, + PageFooter, + PageHeader, + PageHeaderItem, + PageHeaderTitle, +} from '~/components/page'; +import { Button } from '~/components/ui/button'; +import { + AccountSelector, + toAccountSelectorOption, +} from '~/features/accounts/account-selector'; +import { accountOfflineToast } from '~/features/accounts/utils'; +import { ConvertedMoneySwitcher } from '~/features/shared/converted-money-switcher'; +import { getDefaultUnit } from '~/features/shared/currencies'; +import { DomainError } from '~/features/shared/error'; +import useAnimation from '~/hooks/use-animation'; +import { useMoneyInput } from '~/hooks/use-money-input'; +import { useRedirectTo } from '~/hooks/use-redirect-to'; +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'; +import { useAccount, useAccounts } from '../accounts/account-hooks'; +import { BuyFaqDrawer } from './buy-faq-drawer'; +import { useBuyStore } from './buy-provider'; +import { CashAppLogo } from './cash-app'; + +export default function BuyInput() { + const navigate = useNavigateWithViewTransition(); + const buildLinkWithSearchParams = useBuildLinkWithSearchParams(); + const { toast } = useToast(); + const { redirectTo } = useRedirectTo('/'); + const { animationClass: shakeAnimationClass, start: startShakeAnimation } = + useAnimation({ name: 'shake' }); + + const buyAccountId = useBuyStore((s) => s.accountId); + const buyAccount = useAccount(buyAccountId); + const buyAmount = useBuyStore((s) => s.amount); + const buyCurrencyUnit = getDefaultUnit(buyAccount.currency); + const setBuyAccount = useBuyStore((s) => s.setAccount); + const getBuyQuote = useBuyStore((s) => s.getBuyQuote); + const status = useBuyStore((s) => s.status); + const { data: accounts } = useAccounts(); + + const { + rawInputValue, + maxInputDecimals, + inputValue, + convertedValue, + exchangeRateError, + handleNumberInput, + switchInputCurrency, + } = useMoneyInput({ + initialRawInputValue: buyAmount?.toString(buyCurrencyUnit) || '0', + initialInputCurrency: buyAccount.currency, + initialOtherCurrency: buyAccount.currency === 'BTC' ? 'USD' : 'BTC', + }); + + const handleContinue = async () => { + if (!buyAccount.isOnline) { + toast(accountOfflineToast); + return; + } + + let amount: Money; + if (inputValue.currency === buyAccount.currency) { + amount = inputValue; + } else { + if (!convertedValue) { + return; + } + amount = convertedValue; + } + + const result = await getBuyQuote(amount); + + if (!result.success) { + if (result.error instanceof DomainError) { + toast({ description: result.error.message }); + } else { + console.error('Failed to create invoice', { cause: result.error }); + toast({ + description: 'Failed to create invoice. Please try again.', + variant: 'destructive', + }); + } + return; + } + + navigate(buildLinkWithSearchParams('/buy/checkout'), { + transition: 'slideLeft', + applyTo: 'newView', + }); + }; + + return ( + <> + + + Buy + + + + + + +
+
+ +
+ + {!exchangeRateError && ( + + )} +
+ +
+ + toAccountSelectorOption(account), + )} + selectedAccount={toAccountSelectorOption(buyAccount)} + onSelect={(account) => { + setBuyAccount(account); + if (account.currency !== inputValue.currency) { + switchInputCurrency(); + } + }} + disabled={accounts.length === 1} + /> +
+ +
+
+
+ + Pay with + + +
+
+
+ +
+
+
+ + + 0} + onButtonClick={(value) => { + handleNumberInput(value, startShakeAnimation); + }} + /> + + + ); +} diff --git a/app/features/buy/buy-provider.tsx b/app/features/buy/buy-provider.tsx new file mode 100644 index 00000000..b19520db --- /dev/null +++ b/app/features/buy/buy-provider.tsx @@ -0,0 +1,44 @@ +import { + type PropsWithChildren, + createContext, + useContext, + useState, +} from 'react'; +import { useStore } from 'zustand'; +import type { Account } from '../accounts/account'; +import { useGetAccount } from '../accounts/account-hooks'; +import { useCreateCashuReceiveQuote } from '../receive/cashu-receive-quote-hooks'; +import { useCreateSparkReceiveQuote } from '../receive/spark-receive-quote-hooks'; +import type { BuyState, BuyStore } from './buy-store'; +import { createBuyStore } from './buy-store'; + +const BuyContext = createContext(null); + +type Props = PropsWithChildren<{ + initialAccount: Account; +}>; + +export const BuyProvider = ({ children, initialAccount }: Props) => { + const getAccount = useGetAccount(); + const { mutateAsync: createCashuReceiveQuote } = useCreateCashuReceiveQuote(); + const { mutateAsync: createSparkReceiveQuote } = useCreateSparkReceiveQuote(); + + const [store] = useState(() => + createBuyStore({ + initialAccount, + getAccount, + createCashuReceiveQuote, + createSparkReceiveQuote, + }), + ); + + return {children}; +}; + +export const useBuyStore = (selector: (state: BuyState) => T): T => { + const store = useContext(BuyContext); + if (!store) { + throw new Error('Missing BuyProvider in the tree'); + } + return useStore(store, selector); +}; diff --git a/app/features/buy/buy-store.ts b/app/features/buy/buy-store.ts new file mode 100644 index 00000000..4a9ed120 --- /dev/null +++ b/app/features/buy/buy-store.ts @@ -0,0 +1,105 @@ +import { create } from 'zustand'; +import type { Currency, Money } from '~/lib/money'; +import type { Account, CashuAccount, SparkAccount } from '../accounts/account'; +import type { CashuReceiveQuote } from '../receive/cashu-receive-quote'; +import type { SparkReceiveQuote } from '../receive/spark-receive-quote'; + +export type BuyQuote = { + id: string; + paymentRequest: string; + transactionId: string; + mintingFee?: Money; +}; + +type GetBuyQuoteResult = + | { success: true; quote: BuyQuote } + | { success: false; error: unknown }; + +export type BuyState = { + status: 'idle' | 'quoting' | 'success'; + /** The ID of the account to buy into */ + accountId: string; + /** The amount to buy */ + amount: Money | null; + /** The buy quote (Lightning invoice) */ + quote: BuyQuote | null; + /** Set the account to buy into */ + setAccount: (account: Account) => void; + /** Set the amount to buy */ + setAmount: (amount: Money) => void; + /** Create a receive quote for the given amount */ + getBuyQuote: (amount: Money) => Promise; +}; + +type CreateBuyStoreProps = { + initialAccount: Account; + getAccount: (id: string) => Account; + createCashuReceiveQuote: (params: { + account: CashuAccount; + amount: Money; + description?: string; + }) => Promise; + createSparkReceiveQuote: (params: { + account: SparkAccount; + amount: Money; + description?: string; + }) => Promise; +}; + +export const createBuyStore = ({ + initialAccount, + getAccount, + createCashuReceiveQuote, + createSparkReceiveQuote, +}: CreateBuyStoreProps) => { + return create((set, get) => ({ + status: 'idle', + accountId: initialAccount.id, + amount: null, + quote: null, + setAccount: (account) => + set({ accountId: account.id, amount: null, quote: null }), + setAmount: (amount) => set({ amount }), + getBuyQuote: async (amount) => { + const account = getAccount(get().accountId); + set({ status: 'quoting', amount }); + + try { + let quote: BuyQuote; + + if (account.type === 'cashu') { + const cashuQuote = await createCashuReceiveQuote({ + account, + amount, + description: 'Pay to Agicash', + }); + quote = { + id: cashuQuote.id, + paymentRequest: cashuQuote.paymentRequest, + transactionId: cashuQuote.transactionId, + mintingFee: cashuQuote.mintingFee, + }; + } else { + const sparkQuote = await createSparkReceiveQuote({ + account, + amount, + description: 'Pay to Agicash', + }); + quote = { + id: sparkQuote.id, + paymentRequest: sparkQuote.paymentRequest, + transactionId: sparkQuote.transactionId, + }; + } + + set({ status: 'success', quote }); + return { success: true, quote }; + } catch (error) { + set({ status: 'idle' }); + return { success: false, error }; + } + }, + })); +}; + +export type BuyStore = ReturnType; diff --git a/app/features/buy/cash-app.tsx b/app/features/buy/cash-app.tsx new file mode 100644 index 00000000..74686835 --- /dev/null +++ b/app/features/buy/cash-app.tsx @@ -0,0 +1,17 @@ +export function buildCashAppDeepLink(paymentRequest: string) { + return `https://cash.app/launch/lightning/${paymentRequest}`; +} + +export const CASH_APP_LOGO_URL = + 'https://static.afterpaycdn.com/en-US/integration/logo/lockup/cashapp-color-white-32.svg'; + +export function CashAppLogo({ className }: { className?: string }) { + return ( + Cash App + ); +} diff --git a/app/features/buy/index.ts b/app/features/buy/index.ts new file mode 100644 index 00000000..2dd668fd --- /dev/null +++ b/app/features/buy/index.ts @@ -0,0 +1,3 @@ +export { BuyCheckoutCashu, BuyCheckoutSpark } from './buy-checkout'; +export { default as BuyInput } from './buy-input'; +export { BuyProvider } from './buy-provider'; diff --git a/app/features/receive/receive-input.tsx b/app/features/receive/receive-input.tsx index c9e24723..9bbf13bd 100644 --- a/app/features/receive/receive-input.tsx +++ b/app/features/receive/receive-input.tsx @@ -1,6 +1,6 @@ import { getEncodedToken } from '@cashu/cashu-ts'; -import { ArrowUpDown, Clipboard, Scan } from 'lucide-react'; -import { MoneyDisplay, MoneyInputDisplay } from '~/components/money-display'; +import { Clipboard, Scan } from 'lucide-react'; +import { MoneyInputDisplay } from '~/components/money-display'; import { Numpad } from '~/components/numpad'; import { ClosePageButton, @@ -10,12 +10,12 @@ import { PageHeaderTitle, } from '~/components/page'; import { Button } from '~/components/ui/button'; -import { Skeleton } from '~/components/ui/skeleton'; import { AccountSelector, toAccountSelectorOption, } from '~/features/accounts/account-selector'; import { accountOfflineToast } from '~/features/accounts/utils'; +import { ConvertedMoneySwitcher } from '~/features/shared/converted-money-switcher'; import { getDefaultUnit } from '~/features/shared/currencies'; import useAnimation from '~/hooks/use-animation'; import { useMoneyInput } from '~/hooks/use-money-input'; @@ -23,7 +23,6 @@ import { useRedirectTo } from '~/hooks/use-redirect-to'; import { useBuildLinkWithSearchParams } from '~/hooks/use-search-params-link'; import { useToast } from '~/hooks/use-toast'; import { extractCashuToken } from '~/lib/cashu'; -import type { Money } from '~/lib/money'; import { readClipboard } from '~/lib/read-clipboard'; import { LinkWithViewTransition, @@ -32,36 +31,6 @@ import { import { useAccount, useAccounts } from '../accounts/account-hooks'; import { useReceiveStore } from './receive-provider'; -type ConvertedMoneySwitcherProps = { - onSwitchInputCurrency: () => void; - money?: Money; -}; - -const ConvertedMoneySwitcher = ({ - onSwitchInputCurrency, - money, -}: ConvertedMoneySwitcherProps) => { - if (!money) { - return ; - } - - return ( - - ); -}; - export default function ReceiveInput() { const navigate = useNavigateWithViewTransition(); const { toast } = useToast(); @@ -172,7 +141,7 @@ export default function ReceiveInput() { {!exchangeRateError && ( )} diff --git a/app/features/receive/spark-receive-quote-hooks.ts b/app/features/receive/spark-receive-quote-hooks.ts index f4d56a9a..3fc9243d 100644 --- a/app/features/receive/spark-receive-quote-hooks.ts +++ b/app/features/receive/spark-receive-quote-hooks.ts @@ -268,6 +268,10 @@ type CreateProps = { * If not provided, the invoice will be created for the user that owns the Spark wallet. */ receiverIdentityPubkey?: string; + /** + * Description to include in the Lightning invoice memo. + */ + description?: string; }; /** @@ -287,11 +291,13 @@ export function useCreateSparkReceiveQuote() { account, amount, receiverIdentityPubkey, + description, }: CreateProps) => { const lightningQuote = await getLightningQuote({ wallet: account.wallet, amount, receiverIdentityPubkey, + description, }); return sparkReceiveQuoteService.createReceiveQuote({ diff --git a/app/features/send/send-input.tsx b/app/features/send/send-input.tsx index 3fdb2cac..cea77d45 100644 --- a/app/features/send/send-input.tsx +++ b/app/features/send/send-input.tsx @@ -1,5 +1,4 @@ import { - ArrowUpDown, AtSign, Clipboard, LoaderCircle, @@ -8,7 +7,7 @@ import { ZapIcon, } from 'lucide-react'; import { useState } from 'react'; -import { MoneyDisplay, MoneyInputDisplay } from '~/components/money-display'; +import { MoneyInputDisplay } from '~/components/money-display'; import { Numpad } from '~/components/numpad'; import { ClosePageButton, @@ -20,19 +19,19 @@ import { import { SearchBar } from '~/components/search-bar'; import { Button } from '~/components/ui/button'; import { + Drawer, DrawerContent, DrawerHeader, DrawerTitle, + DrawerTrigger, } from '~/components/ui/drawer'; -import { DrawerTrigger } from '~/components/ui/drawer'; -import { Drawer } from '~/components/ui/drawer'; -import { Skeleton } from '~/components/ui/skeleton'; import { useAccounts } from '~/features/accounts/account-hooks'; import { AccountSelector, toAccountSelectorOption, } from '~/features/accounts/account-selector'; import { accountOfflineToast } from '~/features/accounts/utils'; +import { ConvertedMoneySwitcher } from '~/features/shared/converted-money-switcher'; import useAnimation from '~/hooks/use-animation'; import { useMoneyInput } from '~/hooks/use-money-input'; import { useRedirectTo } from '~/hooks/use-redirect-to'; @@ -51,38 +50,6 @@ import { getDefaultUnit } from '../shared/currencies'; import { DomainError, getErrorMessage } from '../shared/error'; import { useSendStore } from './send-provider'; -type ConvertedMoneySwitcherProps = { - onSwitchInputCurrency: () => void; - money?: Money; -}; - -const ConvertedMoneySwitcher = ({ - onSwitchInputCurrency, - money, -}: ConvertedMoneySwitcherProps) => { - if (!money) { - return ; - } - - return ( - - ); -}; - export function SendInput() { const { toast } = useToast(); const navigate = useNavigateWithViewTransition(); @@ -228,7 +195,7 @@ export function SendInput() { {!exchangeRateError && ( )} @@ -286,7 +253,7 @@ export function SendInput() { diff --git a/app/features/send/send-store.ts b/app/features/send/send-store.ts index ef9e2dba..d38af242 100644 --- a/app/features/send/send-store.ts +++ b/app/features/send/send-store.ts @@ -111,7 +111,7 @@ type DecodedDestination = { }; type State = { - status: 'idle' | 'quoting'; + status: 'idle' | 'quoting' | 'success'; /** * Amount to send. */ @@ -481,7 +481,7 @@ export const createSendStore = ({ } } - set({ status: 'idle' }); + set({ status: 'success' }); return { success: true, next: 'confirmQuote' }; }, }; diff --git a/app/features/shared/converted-money-switcher.tsx b/app/features/shared/converted-money-switcher.tsx new file mode 100644 index 00000000..543725c8 --- /dev/null +++ b/app/features/shared/converted-money-switcher.tsx @@ -0,0 +1,35 @@ +import { ArrowUpDown } from 'lucide-react'; +import { MoneyDisplay } from '~/components/money-display'; +import { Skeleton } from '~/components/ui/skeleton'; +import type { Money } from '~/lib/money'; +import { getDefaultUnit } from './currencies'; + +type ConvertedMoneySwitcherProps = { + onSwitch: () => void; + money?: Money; +}; + +export const ConvertedMoneySwitcher = ({ + onSwitch, + money, +}: ConvertedMoneySwitcherProps) => { + if (!money) { + return ; + } + + return ( + + ); +}; diff --git a/app/routes/_protected._index.tsx b/app/routes/_protected._index.tsx index 41af69e2..4f1a5a0c 100644 --- a/app/routes/_protected._index.tsx +++ b/app/routes/_protected._index.tsx @@ -84,14 +84,27 @@ export default function Index() {
)} -
- - - +
+
+ + + + + + +
+ + + ); +} diff --git a/app/routes/_protected.buy.checkout.tsx b/app/routes/_protected.buy.checkout.tsx new file mode 100644 index 00000000..9dde804e --- /dev/null +++ b/app/routes/_protected.buy.checkout.tsx @@ -0,0 +1,34 @@ +import { Page } from '~/components/page'; +import { Redirect } from '~/components/redirect'; +import { useAccount } from '~/features/accounts/account-hooks'; +import { BuyCheckoutCashu, BuyCheckoutSpark } from '~/features/buy'; +import { useBuyStore } from '~/features/buy/buy-provider'; + +export default function BuyCheckoutPage() { + const buyAmount = useBuyStore((s) => s.amount); + const buyAccountId = useBuyStore((s) => s.accountId); + const quote = useBuyStore((s) => s.quote); + const account = useAccount(buyAccountId); + + if (!buyAmount || !quote) { + return ; + } + + return ( + + {account.type === 'cashu' ? ( + + ) : ( + + )} + + ); +} diff --git a/app/routes/_protected.buy.tsx b/app/routes/_protected.buy.tsx new file mode 100644 index 00000000..ba18e17f --- /dev/null +++ b/app/routes/_protected.buy.tsx @@ -0,0 +1,21 @@ +import type { LinksFunction } from 'react-router'; +import { Outlet, useSearchParams } from 'react-router'; +import { useAccountOrDefault } from '~/features/accounts/account-hooks'; +import { BuyProvider } from '~/features/buy'; +import { CASH_APP_LOGO_URL } from '~/features/buy/cash-app'; + +export const links: LinksFunction = () => [ + { rel: 'prefetch', href: CASH_APP_LOGO_URL, as: 'image' }, +]; + +export default function BuyLayout() { + const [searchParams] = useSearchParams(); + const accountId = searchParams.get('accountId'); + const initialAccount = useAccountOrDefault(accountId); + + return ( + + + + ); +}