Skip to content

Commit

Permalink
Nicer card select
Browse files Browse the repository at this point in the history
  • Loading branch information
razzeee committed Nov 1, 2024
1 parent dabc2e1 commit d2e385b
Show file tree
Hide file tree
Showing 13 changed files with 268 additions and 77 deletions.
44 changes: 44 additions & 0 deletions frontend/@/components/ui/radio-group.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"use client"

import * as React from "react"
import { CheckIcon } from "@radix-ui/react-icons"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"

import { cn } from "@/lib/utils"

const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Root
className={cn("grid gap-2", className)}
{...props}
ref={ref}
/>
)
})
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName

const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<CheckIcon className="h-3.5 w-3.5 fill-primary" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
})
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName

export { RadioGroup, RadioGroupItem }
3 changes: 2 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@mitresthen/matomo-tracker-react": "^0.1.1",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-radio-group": "^1.2.1",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-tooltip": "^1.1.3",
Expand Down Expand Up @@ -117,4 +118,4 @@
"public"
]
}
}
}
35 changes: 7 additions & 28 deletions frontend/pages/payment/details/[transaction_id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,9 @@ import { NextSeo } from "next-seo"
import { useRouter } from "next/router"
import { ReactElement } from "react"
import Breadcrumbs from "../../../src/components/Breadcrumbs"
import TransactionCancelButton from "../../../src/components/payment/transactions/TransactionCancelButton"
import TransactionDetails from "../../../src/components/payment/transactions/TransactionDetails"
import Spinner from "../../../src/components/Spinner"
import LoginGuard from "../../../src/components/login/LoginGuard"
import { Button } from "@/components/ui/button"
import Link from "next/link"
import { useGetTransactionByIdWalletTransactionsTxnGet } from "src/codegen"

export default function TransactionPage() {
Expand All @@ -29,37 +26,19 @@ export default function TransactionPage() {
},
)

let content: ReactElement = <Spinner size="l" />
if (query.error) {
let content: ReactElement

if (query.isFetching) {
content = <Spinner size="l" />
} else if (query.isError) {
content = (
<>
<h1 className="my-8 text-4xl font-extrabold">{t("whoops")}</h1>
<p>{t(query.error.message)}</p>
</>
)
} else if (query.data) {
const unresolved = ["new", "retry"].includes(query.data.data.summary.status)
if (unresolved) {
content = (
<>
<h1 className="my-8 text-4xl font-extrabold">{t("whoops")}</h1>
<p>{t("transaction-went-wrong")}</p>
<div className="flex gap-3">
<TransactionCancelButton
id={query.data.data.summary.id}
onSuccess={() => router.reload()}
/>
<Button asChild size="lg">
<Link href={`/payment/${query.data.data.summary.id}`}>
{t("retry-checkout")}
</Link>
</Button>
</div>
</>
)
} else {
content = <TransactionDetails transaction={query.data.data} />
}
} else {
content = <TransactionDetails transaction={query.data.data} />
}

const pages = [
Expand Down
1 change: 0 additions & 1 deletion frontend/public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,6 @@
"kind-purchase": "Purchase",
"kind-purchase-app": "Purchase {{appName}}",
"kind-donation-app": "Donate to {{appName}}",
"loading-saved-payment-methods": "Loading saved payment methods…",
"payment": "Payment",
"make-donation": "Make Donation",
"no-saved-payment-methods": "No saved payment methods to show.",
Expand Down
9 changes: 5 additions & 4 deletions frontend/src/components/layout/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ const navigation = [

let userNavigation = [
{ name: "my-flathub", href: "/my-flathub" },
{
name: "view-wallet",
href: "/wallet",
condition: (user: UserInfo) => !IS_PRODUCTION,
},
{ name: "developer-portal", href: "/developer-portal" },
{
name: "Admin",
Expand All @@ -52,10 +57,6 @@ let userNavigation = [
{ name: "settings", href: "/settings" },
]

if (!IS_PRODUCTION) {
userNavigation.push({ name: "view-wallet", href: "/wallet" })
}

const MobileMenuButton = ({ open, close, width }) => {
const { t } = useTranslation()

Expand Down
17 changes: 13 additions & 4 deletions frontend/src/components/payment/cards/CardInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import amex from "public/img/payment-methods/amex.svg"
import mastercard from "public/img/payment-methods/mastercard.svg"
import visa from "public/img/payment-methods/visa.svg"
import visaDark from "public/img/payment-methods/visa-dark.svg"
import { Skeleton } from "@/components/ui/skeleton"

interface Props {
card: PaymentCardInfo
Expand All @@ -29,12 +30,22 @@ function getBrandImage(brand: string, theme: string): string {
}
}

const CardInfo: FunctionComponent<Props> = ({ card, onClick, className }) => {
export const CardInfoSkeleton: FunctionComponent = () => {
return (
<Skeleton className="grid grid-cols-3 grid-rows-3 shadow-md w-[250px] h-44 min-w-[200px] p-4 rounded-xl bg-flathub-white dark:bg-flathub-arsenic max-w-[300px]" />
)
}

export const CardInfo: FunctionComponent<Props> = ({
card,
onClick,
className,
}) => {
const { t } = useTranslation()
const { resolvedTheme } = useTheme()

const classes = [
"grid grid-cols-3 grid-rows-3 shadow-md w-[250px] min-w-[200px] p-4 rounded-xl bg-flathub-white dark:bg-flathub-arsenic max-w-[300px]",
"grid grid-cols-3 grid-rows-3 shadow-md w-[250px] min-w-[200px] h-44 p-4 rounded-xl bg-flathub-white dark:bg-flathub-arsenic max-w-[300px]",
]
if (onClick) {
classes.push("hover:cursor-pointer")
Expand Down Expand Up @@ -95,5 +106,3 @@ const CardInfo: FunctionComponent<Props> = ({ card, onClick, className }) => {
</div>
)
}

export default CardInfo
5 changes: 2 additions & 3 deletions frontend/src/components/payment/cards/SavedCards.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { useQuery } from "@tanstack/react-query"
import { useTranslation } from "next-i18next"
import { FunctionComponent, ReactNode } from "react"
import Spinner from "../../Spinner"
import CardInfo from "./CardInfo"
import DeleteCardButton from "./DeleteCardButton"
import { getWalletinfoWalletWalletinfoGet } from "src/codegen"
import { CardInfo, CardInfoSkeleton } from "./CardInfo"

const SavedCards: FunctionComponent = () => {
const { t } = useTranslation()
Expand All @@ -19,7 +18,7 @@ const SavedCards: FunctionComponent = () => {
})

if (walletQuery.isPending) {
return <Spinner size="m" text={t("loading-saved-payment-methods")} />
return <CardInfoSkeleton />
}

let content: ReactNode
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,9 @@ export const Generated = () => {
<CardSelect
transaction={transaction}
clientSecret={clientSecret}
cards={cards}
error={"error"}
submit={() => {}}
skip={() => {}}
walletQuery={{ data: { data: { cards } } }}
/>
</Elements>
)
Expand Down
68 changes: 42 additions & 26 deletions frontend/src/components/payment/checkout/CardSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { useStripe } from "@stripe/react-stripe-js"
import { useTranslation } from "next-i18next"
import { FunctionComponent, ReactElement, useState } from "react"
import Spinner from "../../Spinner"
import CardInfo from "../cards/CardInfo"
import { CardInfo, CardInfoSkeleton } from "../cards/CardInfo"
import { handleStripeError } from "./stripe"
import { useMutation } from "@tanstack/react-query"
import { AxiosError } from "axios"
Expand All @@ -14,28 +13,33 @@ import { PaymentCardInfo, Transaction } from "src/codegen/model"
import { Button } from "@/components/ui/button"
import { Loader2 } from "lucide-react"
import { TransactionCancelButtonPrep } from "./Checkout"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"

interface Props {
transaction: Transaction
clientSecret: string
cards: PaymentCardInfo[]
error: string | null
submit: () => void
skip: () => void
walletQuery: any
}

const CardSelect: FunctionComponent<Props> = ({
transaction,
clientSecret,
cards,
error,
submit,
skip,
walletQuery,
}) => {
const { t } = useTranslation()
const stripe = useStripe()

const [useCard, setUseCard] = useState<PaymentCardInfo | null>(null)
const error = walletQuery.isError ? "failed-to-load-refresh" : null

const cards = walletQuery?.data?.data.cards ?? []

const [useCard, setUseCard] = useState<PaymentCardInfo | null>(
cards[0] ?? null,
)

const mutation = useMutation({
mutationFn: async ({ id }: { id: string }) => {
Expand Down Expand Up @@ -69,31 +73,43 @@ const CardSelect: FunctionComponent<Props> = ({
let cardSection: ReactElement
if (error) {
cardSection = <p>{t(error)}</p>
} else if (walletQuery.isPending) {
cardSection = (
<div className="flex flex-row gap-5">
<RadioGroup className="flex items-center space-x-2">
<RadioGroupItem value={""} />
<CardInfoSkeleton />
</RadioGroup>
</div>
)
} else if (cards) {
const cardElems = cards.map((card) => {
return (
<CardInfo
key={card.id}
card={card}
onClick={() => setUseCard(card)}
className={
useCard && card.id === useCard.id
? "border border-flathub-celestial-blue dark:border-flathub-celestial-blue"
: ""
}
/>
)
})

cardSection = <div className="flex flex-row gap-5 p-5">{cardElems}</div>
} else {
cardSection = <Spinner size="m" text={t("loading-saved-payment-methods")} />
cardSection = (
<RadioGroup value={useCard?.id} className="flex flex-row gap-5">
{cards.map((card) => {
return (
<div className="flex items-center space-x-2" key={card.id}>
<RadioGroupItem value={card.id} id={card.id} />
<CardInfo
key={card.id}
card={card}
onClick={() => setUseCard(card)}
className={
useCard &&
card.id === useCard.id &&
"border border-flathub-celestial-blue dark:border-flathub-celestial-blue"
}
/>
</div>
)
})}
</RadioGroup>
)
}

// Should always present the option to use a new card in case user
// doesn't want to wait for a slow network
return (
<div className="max-w-11/12 mx-auto my-0 w-11/12 2xl:w-[1400px] 2xl:max-w-[1400px]">
<div className="max-w-11/12 mx-auto my-0 w-11/12 2xl:w-[1400px] 2xl:max-w-[1400px] flex gap-4 flex-col">
<h3 className="my-4 text-xl font-semibold">{t("saved-cards")}</h3>
{cardSection}
<div className="flex flex-col-reverse gap-4 sm:flex-row">
Expand Down
13 changes: 6 additions & 7 deletions frontend/src/components/payment/checkout/Checkout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { getWalletinfoWalletWalletinfoGet, Transaction } from "src/codegen"
enum Stage {
TermsAgreement,
CardSelect,
CardInput,
AmountInput,
}

const detailsPage = `${process.env.NEXT_PUBLIC_SITE_BASE_URI}/payment/details`
Expand Down Expand Up @@ -65,15 +65,15 @@ const Checkout: FunctionComponent<{

// User may have no saved cards to select from
setStage(
wallet.data.cards.length > 0 ? Stage.CardSelect : Stage.CardInput,
wallet.data.cards.length > 0 ? Stage.CardSelect : Stage.AmountInput,
)

return wallet
},
enabled: termsAgreed,
})

if (walletQuery.isPending || !transaction) {
if (!transaction) {
return <Spinner size="m" />
}

Expand All @@ -97,18 +97,17 @@ const Checkout: FunctionComponent<{
<CardSelect
transaction={transaction}
clientSecret={clientSecret}
cards={cards}
error={walletQuery.isError ? "failed-to-load-refresh" : null}
walletQuery={walletQuery}
submit={() =>
router.push(`${detailsPage}/${transactionId}`, undefined, {
locale: router.locale,
})
}
skip={() => setStage(Stage.CardInput)}
skip={() => setStage(Stage.AmountInput)}
/>
)
break
case Stage.CardInput:
case Stage.AmountInput:
flowContent = (
<PaymentForm
transactionId={transactionId}
Expand Down
Loading

0 comments on commit d2e385b

Please sign in to comment.