@ecomiq/storefront 1.1.9 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -415
- package/bin/ecomiq.mjs +165 -0
- package/package.json +72 -94
- package/src/api-proxy.ts +194 -0
- package/src/auth/create-storefront-auth.ts +35 -0
- package/src/auth/ecomiq-oauth-password-client.ts +26 -0
- package/src/auth/ecomiq-oauth-password-constants.ts +26 -0
- package/src/auth/ecomiq-oauth-password-plugin.ts +299 -0
- package/src/auth/ecomiq-oauth-password-types.ts +25 -0
- package/src/auth/ecomiq-storefront-sign-in.ts +47 -0
- package/src/auth/env.ts +7 -0
- package/src/cart-session.ts +241 -0
- package/src/client.ts +48 -0
- package/src/codegen.mjs +704 -0
- package/src/components/checkout/cart-sync.ts +143 -0
- package/src/components/checkout/checkout-external-update-banner.tsx +15 -0
- package/src/components/checkout/checkout-form-controls.tsx +139 -0
- package/src/components/checkout/checkout-line-items.tsx +43 -0
- package/src/components/checkout/checkout-order-summary.tsx +156 -0
- package/src/components/checkout/checkout-payment-section.tsx +215 -0
- package/src/components/checkout/checkout-session-view.tsx +240 -0
- package/src/components/checkout/checkout-shipping-address-section.tsx +266 -0
- package/src/components/checkout/checkout-shipping-section.tsx +90 -0
- package/src/components/checkout/checkout-states.tsx +73 -0
- package/src/components/checkout/form-schema.ts +229 -0
- package/src/components/checkout/format.ts +27 -0
- package/src/components/checkout/index.ts +51 -0
- package/src/components/checkout/types.ts +43 -0
- package/src/components/checkout/use-checkout-geo.ts +50 -0
- package/src/components/checkout/use-checkout-session.ts +583 -0
- package/src/components/hero.tsx +291 -0
- package/src/config.d.ts +69 -0
- package/src/config.mjs +165 -0
- package/src/index.ts +274 -0
- package/src/link.tsx +2 -0
- package/src/pages/checkout.$checkoutId.tsx +60 -0
- package/src/pages/checkout.tsx +31 -0
- package/src/pages/thank-you.tsx +53 -0
- package/src/providers/auth.tsx +69 -0
- package/src/providers/cart.tsx +275 -0
- package/src/providers/ecomiq.ts +137 -0
- package/src/providers/index.ts +26 -0
- package/src/public-config.ts +44 -0
- package/src/review-submission.ts +89 -0
- package/src/runtime/context.ts +1 -0
- package/src/runtime/default-layout.tsx +80 -0
- package/src/runtime/head.ts +126 -0
- package/src/runtime/page.tsx +82 -0
- package/src/runtime/postcss-browser-stub.mjs +5 -0
- package/src/server-config-store.ts +26 -0
- package/src/server.ts +279 -0
- package/src/styles.css +76 -0
- package/src/ui/badge.tsx +32 -0
- package/src/ui/button.tsx +31 -0
- package/src/ui/card.tsx +31 -0
- package/src/ui/carousel.tsx +218 -0
- package/src/ui/cn.ts +6 -0
- package/src/ui/index.ts +8 -0
- package/src/ui/separator.tsx +6 -0
- package/src/ui/skeleton.tsx +6 -0
- package/src/ui/storefront-error.tsx +49 -0
- package/src/utils.ts +36 -0
- package/src/vite-inline.mjs +135 -0
- package/templates/__root.tsx +77 -0
- package/templates/router.tsx +19 -0
- package/dist/chunk-64GLOWMJ.js +0 -216
- package/dist/chunk-64GLOWMJ.js.map +0 -1
- package/dist/chunk-AKD5IDEB.js +0 -177
- package/dist/chunk-AKD5IDEB.js.map +0 -1
- package/dist/chunk-EFEVJJWF.js +0 -1345
- package/dist/chunk-EFEVJJWF.js.map +0 -1
- package/dist/chunk-NAJQB5DN.js +0 -155
- package/dist/chunk-NAJQB5DN.js.map +0 -1
- package/dist/chunk-OABDGTBI.js +0 -253
- package/dist/chunk-OABDGTBI.js.map +0 -1
- package/dist/chunk-Q4U5OEUT.js +0 -21
- package/dist/chunk-Q4U5OEUT.js.map +0 -1
- package/dist/chunk-S7CPZ4UE.js +0 -6
- package/dist/chunk-S7CPZ4UE.js.map +0 -1
- package/dist/chunks/client.css +0 -984
- package/dist/chunks/client.css.map +0 -1
- package/dist/chunks/client.d.ts +0 -10759
- package/dist/chunks/client.js +0 -5398
- package/dist/chunks/client.js.map +0 -1
- package/dist/chunks/config.d.ts +0 -31
- package/dist/chunks/config.js +0 -5
- package/dist/chunks/config.js.map +0 -1
- package/dist/chunks/core-public.d.ts +0 -130
- package/dist/chunks/core-public.js +0 -92
- package/dist/chunks/core-public.js.map +0 -1
- package/dist/chunks/server-nextjs.d.ts +0 -33
- package/dist/chunks/server-nextjs.js +0 -59
- package/dist/chunks/server-nextjs.js.map +0 -1
- package/dist/chunks/server-tanstack.d.ts +0 -22
- package/dist/chunks/server-tanstack.js +0 -40
- package/dist/chunks/server-tanstack.js.map +0 -1
- package/dist/collection-DU7gREXS.d.ts +0 -4062
- package/dist/ecomiq-oauth-password-plugin-B_gMdGuc.d.ts +0 -3631
- package/dist/ecomiq-shell-ui-SWIFLQVM.js +0 -8
- package/dist/ecomiq-shell-ui-SWIFLQVM.js.map +0 -1
- package/dist/index.d.ts +0 -9
- package/dist/index.js +0 -1
- package/dist/index.js.map +0 -1
- package/dist/index.server.d.ts +0 -9
- package/dist/index.server.js +0 -8
- package/dist/index.server.js.map +0 -1
- package/dist/nextjs/api.d.ts +0 -45
- package/dist/nextjs/api.js +0 -32
- package/dist/nextjs/api.js.map +0 -1
- package/dist/nextjs/globals.css +0 -13
- package/dist/nextjs.d.ts +0 -2
- package/dist/nextjs.js +0 -2
- package/dist/nextjs.js.map +0 -1
- package/dist/nextjs.server.d.ts +0 -18
- package/dist/nextjs.server.js +0 -1
- package/dist/nextjs.server.js.map +0 -1
- package/dist/postcss.config.mjs +0 -6
- package/dist/react/checkout-page.css +0 -855
- package/dist/react/client.css +0 -984
- package/dist/react/payment-provider.css +0 -91
- package/dist/react/thank-you-page.css +0 -105
- package/dist/react.d.ts +0 -1
- package/dist/react.js +0 -2
- package/dist/react.js.map +0 -1
- package/dist/store-proxy-handler-CNzKlTFA.d.ts +0 -10
- package/dist/tanstack/api.d.ts +0 -41
- package/dist/tanstack/api.js +0 -30
- package/dist/tanstack/api.js.map +0 -1
- package/dist/tanstack.d.ts +0 -1
- package/dist/tanstack.js +0 -2
- package/dist/tanstack.js.map +0 -1
- package/dist/tanstack.server.d.ts +0 -18
- package/dist/tanstack.server.js +0 -1
- package/dist/tanstack.server.js.map +0 -1
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Banknote,
|
|
3
|
+
CreditCard,
|
|
4
|
+
Landmark,
|
|
5
|
+
Wallet,
|
|
6
|
+
} from 'lucide-react'
|
|
7
|
+
import type { CartCheckoutResponse, CartPaymentMethod } from '@ecomiq/core'
|
|
8
|
+
import { Button, cn } from '../../ui'
|
|
9
|
+
import { methodUsesProviderCheckoutUi } from './cart-sync'
|
|
10
|
+
import type { CheckoutFieldId } from './form-schema'
|
|
11
|
+
import { FieldError } from './checkout-form-controls'
|
|
12
|
+
import { paymentRedirectUrl } from './format'
|
|
13
|
+
|
|
14
|
+
function paymentIcon(method: CartPaymentMethod) {
|
|
15
|
+
const key = `${method.kind} ${method.code} ${method.providerMethodCode ?? ''}`.toLowerCase()
|
|
16
|
+
if (key.includes('paypal')) return Wallet
|
|
17
|
+
if (key.includes('stripe') || key.includes('card') || key.includes('credit')) return CreditCard
|
|
18
|
+
if (key.includes('mercado')) return Wallet
|
|
19
|
+
if (method.isManual || key.includes('transfer') || key.includes('cash')) return Landmark
|
|
20
|
+
return Banknote
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function CheckoutPaymentSection({
|
|
24
|
+
methods,
|
|
25
|
+
selectedCode,
|
|
26
|
+
fieldError,
|
|
27
|
+
checkoutPaymentLoading,
|
|
28
|
+
checkoutPaymentError,
|
|
29
|
+
checkoutPaymentResponse,
|
|
30
|
+
checkoutPaymentMethodCode,
|
|
31
|
+
providerPaymentCompleted,
|
|
32
|
+
receiptUrl,
|
|
33
|
+
receiptError,
|
|
34
|
+
onSelect,
|
|
35
|
+
onClearError,
|
|
36
|
+
onMarkProviderCompleted,
|
|
37
|
+
onReceiptFileChange,
|
|
38
|
+
}: {
|
|
39
|
+
methods: CartPaymentMethod[]
|
|
40
|
+
selectedCode: string | null
|
|
41
|
+
fieldError?: string
|
|
42
|
+
checkoutPaymentLoading: boolean
|
|
43
|
+
checkoutPaymentError: string | null
|
|
44
|
+
checkoutPaymentResponse: CartCheckoutResponse | null
|
|
45
|
+
checkoutPaymentMethodCode: string | null
|
|
46
|
+
providerPaymentCompleted: boolean
|
|
47
|
+
receiptUrl: string | null
|
|
48
|
+
receiptError: string | null
|
|
49
|
+
onSelect: (code: string) => void
|
|
50
|
+
onClearError: (id: CheckoutFieldId) => void
|
|
51
|
+
onMarkProviderCompleted: () => void
|
|
52
|
+
onReceiptFileChange: (file: File | null) => void
|
|
53
|
+
}) {
|
|
54
|
+
const selectedMethod = methods.find((m) => m.code === selectedCode) ?? null
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<section className="space-y-4">
|
|
58
|
+
<h2 className="text-xl font-semibold tracking-tight">Seleccionar método de pago</h2>
|
|
59
|
+
<div className="space-y-3">
|
|
60
|
+
{methods.map((method) => {
|
|
61
|
+
const selected = selectedCode === method.code
|
|
62
|
+
const Icon = paymentIcon(method)
|
|
63
|
+
const usesProviderUi = methodUsesProviderCheckoutUi(method)
|
|
64
|
+
const showExpanded = selected
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div
|
|
68
|
+
key={method.code}
|
|
69
|
+
className={cn(
|
|
70
|
+
'overflow-hidden rounded-lg border transition-colors',
|
|
71
|
+
selected ? 'border-border bg-muted/40' : 'border-border bg-background',
|
|
72
|
+
)}
|
|
73
|
+
>
|
|
74
|
+
<label className="flex cursor-pointer items-center gap-3 px-4 py-3.5">
|
|
75
|
+
<input
|
|
76
|
+
type="radio"
|
|
77
|
+
name="payment"
|
|
78
|
+
className="size-4 accent-blue-600"
|
|
79
|
+
checked={selected}
|
|
80
|
+
onChange={() => {
|
|
81
|
+
onClearError('checkout-payment-method')
|
|
82
|
+
onSelect(method.code)
|
|
83
|
+
}}
|
|
84
|
+
/>
|
|
85
|
+
<span className="min-w-0 flex-1">
|
|
86
|
+
<span className="block text-sm font-medium">{method.name}</span>
|
|
87
|
+
{method.description ? (
|
|
88
|
+
<span className="mt-0.5 block text-xs text-muted-foreground">
|
|
89
|
+
{method.description}
|
|
90
|
+
</span>
|
|
91
|
+
) : null}
|
|
92
|
+
</span>
|
|
93
|
+
<Icon className="size-5 shrink-0 text-muted-foreground" aria-hidden="true" />
|
|
94
|
+
</label>
|
|
95
|
+
|
|
96
|
+
{showExpanded ? (
|
|
97
|
+
<div className="space-y-3 border-t px-4 py-4">
|
|
98
|
+
{usesProviderUi ? (
|
|
99
|
+
<ProviderPaymentPanel
|
|
100
|
+
loading={
|
|
101
|
+
checkoutPaymentMethodCode === method.code && checkoutPaymentLoading
|
|
102
|
+
}
|
|
103
|
+
error={
|
|
104
|
+
checkoutPaymentMethodCode === method.code
|
|
105
|
+
? checkoutPaymentError
|
|
106
|
+
: null
|
|
107
|
+
}
|
|
108
|
+
response={
|
|
109
|
+
checkoutPaymentMethodCode === method.code
|
|
110
|
+
? checkoutPaymentResponse
|
|
111
|
+
: null
|
|
112
|
+
}
|
|
113
|
+
completed={providerPaymentCompleted}
|
|
114
|
+
onMarkCompleted={onMarkProviderCompleted}
|
|
115
|
+
/>
|
|
116
|
+
) : null}
|
|
117
|
+
|
|
118
|
+
{method.isManual ? (
|
|
119
|
+
<div className="space-y-2">
|
|
120
|
+
<p className="text-sm font-medium">Comprobante de pago</p>
|
|
121
|
+
<input
|
|
122
|
+
type="file"
|
|
123
|
+
accept="image/*,application/pdf"
|
|
124
|
+
className="block w-full text-sm"
|
|
125
|
+
onChange={(e) =>
|
|
126
|
+
onReceiptFileChange(e.target.files?.[0] ?? null)
|
|
127
|
+
}
|
|
128
|
+
/>
|
|
129
|
+
{receiptError ? (
|
|
130
|
+
<p className="text-xs text-destructive">{receiptError}</p>
|
|
131
|
+
) : null}
|
|
132
|
+
{receiptUrl ? (
|
|
133
|
+
<p className="text-xs text-muted-foreground">Comprobante listo</p>
|
|
134
|
+
) : null}
|
|
135
|
+
</div>
|
|
136
|
+
) : null}
|
|
137
|
+
|
|
138
|
+
{!usesProviderUi && !method.isManual ? (
|
|
139
|
+
<p className="text-xs text-muted-foreground">
|
|
140
|
+
Continúa con Pay now para confirmar este método de pago.
|
|
141
|
+
</p>
|
|
142
|
+
) : null}
|
|
143
|
+
</div>
|
|
144
|
+
) : null}
|
|
145
|
+
</div>
|
|
146
|
+
)
|
|
147
|
+
})}
|
|
148
|
+
</div>
|
|
149
|
+
<FieldError message={fieldError} />
|
|
150
|
+
</section>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function ProviderPaymentPanel({
|
|
155
|
+
loading,
|
|
156
|
+
error,
|
|
157
|
+
response,
|
|
158
|
+
completed,
|
|
159
|
+
onMarkCompleted,
|
|
160
|
+
}: {
|
|
161
|
+
loading: boolean
|
|
162
|
+
error: string | null
|
|
163
|
+
response: CartCheckoutResponse | null
|
|
164
|
+
completed: boolean
|
|
165
|
+
onMarkCompleted: () => void
|
|
166
|
+
}) {
|
|
167
|
+
if (loading) {
|
|
168
|
+
return <p className="text-sm text-muted-foreground">Preparando pasarela de pago…</p>
|
|
169
|
+
}
|
|
170
|
+
if (error) {
|
|
171
|
+
return <p className="text-sm text-destructive">{error}</p>
|
|
172
|
+
}
|
|
173
|
+
if (!response) {
|
|
174
|
+
return (
|
|
175
|
+
<p className="text-xs text-muted-foreground">
|
|
176
|
+
Completa el pago de forma segura con el proveedor seleccionado.
|
|
177
|
+
</p>
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const url = paymentRedirectUrl(response)
|
|
182
|
+
const clientSecret =
|
|
183
|
+
response.clientSecret ??
|
|
184
|
+
response.intentClientSecret ??
|
|
185
|
+
response.payment?.action?.clientSecret
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<div className="space-y-3 text-sm">
|
|
189
|
+
{url ? (
|
|
190
|
+
<a
|
|
191
|
+
href={url}
|
|
192
|
+
target="_blank"
|
|
193
|
+
rel="noreferrer"
|
|
194
|
+
className="font-medium text-primary underline-offset-4 hover:underline"
|
|
195
|
+
>
|
|
196
|
+
Abrir pasarela de pago
|
|
197
|
+
</a>
|
|
198
|
+
) : null}
|
|
199
|
+
{clientSecret && !url ? (
|
|
200
|
+
<p className="text-muted-foreground">
|
|
201
|
+
Sesión de pago lista. Completa el cobro en la pasarela cuando esté disponible.
|
|
202
|
+
</p>
|
|
203
|
+
) : null}
|
|
204
|
+
{completed ? (
|
|
205
|
+
<p className="text-xs text-muted-foreground">
|
|
206
|
+
Pago confirmado. Pulsa Pay now para finalizar tu pedido.
|
|
207
|
+
</p>
|
|
208
|
+
) : (
|
|
209
|
+
<Button type="button" size="sm" variant="outline" onClick={onMarkCompleted}>
|
|
210
|
+
Ya completé el pago en la pasarela
|
|
211
|
+
</Button>
|
|
212
|
+
)}
|
|
213
|
+
</div>
|
|
214
|
+
)
|
|
215
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { useState, type FormEvent } from 'react'
|
|
2
|
+
import { ArrowLeft, Lock } from 'lucide-react'
|
|
3
|
+
import type { CartCheckoutResponse } from '@ecomiq/core'
|
|
4
|
+
import { Link } from '../../link'
|
|
5
|
+
import { useAuth, useSettings } from '../../providers'
|
|
6
|
+
import { buttonVariants } from '../../ui'
|
|
7
|
+
import { CheckoutExternalUpdateBanner } from './checkout-external-update-banner'
|
|
8
|
+
import { CheckoutOrderBumps } from './checkout-line-items'
|
|
9
|
+
import { CheckoutOrderSummary } from './checkout-order-summary'
|
|
10
|
+
import { CheckoutPaymentSection } from './checkout-payment-section'
|
|
11
|
+
import { CheckoutShippingAddressSection } from './checkout-shipping-address-section'
|
|
12
|
+
import { CheckoutShippingSection } from './checkout-shipping-section'
|
|
13
|
+
import {
|
|
14
|
+
CheckoutAlreadyPurchasedState,
|
|
15
|
+
CheckoutEmptyState,
|
|
16
|
+
CheckoutErrorState,
|
|
17
|
+
CheckoutLoadingState,
|
|
18
|
+
} from './checkout-states'
|
|
19
|
+
import { formatMoney, paymentRedirectUrl, thankYouUrl } from './format'
|
|
20
|
+
import {
|
|
21
|
+
buildCheckoutFormValidationSchema,
|
|
22
|
+
checkoutValidationInputFromContext,
|
|
23
|
+
firstInvalidFieldId,
|
|
24
|
+
focusCheckoutField,
|
|
25
|
+
mapZodIssuesToFieldErrors,
|
|
26
|
+
type CheckoutFieldId,
|
|
27
|
+
} from './form-schema'
|
|
28
|
+
import type { CheckoutContactForm } from './types'
|
|
29
|
+
import { useCheckoutGeo } from './use-checkout-geo'
|
|
30
|
+
import { useCheckoutSession } from './use-checkout-session'
|
|
31
|
+
|
|
32
|
+
export function CheckoutSessionView({ cartId }: { cartId: string }) {
|
|
33
|
+
const { store } = useSettings()
|
|
34
|
+
const { session } = useAuth()
|
|
35
|
+
const checkout = useCheckoutSession(cartId)
|
|
36
|
+
const [fieldErrors, setFieldErrors] = useState<
|
|
37
|
+
Partial<Record<CheckoutFieldId, string>>
|
|
38
|
+
>({})
|
|
39
|
+
const [receiptFile, setReceiptFile] = useState<File | null>(null)
|
|
40
|
+
const [receiptUrl, setReceiptUrl] = useState<string | null>(null)
|
|
41
|
+
const [receiptError, setReceiptError] = useState<string | null>(null)
|
|
42
|
+
|
|
43
|
+
const geo = useCheckoutGeo(
|
|
44
|
+
checkout.contact.countryCode,
|
|
45
|
+
checkout.contact.departmentCode,
|
|
46
|
+
checkout.contact.provinceCode,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if (checkout.loadStatus === 'loading' || checkout.isReconciling) {
|
|
50
|
+
return <CheckoutLoadingState />
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (checkout.checkoutAlreadyPurchased) {
|
|
54
|
+
return <CheckoutAlreadyPurchasedState />
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (checkout.loadStatus === 'error') {
|
|
58
|
+
return (
|
|
59
|
+
<CheckoutErrorState
|
|
60
|
+
description={checkout.loadError}
|
|
61
|
+
onRetry={() => checkout.reloadCheckout()}
|
|
62
|
+
/>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const cart = checkout.uiCart
|
|
67
|
+
if (!cart || cart.items.length === 0) {
|
|
68
|
+
return <CheckoutEmptyState />
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const money = (value: number) => formatMoney(value, store.language, cart.currencyCode)
|
|
72
|
+
const selectedMethod =
|
|
73
|
+
checkout.paymentMethods.find((m) => m.code === checkout.selectedPaymentCode) ?? null
|
|
74
|
+
const isManual = Boolean(selectedMethod?.isManual)
|
|
75
|
+
|
|
76
|
+
const clearFieldError = (id: CheckoutFieldId) => {
|
|
77
|
+
setFieldErrors((prev) => {
|
|
78
|
+
if (!prev[id]) return prev
|
|
79
|
+
const next = { ...prev }
|
|
80
|
+
delete next[id]
|
|
81
|
+
return next
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const patchContact = (patch: Partial<CheckoutContactForm>) => {
|
|
86
|
+
checkout.setContact((c) => ({ ...c, ...patch }))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const handlePay = async (event: FormEvent) => {
|
|
90
|
+
event.preventDefault()
|
|
91
|
+
const schema = buildCheckoutFormValidationSchema(store.currency)
|
|
92
|
+
const parsed = schema.safeParse(
|
|
93
|
+
checkoutValidationInputFromContext({
|
|
94
|
+
contact: checkout.contact,
|
|
95
|
+
selectedShippingKey: checkout.selectedShippingKey,
|
|
96
|
+
shippingOptionsCount: checkout.shippingFlat.length,
|
|
97
|
+
selectedPaymentCode: checkout.selectedPaymentCode,
|
|
98
|
+
paymentMethodsCount: checkout.paymentMethods.length,
|
|
99
|
+
}),
|
|
100
|
+
)
|
|
101
|
+
if (!parsed.success) {
|
|
102
|
+
const errors = mapZodIssuesToFieldErrors(parsed.error)
|
|
103
|
+
setFieldErrors(errors)
|
|
104
|
+
const first = firstInvalidFieldId(errors)
|
|
105
|
+
if (first) focusCheckoutField(first)
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
setFieldErrors({})
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
let uploadedReceipt = receiptUrl ?? undefined
|
|
112
|
+
if (isManual && receiptFile && !uploadedReceipt) {
|
|
113
|
+
const uploaded = await checkout.uploadPaymentReceipt(receiptFile)
|
|
114
|
+
uploadedReceipt = uploaded.url
|
|
115
|
+
setReceiptUrl(uploaded.url)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const result: CartCheckoutResponse | undefined = await checkout.payNow(
|
|
119
|
+
uploadedReceipt ? { receiptUrl: uploadedReceipt } : undefined,
|
|
120
|
+
)
|
|
121
|
+
if (!result) return
|
|
122
|
+
|
|
123
|
+
checkout.clearCart()
|
|
124
|
+
const redirect = paymentRedirectUrl(result)
|
|
125
|
+
if (redirect) {
|
|
126
|
+
window.location.assign(redirect)
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
window.location.assign(
|
|
130
|
+
thankYouUrl(cartId, result.payment?.transactionId ?? undefined),
|
|
131
|
+
)
|
|
132
|
+
} catch {
|
|
133
|
+
/* formError en sesión */
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<div className="mx-auto w-full max-w-6xl px-4 py-8 sm:py-10">
|
|
139
|
+
{checkout.cartChangedExternally ? (
|
|
140
|
+
<CheckoutExternalUpdateBanner onReload={checkout.reloadCheckout} />
|
|
141
|
+
) : null}
|
|
142
|
+
|
|
143
|
+
<header className="mb-8 flex flex-wrap items-end justify-between gap-4 border-b pb-6">
|
|
144
|
+
<div>
|
|
145
|
+
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
|
146
|
+
Compra
|
|
147
|
+
</p>
|
|
148
|
+
<h1 className="mt-2 text-3xl font-semibold tracking-tight sm:text-4xl">Checkout</h1>
|
|
149
|
+
</div>
|
|
150
|
+
<Link to="/cart" className={buttonVariants({ variant: 'ghost' })}>
|
|
151
|
+
<ArrowLeft aria-hidden="true" />
|
|
152
|
+
Volver al carrito
|
|
153
|
+
</Link>
|
|
154
|
+
</header>
|
|
155
|
+
|
|
156
|
+
<form
|
|
157
|
+
onSubmit={(e) => void handlePay(e)}
|
|
158
|
+
className="grid items-start gap-8 lg:grid-cols-[minmax(0,1fr)_22rem] lg:gap-10"
|
|
159
|
+
>
|
|
160
|
+
<div className="space-y-10">
|
|
161
|
+
<CheckoutShippingAddressSection
|
|
162
|
+
contact={checkout.contact}
|
|
163
|
+
fieldErrors={fieldErrors}
|
|
164
|
+
countries={geo.countries}
|
|
165
|
+
departments={geo.departments}
|
|
166
|
+
provinces={geo.provinces}
|
|
167
|
+
districts={geo.districts}
|
|
168
|
+
sessionLabel={session?.user?.email ?? session?.user?.name}
|
|
169
|
+
onPatch={patchContact}
|
|
170
|
+
onClearError={clearFieldError}
|
|
171
|
+
/>
|
|
172
|
+
|
|
173
|
+
<CheckoutShippingSection
|
|
174
|
+
options={checkout.shippingFlat}
|
|
175
|
+
selectedKey={checkout.selectedShippingKey}
|
|
176
|
+
loading={checkout.shippingLoading}
|
|
177
|
+
error={checkout.shippingError}
|
|
178
|
+
needsPostalCode={!checkout.contact.postalCode.trim()}
|
|
179
|
+
fieldError={fieldErrors['checkout-shipping-method']}
|
|
180
|
+
money={money}
|
|
181
|
+
onSelect={checkout.setSelectedShippingKey}
|
|
182
|
+
onClearError={clearFieldError}
|
|
183
|
+
/>
|
|
184
|
+
|
|
185
|
+
<CheckoutPaymentSection
|
|
186
|
+
methods={checkout.paymentMethods}
|
|
187
|
+
selectedCode={checkout.selectedPaymentCode}
|
|
188
|
+
fieldError={fieldErrors['checkout-payment-method']}
|
|
189
|
+
checkoutPaymentLoading={checkout.checkoutPaymentLoading}
|
|
190
|
+
checkoutPaymentError={checkout.checkoutPaymentError}
|
|
191
|
+
checkoutPaymentResponse={checkout.checkoutPaymentResponse}
|
|
192
|
+
checkoutPaymentMethodCode={checkout.checkoutPaymentMethodCode}
|
|
193
|
+
providerPaymentCompleted={checkout.providerPaymentCompleted}
|
|
194
|
+
receiptUrl={receiptUrl}
|
|
195
|
+
receiptError={receiptError}
|
|
196
|
+
onSelect={checkout.selectPaymentMethod}
|
|
197
|
+
onClearError={clearFieldError}
|
|
198
|
+
onMarkProviderCompleted={() => checkout.setProviderPaymentCompleted(true)}
|
|
199
|
+
onReceiptFileChange={(file) => {
|
|
200
|
+
setReceiptFile(file)
|
|
201
|
+
setReceiptUrl(null)
|
|
202
|
+
setReceiptError(null)
|
|
203
|
+
}}
|
|
204
|
+
/>
|
|
205
|
+
|
|
206
|
+
<CheckoutOrderBumps
|
|
207
|
+
bumps={checkout.orderBumps}
|
|
208
|
+
money={money}
|
|
209
|
+
busy={checkout.isSaving}
|
|
210
|
+
onAdd={(bump) => void checkout.handleAddOrderBump(bump)}
|
|
211
|
+
/>
|
|
212
|
+
|
|
213
|
+
<p className="flex items-start gap-2 text-xs leading-5 text-muted-foreground">
|
|
214
|
+
<Lock className="mt-0.5 size-3.5 shrink-0" aria-hidden="true" />
|
|
215
|
+
Tus datos se usan solo para procesar este pedido.
|
|
216
|
+
</p>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
<CheckoutOrderSummary
|
|
220
|
+
items={cart.items}
|
|
221
|
+
money={money}
|
|
222
|
+
subtotal={checkout.subtotal}
|
|
223
|
+
discount={checkout.discount}
|
|
224
|
+
shipping={checkout.shipping}
|
|
225
|
+
tax={checkout.tax}
|
|
226
|
+
total={checkout.total}
|
|
227
|
+
hasShippingOptions={checkout.shippingFlat.length > 0}
|
|
228
|
+
couponCode={checkout.couponCode}
|
|
229
|
+
couponError={checkout.couponError}
|
|
230
|
+
promotionCodes={cart.promotionCodes}
|
|
231
|
+
formError={checkout.formError}
|
|
232
|
+
isSaving={checkout.isSaving}
|
|
233
|
+
onCouponCodeChange={checkout.setCouponCode}
|
|
234
|
+
onApplyCoupon={() => void checkout.handleApplyCoupon()}
|
|
235
|
+
onRemoveCoupon={(code) => void checkout.handleRemoveCoupon(code)}
|
|
236
|
+
/>
|
|
237
|
+
</form>
|
|
238
|
+
</div>
|
|
239
|
+
)
|
|
240
|
+
}
|