feat(payment): share payment method selector

This commit is contained in:
2026-07-17 18:27:24 +08:00
parent 66445a9972
commit b22db6d147
13 changed files with 386 additions and 173 deletions
@@ -0,0 +1,108 @@
"use client";
import Image from "next/image";
import type { PayChannel } from "@/data/schemas/payment";
const PAYMENT_METHODS: readonly {
channel: PayChannel;
title: string;
logoSrc?: string;
}[] = [
{
channel: "stripe",
title: "Stripe",
},
{
channel: "ezpay",
title: "GCash",
logoSrc: "/images/subscription/gcash-logo.svg",
},
];
export interface PaymentMethodSelectorProps {
value: PayChannel;
defaultChannel?: PayChannel;
disabled?: boolean;
caption?: string;
analyticsKey: string;
onChange: (channel: PayChannel) => void;
}
export function PaymentMethodSelector({
value,
defaultChannel = "stripe",
disabled = false,
caption = "Stripe by default",
analyticsKey,
onChange,
}: PaymentMethodSelectorProps) {
const paymentMethods =
defaultChannel === "ezpay"
? [...PAYMENT_METHODS].sort((a, b) =>
a.channel === "ezpay" ? -1 : b.channel === "ezpay" ? 1 : 0,
)
: PAYMENT_METHODS;
return (
<section
className="flex flex-col gap-[clamp(8px,1.852vw,10px)]"
aria-labelledby="payment-method-title"
>
<div className="flex items-baseline justify-between gap-sm px-xs">
<h2
id="payment-method-title"
className="m-0 text-(length:--responsive-card-title,var(--font-size-lg)) font-bold leading-[1.2] text-auth-text-primary"
style={{ fontFamily: "var(--font-athelas), var(--font-system)" }}
>
Payment Method
</h2>
<span className="whitespace-nowrap text-(length:--responsive-caption,var(--font-size-sm)) leading-[1.2] text-text-secondary">
{caption}
</span>
</div>
<div className="grid grid-cols-2 gap-sm">
{paymentMethods.map((method) => {
const selected = method.channel === value;
const classes = [
"flex min-h-[clamp(54px,11.481vw,62px)] cursor-pointer items-center justify-center rounded-(--responsive-card-radius-sm,18px) px-[clamp(12px,2.593vw,14px)] py-[clamp(9px,1.852vw,10px)] text-center font-[inherit] text-[#3c3b3b] transition-[border-color,box-shadow,transform] duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent active:enabled:scale-98 disabled:cursor-not-allowed disabled:opacity-72",
selected
? "border-2 border-[#f657a0] bg-[#fff4f9] shadow-[0_8px_18px_rgba(217,47,127,0.24),0_0_0_3px_rgba(217,47,127,0.18)] -translate-y-0.5"
: "border border-[rgba(246,87,160,0.18)] bg-white shadow-[0_5px_7px_0_rgba(247,89,168,0.08)]",
]
.filter(Boolean)
.join(" ");
return (
<button
key={method.channel}
type="button"
data-analytics-key={analyticsKey}
data-analytics-label="Select payment method"
className={classes}
disabled={disabled}
aria-pressed={selected}
aria-label={method.title}
onClick={() => onChange(method.channel)}
>
{method.logoSrc ? (
<Image
src={method.logoSrc}
alt=""
aria-hidden="true"
width={122}
height={29}
className="block max-h-8.5 w-[min(100%,92px)] object-contain"
/>
) : (
<span className="text-(length:--responsive-body,16px) font-bold leading-[1.1] text-[#1e1e1e]">
{method.title}
</span>
)}
</button>
);
})}
</div>
</section>
);
}