diff --git a/src/hooks/useCulqiScript.tsx b/src/hooks/useCulqiScript.tsx new file mode 100644 index 0000000..76b775c --- /dev/null +++ b/src/hooks/useCulqiScript.tsx @@ -0,0 +1,45 @@ +import { useEffect, useRef, useState } from "react"; + +interface CulqiInstance { + open: () => void; + close: () => void; + token?: { id: string }; + error?: Error; + culqi?: () => void; +} + +declare global { + interface Window { + CulqiCheckout: new (publicKey: string, config: object) => CulqiInstance; + } +} + +/** + * Custom hook to load Culqi Checkout script and return the CulqiCheckout constructor + * @returns {Window["CulqiCheckout"] | null} CulqiCheckout constructor or null if not loaded + */ +export function useCulqiScript(): Window["CulqiCheckout"] | null { + const [loaded, setLoaded] = useState(false); + const scriptRef = useRef(null); + + useEffect(() => { + if (window.CulqiCheckout) { + setLoaded(true); + return; + } + const script = document.createElement("script"); + script.src = "https://js.culqi.com/checkout-js"; + script.async = true; + scriptRef.current = script; + script.onload = () => setLoaded(true); + script.onerror = () => setLoaded(false); + document.head.appendChild(script); + return () => { + if (scriptRef.current) { + scriptRef.current.remove(); + } + }; + }, []); + + return loaded ? window.CulqiCheckout ?? null : null; +} diff --git a/src/routes/cart/index.tsx b/src/routes/cart/index.tsx index d330cef..9ae9c14 100644 --- a/src/routes/cart/index.tsx +++ b/src/routes/cart/index.tsx @@ -3,7 +3,6 @@ import { Form, Link } from "react-router"; import { Button, Container, Section } from "@/components/ui"; import { calculateTotal, getCart } from "@/lib/cart"; -import { type Cart } from "@/models/cart.model"; import { getSession } from "@/session.server"; import type { Route } from "./+types"; @@ -55,7 +54,7 @@ export default function Cart({ loaderData }: Route.ComponentProps) {

- ${product.price.toFixed(2)} + S/{product.price.toFixed(2)}

diff --git a/src/routes/checkout/index.tsx b/src/routes/checkout/index.tsx index 063dd5f..d9d0736 100644 --- a/src/routes/checkout/index.tsx +++ b/src/routes/checkout/index.tsx @@ -1,6 +1,7 @@ +import { useCulqiScript } from "@/hooks/useCulqiScript"; import { zodResolver } from "@hookform/resolvers/zod"; import { X } from "lucide-react"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { redirect, useNavigation, useSubmit } from "react-router"; import { z } from "zod"; @@ -10,8 +11,8 @@ import { Container, InputField, Section, - Separator, SelectField, + Separator, } from "@/components/ui"; import { calculateTotal, getCart } from "@/lib/cart"; import { type CartItem } from "@/models/cart.model"; @@ -86,7 +87,7 @@ export async function action({ request }: Route.ActionArgs) { const token = formData.get("token") as string; const body = { - amount: 2000, // TODO: Calculate total dynamically + amount: Math.round(calculateTotal(cartItems) * 100), //TODO: Calculate total dynamically currency_code: "PEN", email: shippingDetails.email, source_id: token, @@ -97,7 +98,7 @@ export async function action({ request }: Route.ActionArgs) { method: "POST", headers: { "content-type": "application/json", - Authorization: `Bearer sk_test_EC8oOLd3ZiCTKqjN`, // TODO: Use environment variable + Authorization: `Bearer ${process.env.CULQUI_SECRET_KEY}`, // TODO: Use environment variable for security secret key }, body: JSON.stringify(body), }); @@ -109,6 +110,9 @@ export async function action({ request }: Route.ActionArgs) { throw new Error("Error processing payment"); } + // Obtener el charge de Culqi + const charge = await response.json(); + const items = cartItems.map((item) => ({ productId: item.product.id, quantity: item.quantity, @@ -119,7 +123,9 @@ export async function action({ request }: Route.ActionArgs) { // TODO // @ts-expect-error Arreglar el tipo de shippingDetails - const { id: orderId } = await createOrder(items, shippingDetails); // TODO: Add payment information to the order + const { id: orderId } = await createOrder(items, shippingDetails, { + culquiChargeId: charge.id, + }); // TODO: Add payment information to the order await deleteRemoteCart(request); const session = await getSession(request.headers.get("Cookie")); @@ -158,7 +164,7 @@ export default function Checkout({ loaderData }: Route.ComponentProps) { const loading = navigation.state === "submitting"; const [culqui, setCulqui] = useState(null); - const scriptRef = useRef(null); + const CulqiCheckout = useCulqiScript(); const { register, @@ -183,96 +189,46 @@ export default function Checkout({ loaderData }: Route.ComponentProps) { }); useEffect(() => { - // Function to load the Culqi script - const loadCulqiScript = (): Promise => { - return new Promise((resolve, reject) => { - if (window.CulqiCheckout) { - resolve(window.CulqiCheckout); - return; - } - - // Create script element - const script = document.createElement("script"); - script.src = "https://js.culqi.com/checkout-js"; - script.async = true; - - // Store reference for cleanup - scriptRef.current = script; - - script.onload = () => { - if (window.CulqiCheckout) { - resolve(window.CulqiCheckout); - } else { - reject( - new Error( - "Culqi script loaded but CulqiCheckout object not found" - ) - ); - } - }; - - script.onerror = () => { - reject(new Error("Failed to load CulqiCheckout script")); - }; - - document.head.appendChild(script); - }); + if (!CulqiCheckout) return; + const config = { + settings: { + currency: "PEN", + amount: total * 100, + }, + client: { + email: user?.email, + }, + options: { + paymentMethods: { + tarjeta: true, + yape: false, + }, + }, + appearance: {}, }; - - loadCulqiScript() - .then((CulqiCheckout) => { - const config = { - settings: { - currency: "PEN", - amount: total * 100, - }, - client: { - email: user?.email, + const publicKey = "pk_test_Ws4NXfH95QXlZgaz"; + const culqiInstance = new CulqiCheckout(publicKey, config); + const handleCulqiAction = () => { + if (culqiInstance.token) { + const token = culqiInstance.token.id; + culqiInstance.close(); + const formData = getValues(); + submit( + { + shippingDetailsJson: JSON.stringify(formData), + cartItemsJson: JSON.stringify(cart.items), + token, }, - options: { - paymentMethods: { - tarjeta: true, - yape: false, - }, - }, - appearance: {}, - }; - - const publicKey = "pk_test_Ws4NXfH95QXlZgaz"; - const culqiInstance = new CulqiCheckout(publicKey, config); - - const handleCulqiAction = () => { - if (culqiInstance.token) { - const token = culqiInstance.token.id; - culqiInstance.close(); - const formData = getValues(); - submit( - { - shippingDetailsJson: JSON.stringify(formData), - cartItemsJson: JSON.stringify(cart.items), - token, - }, - { method: "POST" } - ); - } else { - console.log("Error : ", culqiInstance.error); - } - }; - - culqiInstance.culqi = handleCulqiAction; - - setCulqui(culqiInstance); - }) - .catch((error) => { - console.error("Error loading Culqi script:", error); - }); - - return () => { - if (scriptRef.current) { - scriptRef.current.remove(); + { method: "POST" } + ); + } else { + console.log("Error : ", culqiInstance.error); } }; - }, [total, user, submit, getValues, cart.items]); + culqiInstance.culqi = handleCulqiAction; + setCulqui(culqiInstance); + // Limpieza no necesaria, el hook ya remueve el script + }, [CulqiCheckout, total, user, submit, getValues, cart.items]); async function onSubmit() { if (culqui) {