@ecomiq/storefront 1.1.9 → 2.0.0

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.
Files changed (134) hide show
  1. package/README.md +26 -418
  2. package/bin/ecomiq.mjs +154 -0
  3. package/package.json +72 -94
  4. package/src/api-proxy.ts +194 -0
  5. package/src/auth/create-storefront-auth.ts +35 -0
  6. package/src/auth/ecomiq-oauth-password-client.ts +26 -0
  7. package/src/auth/ecomiq-oauth-password-constants.ts +26 -0
  8. package/src/auth/ecomiq-oauth-password-plugin.ts +299 -0
  9. package/src/auth/ecomiq-oauth-password-types.ts +25 -0
  10. package/src/auth/ecomiq-storefront-sign-in.ts +47 -0
  11. package/src/auth/env.ts +7 -0
  12. package/src/cart-session.ts +241 -0
  13. package/src/client.ts +48 -0
  14. package/src/codegen.mjs +603 -0
  15. package/src/components/checkout/cart-sync.ts +143 -0
  16. package/src/components/checkout/checkout-external-update-banner.tsx +15 -0
  17. package/src/components/checkout/checkout-form-controls.tsx +139 -0
  18. package/src/components/checkout/checkout-line-items.tsx +43 -0
  19. package/src/components/checkout/checkout-order-summary.tsx +156 -0
  20. package/src/components/checkout/checkout-payment-section.tsx +215 -0
  21. package/src/components/checkout/checkout-session-view.tsx +240 -0
  22. package/src/components/checkout/checkout-shipping-address-section.tsx +266 -0
  23. package/src/components/checkout/checkout-shipping-section.tsx +90 -0
  24. package/src/components/checkout/checkout-states.tsx +73 -0
  25. package/src/components/checkout/form-schema.ts +229 -0
  26. package/src/components/checkout/format.ts +27 -0
  27. package/src/components/checkout/index.ts +51 -0
  28. package/src/components/checkout/types.ts +43 -0
  29. package/src/components/checkout/use-checkout-geo.ts +50 -0
  30. package/src/components/checkout/use-checkout-session.ts +583 -0
  31. package/src/components/hero.tsx +291 -0
  32. package/src/config.d.ts +69 -0
  33. package/src/config.mjs +165 -0
  34. package/src/index.ts +274 -0
  35. package/src/link.tsx +2 -0
  36. package/src/pages/checkout.$checkoutId.tsx +60 -0
  37. package/src/pages/checkout.tsx +31 -0
  38. package/src/pages/thank-you.tsx +53 -0
  39. package/src/providers/auth.tsx +69 -0
  40. package/src/providers/cart.tsx +275 -0
  41. package/src/providers/ecomiq.ts +137 -0
  42. package/src/providers/index.ts +26 -0
  43. package/src/public-config.ts +44 -0
  44. package/src/review-submission.ts +89 -0
  45. package/src/runtime/context.ts +1 -0
  46. package/src/runtime/default-layout.tsx +80 -0
  47. package/src/runtime/head.ts +126 -0
  48. package/src/runtime/page.tsx +82 -0
  49. package/src/runtime/postcss-browser-stub.mjs +5 -0
  50. package/src/server-config-store.ts +26 -0
  51. package/src/server.ts +279 -0
  52. package/src/styles.css +76 -0
  53. package/src/ui/badge.tsx +32 -0
  54. package/src/ui/button.tsx +31 -0
  55. package/src/ui/card.tsx +31 -0
  56. package/src/ui/carousel.tsx +218 -0
  57. package/src/ui/cn.ts +6 -0
  58. package/src/ui/index.ts +8 -0
  59. package/src/ui/separator.tsx +6 -0
  60. package/src/ui/skeleton.tsx +6 -0
  61. package/src/ui/storefront-error.tsx +49 -0
  62. package/src/utils.ts +36 -0
  63. package/src/vite-inline.mjs +95 -0
  64. package/templates/__root.tsx +77 -0
  65. package/templates/router.tsx +19 -0
  66. package/dist/chunk-64GLOWMJ.js +0 -216
  67. package/dist/chunk-64GLOWMJ.js.map +0 -1
  68. package/dist/chunk-AKD5IDEB.js +0 -177
  69. package/dist/chunk-AKD5IDEB.js.map +0 -1
  70. package/dist/chunk-EFEVJJWF.js +0 -1345
  71. package/dist/chunk-EFEVJJWF.js.map +0 -1
  72. package/dist/chunk-NAJQB5DN.js +0 -155
  73. package/dist/chunk-NAJQB5DN.js.map +0 -1
  74. package/dist/chunk-OABDGTBI.js +0 -253
  75. package/dist/chunk-OABDGTBI.js.map +0 -1
  76. package/dist/chunk-Q4U5OEUT.js +0 -21
  77. package/dist/chunk-Q4U5OEUT.js.map +0 -1
  78. package/dist/chunk-S7CPZ4UE.js +0 -6
  79. package/dist/chunk-S7CPZ4UE.js.map +0 -1
  80. package/dist/chunks/client.css +0 -984
  81. package/dist/chunks/client.css.map +0 -1
  82. package/dist/chunks/client.d.ts +0 -10759
  83. package/dist/chunks/client.js +0 -5398
  84. package/dist/chunks/client.js.map +0 -1
  85. package/dist/chunks/config.d.ts +0 -31
  86. package/dist/chunks/config.js +0 -5
  87. package/dist/chunks/config.js.map +0 -1
  88. package/dist/chunks/core-public.d.ts +0 -130
  89. package/dist/chunks/core-public.js +0 -92
  90. package/dist/chunks/core-public.js.map +0 -1
  91. package/dist/chunks/server-nextjs.d.ts +0 -33
  92. package/dist/chunks/server-nextjs.js +0 -59
  93. package/dist/chunks/server-nextjs.js.map +0 -1
  94. package/dist/chunks/server-tanstack.d.ts +0 -22
  95. package/dist/chunks/server-tanstack.js +0 -40
  96. package/dist/chunks/server-tanstack.js.map +0 -1
  97. package/dist/collection-DU7gREXS.d.ts +0 -4062
  98. package/dist/ecomiq-oauth-password-plugin-B_gMdGuc.d.ts +0 -3631
  99. package/dist/ecomiq-shell-ui-SWIFLQVM.js +0 -8
  100. package/dist/ecomiq-shell-ui-SWIFLQVM.js.map +0 -1
  101. package/dist/index.d.ts +0 -9
  102. package/dist/index.js +0 -1
  103. package/dist/index.js.map +0 -1
  104. package/dist/index.server.d.ts +0 -9
  105. package/dist/index.server.js +0 -8
  106. package/dist/index.server.js.map +0 -1
  107. package/dist/nextjs/api.d.ts +0 -45
  108. package/dist/nextjs/api.js +0 -32
  109. package/dist/nextjs/api.js.map +0 -1
  110. package/dist/nextjs/globals.css +0 -13
  111. package/dist/nextjs.d.ts +0 -2
  112. package/dist/nextjs.js +0 -2
  113. package/dist/nextjs.js.map +0 -1
  114. package/dist/nextjs.server.d.ts +0 -18
  115. package/dist/nextjs.server.js +0 -1
  116. package/dist/nextjs.server.js.map +0 -1
  117. package/dist/postcss.config.mjs +0 -6
  118. package/dist/react/checkout-page.css +0 -855
  119. package/dist/react/client.css +0 -984
  120. package/dist/react/payment-provider.css +0 -91
  121. package/dist/react/thank-you-page.css +0 -105
  122. package/dist/react.d.ts +0 -1
  123. package/dist/react.js +0 -2
  124. package/dist/react.js.map +0 -1
  125. package/dist/store-proxy-handler-CNzKlTFA.d.ts +0 -10
  126. package/dist/tanstack/api.d.ts +0 -41
  127. package/dist/tanstack/api.js +0 -30
  128. package/dist/tanstack/api.js.map +0 -1
  129. package/dist/tanstack.d.ts +0 -1
  130. package/dist/tanstack.js +0 -2
  131. package/dist/tanstack.js.map +0 -1
  132. package/dist/tanstack.server.d.ts +0 -18
  133. package/dist/tanstack.server.js +0 -1
  134. 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
+ }