Payment Method Selector

PreviousNext

A payment method selector component with support for cards, digital wallets, UPI, and BNPL services

Docs
billingsdkblock

Preview

Loading preview…
registry/billingsdk/payment-method-selector.tsx
"use client";

import type React from "react";
import { useState } from "react";
import { motion, AnimatePresence } from "motion/react";
import {
  X,
  CreditCard,
  QrCode,
  Wallet,
  Landmark,
  ArrowRight,
} from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge";

type PaymentMethod = "cards" | "digital-wallets" | "upi" | "bnpl-services";

interface PaymentOption {
  id: PaymentMethod;
  name: string;
  description: string;
  icon: React.ReactNode;
  examples: string[];
  details: string;
  features: string[];
}

interface FormData {
  cardNumber?: string;
  expiryDate?: string;
  cvv?: string;
  cardholderName?: string;
  email?: string;
  phone?: string;
  upiId?: string;
  income?: string;
}

const paymentOptions: PaymentOption[] = [
  {
    id: "cards",
    name: "Cards",
    description: "Visa, Mastercard, etc.",
    icon: <CreditCard className="h-4 w-4 sm:h-5 sm:w-5" />,
    examples: ["Visa", "Mastercard", "American Express", "Discover"],
    details: "Secure card payments with fraud protection",
    features: [
      "Instant processing",
      "Worldwide acceptance",
      "Cashback rewards",
      "Purchase protection",
    ],
  },
  {
    id: "digital-wallets",
    name: "Digital Wallets",
    description: "Apple Pay, Google Pay, etc.",
    icon: <Wallet className="h-4 w-4 sm:h-5 sm:w-5" />,
    examples: ["Apple Pay", "Google Pay", "Cash App", "Venmo"],
    details: "Quick and secure mobile payments",
    features: [
      "Touch/Face ID security",
      "No card details shared",
      "Fast checkout",
      "Loyalty integration",
    ],
  },
  {
    id: "upi",
    name: "UPI",
    description: "Unified Payments Interface",
    icon: <QrCode className="h-4 w-4 sm:h-5 sm:w-5" />,
    examples: ["PhonePe", "Google Pay", "Paytm", "BHIM"],
    details: "Real-time bank-to-bank transfers",
    features: [
      "24/7 availability",
      "No transaction fees",
      "QR code payments",
      "Bank account linking",
    ],
  },
  {
    id: "bnpl-services",
    name: "Buy Now, Pay Later",
    description: "Klarna, Affirm, Afterpay",
    icon: <Landmark className="h-4 w-4 sm:h-5 sm:w-5" />,
    examples: ["Klarna", "Affirm", "Afterpay", "Sezzle"],
    details: "Buy now, pay later in installments",
    features: [
      "Split payments",
      "No interest options",
      "Flexible terms",
      "Credit building",
    ],
  },
];

export interface PaymentMethodSelectorProps {
  className?: string;
  onProceed?: (method: PaymentMethod, data: FormData) => void;
}

export function PaymentMethodSelector({
  onProceed,
}: PaymentMethodSelectorProps) {
  const [selectedMethod, setSelectedMethod] = useState<PaymentMethod | null>(
    null,
  );
  const [formData, setFormData] = useState<FormData>({});

  const handleMethodSelect = (method: PaymentMethod) => {
    setSelectedMethod(selectedMethod === method ? null : method);
    setFormData({});
  };

  const handleClose = () => {
    setSelectedMethod(null);
    setFormData({});
  };

  const handleInputChange = (field: keyof FormData, value: string) => {
    setFormData((prev) => ({ ...prev, [field]: value }));
  };

  const renderForm = (method: PaymentMethod) => {
    switch (method) {
      case "cards":
        return (
          <motion.div
            initial={{ y: 8, opacity: 0 }}
            animate={{ y: 0, opacity: 1 }}
            transition={{
              delay: 0.25,
              duration: 0.5,
              ease: [0.25, 0.46, 0.45, 0.94],
            }}
            className="space-y-4"
            onClick={(e) => e.stopPropagation()}
          >
            <div className="grid grid-cols-2 gap-3 sm:gap-4">
              <div className="col-span-2">
                <Label
                  htmlFor="cardNumber"
                  className="text-foreground text-xs font-medium sm:text-sm"
                >
                  Card Number
                </Label>
                <Input
                  id="cardNumber"
                  type="text"
                  placeholder="1234 5678 9012 3456"
                  value={formData.cardNumber || ""}
                  onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
                    handleInputChange("cardNumber", e.target.value)
                  }
                  className="border-border/50 focus:border-primary/50 mt-1.5 transition-colors"
                />
              </div>
              <div>
                <Label
                  htmlFor="expiryDate"
                  className="text-foreground text-xs font-medium sm:text-sm"
                >
                  Expiry Date
                </Label>
                <Input
                  id="expiryDate"
                  type="text"
                  placeholder="MM/YY"
                  value={formData.expiryDate || ""}
                  onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
                    handleInputChange("expiryDate", e.target.value)
                  }
                  className="border-border/50 focus:border-primary/50 mt-1.5 transition-colors"
                />
              </div>
              <div>
                <Label
                  htmlFor="cvv"
                  className="text-foreground text-xs font-medium sm:text-sm"
                >
                  CVV
                </Label>
                <Input
                  id="cvv"
                  type="text"
                  placeholder="123"
                  value={formData.cvv || ""}
                  onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
                    handleInputChange("cvv", e.target.value)
                  }
                  className="border-border/50 focus:border-primary/50 mt-1.5 transition-colors"
                />
              </div>
              <div className="col-span-2">
                <Label
                  htmlFor="cardholderName"
                  className="text-foreground text-xs font-medium sm:text-sm"
                >
                  Cardholder Name
                </Label>
                <Input
                  id="cardholderName"
                  type="text"
                  placeholder="John Doe"
                  value={formData.cardholderName || ""}
                  onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
                    handleInputChange("cardholderName", e.target.value)
                  }
                  className="border-border/50 focus:border-primary/50 mt-1.5 transition-colors"
                />
              </div>
            </div>
          </motion.div>
        );

      case "digital-wallets":
        return (
          <motion.div
            initial={{ y: 8, opacity: 0 }}
            animate={{ y: 0, opacity: 1 }}
            transition={{
              delay: 0.25,
              duration: 0.5,
              ease: [0.25, 0.46, 0.45, 0.94],
            }}
            className="space-y-4"
            onClick={(e) => e.stopPropagation()}
          >
            <div className="space-y-4">
              <div>
                <Label
                  htmlFor="email"
                  className="text-foreground text-xs font-medium sm:text-sm"
                >
                  Email Address
                </Label>
                <Input
                  id="email"
                  type="email"
                  placeholder="john@example.com"
                  value={formData.email || ""}
                  onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
                    handleInputChange("email", e.target.value)
                  }
                  className="border-border/50 focus:border-primary/50 mt-1.5 transition-colors"
                />
              </div>
              <div>
                <Label
                  htmlFor="phone"
                  className="text-foreground text-xs font-medium sm:text-sm"
                >
                  Phone Number
                </Label>
                <Input
                  id="phone"
                  type="tel"
                  placeholder="+1 (555) 123-4567"
                  value={formData.phone || ""}
                  onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
                    handleInputChange("phone", e.target.value)
                  }
                  className="border-border/50 focus:border-primary/50 mt-1.5 transition-colors"
                />
              </div>
            </div>
            <div className="bg-muted/20 border-border/30 rounded-lg border p-3">
              <p className="text-muted-foreground text-xs leading-relaxed sm:text-sm">
                You'll be redirected to your selected wallet app to complete the
                payment securely.
              </p>
            </div>
          </motion.div>
        );

      case "upi":
        return (
          <motion.div
            initial={{ y: 8, opacity: 0 }}
            animate={{ y: 0, opacity: 1 }}
            transition={{
              delay: 0.25,
              duration: 0.5,
              ease: [0.25, 0.46, 0.45, 0.94],
            }}
            className="space-y-4"
            onClick={(e) => e.stopPropagation()}
          >
            <div>
              <Label
                htmlFor="upiId"
                className="text-foreground text-xs font-medium sm:text-sm"
              >
                UPI ID
              </Label>
              <Input
                id="upiId"
                type="text"
                placeholder="yourname@paytm"
                value={formData.upiId || ""}
                onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
                  handleInputChange("upiId", e.target.value)
                }
                className="border-border/50 focus:border-primary/50 mt-1.5 transition-colors"
              />
            </div>
            <div className="bg-muted/20 border-border/30 rounded-lg border p-3">
              <p className="text-muted-foreground text-xs leading-relaxed sm:text-sm">
                Enter your UPI ID to receive a payment request. Complete the
                payment in your UPI app.
              </p>
            </div>
          </motion.div>
        );

      case "bnpl-services":
        return (
          <motion.div
            initial={{ y: 8, opacity: 0 }}
            animate={{ y: 0, opacity: 1 }}
            transition={{
              delay: 0.25,
              duration: 0.5,
              ease: [0.25, 0.46, 0.45, 0.94],
            }}
            className="space-y-4"
            onClick={(e) => e.stopPropagation()}
          >
            <div className="space-y-4">
              <div>
                <Label
                  htmlFor="bnplEmail"
                  className="text-foreground text-xs font-medium sm:text-sm"
                >
                  Email Address
                </Label>
                <Input
                  id="bnplEmail"
                  type="email"
                  placeholder="john@example.com"
                  value={formData.email || ""}
                  onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
                    handleInputChange("email", e.target.value)
                  }
                  className="border-border/50 focus:border-primary/50 mt-1.5 transition-colors"
                />
              </div>
              <div>
                <Label
                  htmlFor="bnplPhone"
                  className="text-foreground text-xs font-medium sm:text-sm"
                >
                  Phone Number
                </Label>
                <Input
                  id="bnplPhone"
                  type="tel"
                  placeholder="+1 (555) 123-4567"
                  value={formData.phone || ""}
                  onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
                    handleInputChange("phone", e.target.value)
                  }
                  className="border-border/50 focus:border-primary/50 mt-1.5 transition-colors"
                />
              </div>
              <div>
                <Label
                  htmlFor="income"
                  className="text-foreground text-xs font-medium sm:text-sm"
                >
                  Annual Income
                  <Badge
                    variant="secondary"
                    className="px-1.5 py-0.5 text-[10px]"
                  >
                    Optional
                  </Badge>
                </Label>
                <Input
                  id="income"
                  type="text"
                  placeholder="$50,000"
                  value={formData.income || ""}
                  onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
                    handleInputChange("income", e.target.value)
                  }
                  className="border-border/50 focus:border-primary/50 mt-1.5 transition-colors"
                />
              </div>
            </div>
            <div className="bg-muted/20 border-border/30 rounded-lg border p-3">
              <p className="text-muted-foreground text-xs leading-relaxed sm:text-sm">
                You'll be redirected to complete a quick eligibility check and
                set up your payment plan.
              </p>
            </div>
          </motion.div>
        );

      default:
        return null;
    }
  };

  return (
    <Card className="mx-auto w-full max-w-lg overflow-hidden text-left shadow-lg">
      <CardHeader className="px-4 pb-2 sm:px-6">
        <CardTitle className="text-base font-semibold">
          Payment Methods
        </CardTitle>
        <p className="text-muted-foreground text-sm">
          Choose your preferred payment method to continue
        </p>
      </CardHeader>

      <CardContent className="space-y-3 px-4 sm:px-6">
        <AnimatePresence mode="wait">
          {paymentOptions.map((option) => {
            const isSelected = selectedMethod === option.id;

            return (
              <motion.div
                key={option.id}
                onClick={() => handleMethodSelect(option.id)}
                className={`cursor-pointer rounded-lg border p-4 shadow-sm transition-all duration-300 hover:shadow-md ${
                  isSelected
                    ? "border-primary shadow-md"
                    : "border-border hover:border-primary/50"
                }`}
              >
                {isSelected && (
                  <motion.button
                    onClick={(e) => {
                      e.stopPropagation();
                      handleClose();
                    }}
                    className="bg-background/80 hover:bg-background border-border/50 absolute top-3 right-3 z-20 rounded-full border p-1.5 shadow-sm transition-all duration-200 hover:shadow-md"
                    initial={{ scale: 0, rotate: -45 }}
                    animate={{ scale: 1, rotate: 0 }}
                    transition={{
                      delay: 0.15,
                      duration: 0.4,
                      ease: [0.25, 0.46, 0.45, 0.94],
                    }}
                    whileHover={{ scale: 1.05 }}
                    whileTap={{ scale: 0.95 }}
                  >
                    <X className="text-muted-foreground h-3 w-3" />
                  </motion.button>
                )}

                <div className="flex items-center space-x-4">
                  <div
                    className={`rounded-lg border p-2.5 shadow-sm transition-all duration-300 sm:p-3 ${
                      isSelected
                        ? "bg-background border-primary/30"
                        : "bg-background border-border/50 hover:border-primary/30 hover:shadow-md"
                    }`}
                  >
                    {option.icon}
                  </div>
                  <div className="flex-1 text-left">
                    <h3
                      className={`font-medium transition-colors ${
                        isSelected
                          ? "text-primary"
                          : "text-foreground hover:text-primary"
                      }`}
                    >
                      {option.name}
                    </h3>
                    <p className="text-muted-foreground mt-1 text-xs">
                      {option.description}
                    </p>
                  </div>
                </div>

                <AnimatePresence>
                  {isSelected && (
                    <motion.div
                      initial={{ opacity: 0, height: 0, y: -10 }}
                      animate={{ opacity: 1, height: "auto", y: 0 }}
                      exit={{ opacity: 0, height: 0, y: -10 }}
                      transition={{ duration: 0.3, ease: "easeOut" }}
                      className="overflow-hidden"
                    >
                      <div className="mt-4">{renderForm(option.id)}</div>
                    </motion.div>
                  )}
                </AnimatePresence>
              </motion.div>
            );
          })}
        </AnimatePresence>

        <AnimatePresence>
          {!selectedMethod && (
            <motion.div
              initial={{ y: 8, opacity: 0 }}
              animate={{ y: 0, opacity: 1 }}
              exit={{ y: 8, opacity: 0 }}
              transition={{
                delay: 0.4,
                duration: 0.5,
                ease: [0.25, 0.46, 0.45, 0.94],
              }}
            >
              <Button disabled className="mt-4 w-full">
                <span>Select Payment Method</span>
                <span>→</span>
              </Button>
            </motion.div>
          )}
          {selectedMethod && (
            <motion.div
              initial={{ y: 8, opacity: 0 }}
              animate={{ y: 0, opacity: 1 }}
              exit={{ y: 8, opacity: 0 }}
              transition={{
                delay: 0.4,
                duration: 0.5,
                ease: [0.25, 0.46, 0.45, 0.94],
              }}
            >
              <Button
                className="mt-4 w-full"
                onClick={() => {
                  if (onProceed) onProceed(selectedMethod, formData);
                }}
              >
                <span>Proceed with Payment</span>
                <ArrowRight className="h-4 w-4" />
              </Button>
            </motion.div>
          )}
        </AnimatePresence>
      </CardContent>
    </Card>
  );
}

Installation

npx shadcn@latest add @billingsdk/payment-method-selector

Usage

import { PaymentMethodSelector } from "@/components/payment-method-selector"
<PaymentMethodSelector />