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>
);
}
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>