Back to Home

Accordion

A vertically stacked set of interactive headings that each reveal a section of content.

"use client";

import * as React from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { cn } from "@/lib/utils";
import { toast } from "sonner";
import { createAuthenticationConfirmationAction } from "@/components/redsys/actions/authentication/create-authentication-confirmation";
import type { CreateAuthenticationConfirmationResult } from "@/components/redsys/actions/authentication/create-authentication-confirmation";

export interface AuthenticationConfirmationFormProps
  extends Omit<React.ComponentPropsWithoutRef<"form">, "onSubmit" | "onError"> {
  /**
   * Initial value for the order ID field.
   */
  orderId?: string;
  /**
   * Initial value for the amount field.
   */
  amount?: string | number;
  /**
   * Whether to show the confirmation dialog before submitting.
   * @default true
   */
  showConfirmationDialog?: boolean;
  /**
   * Callback fired when the authentication confirmation succeeds.
   */
  onSuccess?: (result: CreateAuthenticationConfirmationResult) => void;
  /**
   * Callback fired when the authentication confirmation fails or encounters an error.
   */
  onError?: (error: Error) => void;
}

/**
 * Simple UI block for confirming Redsys authentication operations from the browser.
 * Uses a server action under the hood to call trataPeticionREST with transaction type 8.
 * Confirmation completes the authentication by charging the cardholder.
 * Currency defaults to EUR.
 */
export function AuthenticationConfirmationForm({
  className,
  orderId: initialOrderId = "",
  amount: initialAmount = "",
  showConfirmationDialog = true,
  onSuccess,
  onError,
  ...props
}: AuthenticationConfirmationFormProps) {
  const [orderId, setOrderId] = React.useState(initialOrderId || "");
  const [amount, setAmount] = React.useState(
    initialAmount ? String(initialAmount) : ""
  );
  const [isSubmitting, setIsSubmitting] = React.useState(false);
  const [showConfirmDialog, setShowConfirmDialog] = React.useState(false);

  React.useEffect(() => {
    if (initialOrderId) {
      setOrderId(initialOrderId);
    }
  }, [initialOrderId]);

  React.useEffect(() => {
    if (initialAmount) {
      setAmount(String(initialAmount));
    }
  }, [initialAmount]);

  const submitAuthentication = async () => {
    setIsSubmitting(true);

    try {
      const amountValue = Number(amount);
      const result = await createAuthenticationConfirmationAction({
        orderId,
        amount: amountValue,
      });

      if (result.success) {
        toast.success("Authentication confirmation accepted by Redsys", {
          description: `Response code: ${result.responseCode}`,
        });
        onSuccess?.(result);
      } else {
        toast.error("Authentication confirmation rejected by Redsys", {
          description: `Response code: ${result.responseCode}`,
        });
        onError?.(
          new Error(
            `Authentication confirmation rejected: ${result.responseCode}`
          )
        );
      }
    } catch (error) {
      const errorMessage =
        error instanceof Error ? error.message : "Unexpected error";
      toast.error("Error confirming authentication", {
        description: errorMessage,
      });
      onError?.(error instanceof Error ? error : new Error(errorMessage));
    } finally {
      setIsSubmitting(false);
    }
  };

  const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();

    const amountValue = Number(amount);
    if (!amount || isNaN(amountValue) || amountValue <= 0) {
      toast.error("Please enter a valid amount greater than 0");
      return;
    }

    if (showConfirmationDialog) {
      setShowConfirmDialog(true);
    } else {
      await submitAuthentication();
    }
  };

  const handleConfirm = async () => {
    setShowConfirmDialog(false);
    await submitAuthentication();
  };

  return (
    <form
      className={cn("space-y-6 rounded-lg border p-6 bg-card", className)}
      onSubmit={handleSubmit}
      {...props}
    >
      {!initialOrderId && (
        <div className="space-y-2">
          <Label htmlFor="confirm-order">Order ID</Label>
          <Input
            id="confirm-order"
            placeholder="Original Redsys order (Ds_Order) from authentication"
            value={orderId}
            onChange={(event) => setOrderId(event.currentTarget.value)}
            className="max-w-xs"
            required
          />
        </div>
      )}

      {!initialAmount && (
        <div className="space-y-2">
          <Label htmlFor="confirm-amount">Amount</Label>
          <Input
            id="confirm-amount"
            type="number"
            step="0.01"
            min="0"
            value={amount}
            onChange={(event) => setAmount(event.currentTarget.value)}
            placeholder="Enter amount to confirm"
            className="max-w-xs"
            required
          />
          <p className="text-xs text-muted-foreground">
            Amount can be less than or equal to the original authentication
            amount.
          </p>
        </div>
      )}

      <Button type="submit" disabled={isSubmitting}>
        {isSubmitting
          ? "Confirming authentication..."
          : "Confirm Authentication"}
      </Button>

      {showConfirmationDialog && (
        <AlertDialog
          open={showConfirmDialog}
          onOpenChange={setShowConfirmDialog}
        >
          <AlertDialogContent>
            <AlertDialogHeader>
              <AlertDialogTitle>Confirm Authentication</AlertDialogTitle>
              <AlertDialogDescription>
                Are you sure you want to confirm this authentication? This
                action will charge the cardholder for the specified amount.
              </AlertDialogDescription>
              {orderId && (
                <div className="mt-2 space-y-1">
                  <div className="font-medium text-sm">Order ID: {orderId}</div>
                  <div className="font-medium text-sm">
                    Amount: €{Number(amount).toFixed(2)}
                  </div>
                </div>
              )}
            </AlertDialogHeader>
            <AlertDialogFooter>
              <AlertDialogCancel disabled={isSubmitting}>
                Cancel
              </AlertDialogCancel>
              <AlertDialogAction
                onClick={handleConfirm}
                disabled={isSubmitting}
              >
                {isSubmitting ? "Confirming..." : "Confirm"}
              </AlertDialogAction>
            </AlertDialogFooter>
          </AlertDialogContent>
        </AlertDialog>
      )}
    </form>
  );
}

Installation

bunx --bun shadcn@latest add accordion

Usage

import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion";
<Accordion type="single" collapsible>
  <AccordionItem value="item-1">
    <AccordionTrigger>Is it accessible?</AccordionTrigger>
    <AccordionContent>
      Yes. It adheres to the WAI-ARIA design pattern.
    </AccordionContent>
  </AccordionItem>
</Accordion>