11import { zodResolver } from "@hookform/resolvers/zod" ;
22import { X } from "lucide-react" ;
3- import { useEffect , useRef , useState } from "react" ;
3+ import { useEffect , useState } from "react" ;
44import { useForm } from "react-hook-form" ;
55import { redirect , useNavigation , useSubmit } from "react-router" ;
66import { z } from "zod" ;
@@ -13,6 +13,11 @@ import {
1313 Separator ,
1414 SelectField ,
1515} from "@/components/ui" ;
16+ import {
17+ useCulqi ,
18+ type CulqiChargeError ,
19+ type CulqiInstance ,
20+ } from "@/hooks/use-culqui" ;
1621import { calculateTotal , getCart } from "@/lib/cart" ;
1722import { type CartItem } from "@/models/cart.model" ;
1823import { getCurrentUser } from "@/services/auth.service" ;
@@ -22,20 +27,6 @@ import { commitSession, getSession } from "@/session.server";
2227
2328import type { Route } from "./+types" ;
2429
25- interface CulqiInstance {
26- open : ( ) => void ;
27- close : ( ) => void ;
28- token ?: { id : string } ;
29- error ?: Error ;
30- culqi ?: ( ) => void ;
31- }
32-
33- declare global {
34- interface Window {
35- CulqiCheckout : new ( publicKey : string , config : object ) => CulqiInstance ;
36- }
37- }
38-
3930const countryOptions = [
4031 { value : "AR" , label : "Argentina" } ,
4132 { value : "BO" , label : "Bolivia" } ,
@@ -85,8 +76,10 @@ export async function action({ request }: Route.ActionArgs) {
8576 ) as CartItem [ ] ;
8677 const token = formData . get ( "token" ) as string ;
8778
79+ const total = Math . round ( calculateTotal ( cartItems ) * 100 ) ;
80+
8881 const body = {
89- amount : 2000 , // TODO: Calculate total dynamically
82+ amount : total ,
9083 currency_code : "PEN" ,
9184 email : shippingDetails . email ,
9285 source_id : token ,
@@ -97,18 +90,19 @@ export async function action({ request }: Route.ActionArgs) {
9790 method : "POST" ,
9891 headers : {
9992 "content-type" : "application/json" ,
100- Authorization : `Bearer sk_test_EC8oOLd3ZiCTKqjN` , // TODO: Use environment variable
93+ Authorization : `Bearer ${ process . env . CULQI_PRIVATE_KEY } ` ,
10194 } ,
10295 body : JSON . stringify ( body ) ,
10396 } ) ;
10497
10598 if ( ! response . ok ) {
106- const errorData = await response . json ( ) ;
99+ const errorData = ( await response . json ( ) ) as CulqiChargeError ;
107100 console . error ( "Error creating charge:" , errorData ) ;
108- // TODO: Handle error appropriately
109- throw new Error ( "Error processing payment" ) ;
101+ return { error : errorData . user_message || "Error processing payment" } ;
110102 }
111103
104+ const chargeData = await response . json ( ) ;
105+
112106 const items = cartItems . map ( ( item ) => ( {
113107 productId : item . product . id ,
114108 quantity : item . quantity ,
@@ -117,9 +111,13 @@ export async function action({ request }: Route.ActionArgs) {
117111 imgSrc : item . product . imgSrc ,
118112 } ) ) ;
119113
120- // TODO
121- // @ts -expect-error Arreglar el tipo de shippingDetails
122- const { id : orderId } = await createOrder ( items , shippingDetails ) ; // TODO: Add payment information to the order
114+ const { id : orderId } = await createOrder (
115+ items ,
116+ // TODO
117+ // @ts -expect-error Arreglar el tipo de shippingDetails
118+ shippingDetails ,
119+ chargeData . id
120+ ) ;
123121
124122 await deleteRemoteCart ( request ) ;
125123 const session = await getSession ( request . headers . get ( "Cookie" ) ) ;
@@ -151,14 +149,18 @@ export async function loader({ request }: Route.LoaderArgs) {
151149 return user ? { user, cart, total } : { cart, total } ;
152150}
153151
154- export default function Checkout ( { loaderData } : Route . ComponentProps ) {
152+ export default function Checkout ( {
153+ loaderData,
154+ actionData,
155+ } : Route . ComponentProps ) {
155156 const { user, cart, total } = loaderData ;
156157 const navigation = useNavigation ( ) ;
157158 const submit = useSubmit ( ) ;
158159 const loading = navigation . state === "submitting" ;
160+ const paymentError = actionData ?. error ;
159161
160162 const [ culqui , setCulqui ] = useState < CulqiInstance | null > ( null ) ;
161- const scriptRef = useRef < HTMLScriptElement | null > ( null ) ;
163+ const { CulqiCheckout } = useCulqi ( ) ;
162164
163165 const {
164166 register,
@@ -183,96 +185,56 @@ export default function Checkout({ loaderData }: Route.ComponentProps) {
183185 } ) ;
184186
185187 useEffect ( ( ) => {
186- // Function to load the Culqi script
187- const loadCulqiScript = ( ) : Promise < Window [ "CulqiCheckout" ] > => {
188- return new Promise < Window [ "CulqiCheckout" ] > ( ( resolve , reject ) => {
189- if ( window . CulqiCheckout ) {
190- resolve ( window . CulqiCheckout ) ;
191- return ;
192- }
193-
194- // Create script element
195- const script = document . createElement ( "script" ) ;
196- script . src = "https://js.culqi.com/checkout-js" ;
197- script . async = true ;
198-
199- // Store reference for cleanup
200- scriptRef . current = script ;
201-
202- script . onload = ( ) => {
203- if ( window . CulqiCheckout ) {
204- resolve ( window . CulqiCheckout ) ;
205- } else {
206- reject (
207- new Error (
208- "Culqi script loaded but CulqiCheckout object not found"
209- )
210- ) ;
211- }
212- } ;
213-
214- script . onerror = ( ) => {
215- reject ( new Error ( "Failed to load CulqiCheckout script" ) ) ;
216- } ;
217-
218- document . head . appendChild ( script ) ;
219- } ) ;
188+ if ( ! CulqiCheckout ) return ;
189+
190+ const config = {
191+ settings : {
192+ currency : "PEN" ,
193+ amount : Math . round ( total * 100 ) ,
194+ } ,
195+ client : {
196+ email : user ?. email ,
197+ } ,
198+ options : {
199+ paymentMethods : {
200+ tarjeta : true ,
201+ yape : false ,
202+ } ,
203+ } ,
204+ appearance : { } ,
220205 } ;
221206
222- loadCulqiScript ( )
223- . then ( ( CulqiCheckout ) => {
224- const config = {
225- settings : {
226- currency : "PEN" ,
227- amount : total * 100 ,
228- } ,
229- client : {
230- email : user ?. email ,
231- } ,
232- options : {
233- paymentMethods : {
234- tarjeta : true ,
235- yape : false ,
236- } ,
207+ const culqiInstance = new CulqiCheckout (
208+ import . meta . env . VITE_CULQI_PUBLIC_KEY as string ,
209+ config
210+ ) ;
211+
212+ culqiInstance . culqi = ( ) => {
213+ if ( culqiInstance . token ) {
214+ const token = culqiInstance . token . id ;
215+ culqiInstance . close ( ) ;
216+ const formData = getValues ( ) ;
217+ submit (
218+ {
219+ shippingDetailsJson : JSON . stringify ( formData ) ,
220+ cartItemsJson : JSON . stringify ( cart . items ) ,
221+ token ,
237222 } ,
238- appearance : { } ,
239- } ;
240-
241- const publicKey = "pk_test_Ws4NXfH95QXlZgaz" ;
242- const culqiInstance = new CulqiCheckout ( publicKey , config ) ;
243-
244- const handleCulqiAction = ( ) => {
245- if ( culqiInstance . token ) {
246- const token = culqiInstance . token . id ;
247- culqiInstance . close ( ) ;
248- const formData = getValues ( ) ;
249- submit (
250- {
251- shippingDetailsJson : JSON . stringify ( formData ) ,
252- cartItemsJson : JSON . stringify ( cart . items ) ,
253- token,
254- } ,
255- { method : "POST" }
256- ) ;
257- } else {
258- console . log ( "Error : " , culqiInstance . error ) ;
259- }
260- } ;
261-
262- culqiInstance . culqi = handleCulqiAction ;
223+ { method : "POST" }
224+ ) ;
225+ } else {
226+ console . log ( "Error : " , culqiInstance . error ) ;
227+ }
228+ } ;
263229
264- setCulqui ( culqiInstance ) ;
265- } )
266- . catch ( ( error ) => {
267- console . error ( "Error loading Culqi script:" , error ) ;
268- } ) ;
230+ setCulqui ( culqiInstance ) ;
269231
270232 return ( ) => {
271- if ( scriptRef . current ) {
272- scriptRef . current . remove ( ) ;
233+ if ( culqiInstance ) {
234+ culqiInstance . close ( ) ;
273235 }
274236 } ;
275- } , [ total , user , submit , getValues , cart . items ] ) ;
237+ } , [ total , user , submit , getValues , cart . items , CulqiCheckout ] ) ;
276238
277239 async function onSubmit ( ) {
278240 if ( culqui ) {
@@ -397,12 +359,18 @@ export default function Checkout({ loaderData }: Route.ComponentProps) {
397359 />
398360 </ div >
399361 </ fieldset >
400- < Button size = "xl" className = "w-full mt-6" disabled = { ! isValid } >
362+ < Button
363+ size = "xl"
364+ className = "w-full mt-6"
365+ disabled = { ! isValid || ! CulqiCheckout || loading }
366+ >
401367 { loading ? "Procesando..." : "Confirmar Orden" }
402368 </ Button >
369+ { paymentError && (
370+ < p className = "text-red-500 mt-4 text-center" > { paymentError } </ p >
371+ ) }
403372 </ form >
404373 </ div >
405- < div id = "culqi-container" > </ div >
406374 </ Container >
407375 </ Section >
408376 ) ;
0 commit comments