-
Notifications
You must be signed in to change notification settings - Fork 6
Buy bitcoin via Cash App #905
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <div className="flex items-center justify-between"> | ||
| <p className="text-muted-foreground">{label}</p> | ||
| <div>{value}</div> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| function ErrorCard({ message }: { message: string }) { | ||
| return ( | ||
| <Card className="w-full"> | ||
| <CardContent className="flex flex-col items-center justify-center gap-2 p-6"> | ||
| <AlertCircle className="h-8 w-8 text-foreground" /> | ||
| <p className="text-center text-muted-foreground text-sm">{message}</p> | ||
| </CardContent> | ||
| </Card> | ||
| ); | ||
| } | ||
|
|
||
| function ConfirmationDetails({ | ||
| accountName, | ||
| fee, | ||
| }: { accountName: string; fee?: Money }) { | ||
| return ( | ||
| <Card className="w-full"> | ||
| <CardContent className="flex flex-col gap-6 pt-6"> | ||
| <ConfirmationRow label="From" value="Cash App" /> | ||
| <ConfirmationRow label="To" value={accountName} /> | ||
| {fee && ( | ||
| <ConfirmationRow | ||
| label="Fee" | ||
| value={ | ||
| <MoneyDisplay | ||
| size="sm" | ||
| money={fee} | ||
| unit={getDefaultUnit(fee.currency)} | ||
| /> | ||
| } | ||
| /> | ||
| )} | ||
| </CardContent> | ||
| </Card> | ||
| ); | ||
| } | ||
|
|
||
| function MobileCheckoutContent({ | ||
| errorMessage, | ||
| accountName, | ||
| fee, | ||
| }: { | ||
| errorMessage: string | undefined; | ||
| accountName: string; | ||
| fee?: Money; | ||
| }) { | ||
| return ( | ||
| <div className="absolute top-0 right-0 bottom-0 left-0 mx-auto flex max-w-sm items-center justify-center"> | ||
| <div className="m-4 w-full"> | ||
| {errorMessage ? ( | ||
| <ErrorCard message={errorMessage} /> | ||
| ) : ( | ||
| <ConfirmationDetails accountName={accountName} fee={fee} /> | ||
| )} | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| function DesktopCheckoutContent({ | ||
| errorMessage, | ||
| paymentRequest, | ||
| accountName, | ||
| fee, | ||
| }: { | ||
| errorMessage: string | undefined; | ||
| paymentRequest: string; | ||
| accountName: string; | ||
| fee?: Money; | ||
| }) { | ||
| if (errorMessage) { | ||
| return ( | ||
| <div className="max-w-sm"> | ||
| <ErrorCard message={errorMessage} /> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| const deepLinkUrl = buildCashAppDeepLink(paymentRequest); | ||
|
|
||
| return ( | ||
| <> | ||
| <QRCode | ||
| value={deepLinkUrl} | ||
| description="Scan with Cash App" | ||
| className="gap-4" | ||
| /> | ||
| <div className="max-w-sm"> | ||
| <ConfirmationDetails accountName={accountName} fee={fee} /> | ||
| </div> | ||
| </> | ||
| ); | ||
| } | ||
|
|
||
| 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 ( | ||
| <> | ||
| <PageHeader className="z-10"> | ||
| <PageBackButton | ||
| to={buildLinkWithSearchParams('/buy')} | ||
| transition="slideRight" | ||
| applyTo="oldView" | ||
| /> | ||
| <PageHeaderTitle>Buy</PageHeaderTitle> | ||
| </PageHeader> | ||
| <PageContent className="flex flex-col items-center gap-4"> | ||
| <MoneyWithConvertedAmount money={amount} /> | ||
| {isMobile ? ( | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I find this hard to read with this very long chaining of: isMobile ? (bunch of elements) : errorMessage ? (elements) : (elements) I think it sould be reorganized so you can see at first glance what is rendered on mobile and what on desktop and what on error. I'd probably just create a component int his file for mobile and for desktop and then render those here |
||
| <MobileCheckoutContent | ||
| errorMessage={errorMessage} | ||
| accountName={accountName} | ||
| fee={fee} | ||
| /> | ||
| ) : ( | ||
| <DesktopCheckoutContent | ||
| errorMessage={errorMessage} | ||
| paymentRequest={paymentRequest} | ||
| accountName={accountName} | ||
| fee={fee} | ||
| /> | ||
| )} | ||
| </PageContent> | ||
| {isMobile && !errorMessage && ( | ||
| <PageFooter className="pb-14"> | ||
| <Button asChild> | ||
| <a href={deepLinkUrl}>Pay</a> | ||
| </Button> | ||
| </PageFooter> | ||
| )} | ||
| </> | ||
| ); | ||
| } | ||
|
|
||
| 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 ( | ||
| <CashAppCheckout | ||
| paymentRequest={quote.paymentRequest} | ||
| amount={amount} | ||
| accountName={accountName} | ||
| errorMessage={getErrorMessageFromQuoteStatus(quotePaymentStatus)} | ||
| fee={quote.mintingFee} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| 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 ( | ||
| <CashAppCheckout | ||
| paymentRequest={quote.paymentRequest} | ||
| amount={amount} | ||
| accountName={accountName} | ||
| errorMessage={getErrorMessageFromQuoteStatus(quotePaymentStatus)} | ||
| /> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <Drawer> | ||
| <DrawerTrigger asChild> | ||
| <button type="button"> | ||
| <Info className="h-5 w-5 text-muted-foreground" /> | ||
| </button> | ||
| </DrawerTrigger> | ||
| <DrawerContent className="h-[90svh] font-primary"> | ||
| <div className="mx-auto flex h-full w-full max-w-sm flex-col overflow-hidden"> | ||
| <DrawerHeader className="mb-4 shrink-0"> | ||
| <DrawerTitle>Frequently Asked Questions</DrawerTitle> | ||
| </DrawerHeader> | ||
| <div className="min-h-0 flex-1 overflow-y-auto px-4 pb-8"> | ||
| <div className="space-y-6"> | ||
| {faqItems.map((item) => ( | ||
| <div key={item.question}> | ||
| <h3 className="font-semibold text-sm">{item.question}</h3> | ||
| <p className="mt-1 text-muted-foreground text-sm"> | ||
| {item.answer} | ||
| </p> | ||
| </div> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </DrawerContent> | ||
| </Drawer> | ||
| ); | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.