@thorprovider/blocks 1.0.0 → 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.
- package/CHANGELOG.md +48 -0
- package/README.md +156 -40
- package/package.json +1 -1
- package/src/blocks/application/login-forms/login-page.test.tsx +190 -0
- package/src/blocks/application/login-forms/login-page.tsx +240 -0
- package/src/blocks/application/password-recovery/forgot-password-page-1.tsx +6 -8
- package/src/blocks/application/signup-forms/signup-page-1.tsx +127 -129
- package/src/blocks/application/verification/verify-1.tsx +2 -2
- package/src/blocks/application/viewport-ownership.test.tsx +41 -0
- package/src/blocks/e-commerce/category-filters/category-listing-filter-fields.tsx +218 -0
- package/src/blocks/e-commerce/category-filters/{category-filter-2.test.tsx → category-listing.test.tsx} +287 -199
- package/src/blocks/e-commerce/category-filters/category-listing.tsx +934 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-contact-step.tsx +149 -0
- package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-country-field.tsx → checkout-country-field.tsx} +10 -8
- package/src/blocks/e-commerce/checkout-forms/checkout-order-summary.tsx +230 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-payment-step.tsx +100 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-progress.tsx +62 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-review-step.tsx +59 -0
- package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-shipping-step.tsx → checkout-shipping-step.tsx} +65 -41
- package/src/blocks/e-commerce/checkout-forms/checkout.test.tsx +478 -0
- package/src/blocks/e-commerce/checkout-forms/checkout.tsx +550 -0
- package/src/blocks/{marketing/navbars/navbar-4-account-menu.test.tsx → e-commerce/navigation/storefront-navbar-account-menu.test.tsx} +8 -8
- package/src/blocks/{marketing/navbars/navbar-4-account-menu.tsx → e-commerce/navigation/storefront-navbar-account-menu.tsx} +3 -3
- package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.test.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.test.tsx} +22 -22
- package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.tsx} +3 -3
- package/src/blocks/{marketing/navbars/navbar-4-mobile-nav.tsx → e-commerce/navigation/storefront-navbar-mobile-nav.tsx} +4 -12
- package/src/blocks/{marketing/navbars/navbar-4-search-bar.tsx → e-commerce/navigation/storefront-navbar-search-bar.tsx} +3 -3
- package/src/blocks/{marketing/navbars/navbar-4.test.tsx → e-commerce/navigation/storefront-navbar.test.tsx} +40 -40
- package/src/blocks/{marketing/navbars/navbar-4.tsx → e-commerce/navigation/storefront-navbar.tsx} +17 -17
- package/src/blocks/e-commerce/product-list/product-card.tsx +63 -40
- package/src/blocks/e-commerce/shopping-carts/shopping-cart.test.tsx +2 -2
- package/src/blocks/marketing/footers/footer-section-accessibility.test.tsx +118 -0
- package/src/blocks/marketing/footers/footer-section-actions.test.tsx +131 -0
- package/src/blocks/marketing/footers/footer-section-data.test.tsx +127 -0
- package/src/blocks/marketing/footers/footer-section-native-links.test.tsx +100 -0
- package/src/blocks/marketing/footers/footer-section-public-api.test.tsx +11 -0
- package/src/blocks/marketing/footers/footer-section.test.tsx +46 -0
- package/src/blocks/marketing/footers/footer-section.tsx +532 -0
- package/src/blocks/marketing/navbars/navbar-public-api.test.tsx +57 -0
- package/src/blocks/marketing/navbars/navbar.mega.test.tsx +149 -0
- package/src/blocks/marketing/navbars/navbar.test.tsx +155 -0
- package/src/blocks/marketing/navbars/navbar.tsx +735 -0
- package/src/blocks/marketing/testimonials/testimonial-section.test.tsx +171 -0
- package/src/blocks/marketing/testimonials/testimonial-section.tsx +237 -0
- package/src/index.ts +80 -81
- package/src/lib/contracts-curated.test.ts +5 -5
- package/src/lib/contracts.test.ts +55 -0
- package/src/lib/contracts.ts +64 -2
- package/tsup.config.ts +1 -1
- package/src/blocks/application/login-forms/login-page-1.tsx +0 -246
- package/src/blocks/application/login-forms/login-page-2.tsx +0 -207
- package/src/blocks/e-commerce/category-filters/category-filter-1.tsx +0 -260
- package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +0 -222
- package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +0 -921
- package/src/blocks/e-commerce/category-filters/category-filter-4-chip-rail.tsx +0 -57
- package/src/blocks/e-commerce/category-filters/category-filter-4-drawer.tsx +0 -124
- package/src/blocks/e-commerce/category-filters/category-filter-4-fields.tsx +0 -170
- package/src/blocks/e-commerce/category-filters/category-filter-4-sidebar.tsx +0 -52
- package/src/blocks/e-commerce/category-filters/category-filter-4-sort-sheet.tsx +0 -57
- package/src/blocks/e-commerce/category-filters/category-filter-4-state.ts +0 -68
- package/src/blocks/e-commerce/category-filters/category-filter-4.tsx +0 -298
- package/src/blocks/e-commerce/checkout-forms/card-formatting.test.ts +0 -47
- package/src/blocks/e-commerce/checkout-forms/card-formatting.ts +0 -9
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-contact-step.tsx +0 -96
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +0 -252
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +0 -188
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-progress.tsx +0 -36
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1.test.tsx +0 -216
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +0 -338
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-order-summary.tsx +0 -130
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-payment-card.tsx +0 -175
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-shipping-card.tsx +0 -190
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2.tsx +0 -312
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-information-step.tsx +0 -91
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-order-summary.tsx +0 -110
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-payment-step.tsx +0 -137
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-progress.tsx +0 -68
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-review-step.tsx +0 -79
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-shipping-step.tsx +0 -172
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3.tsx +0 -392
- package/src/blocks/marketing/footers/footer-section-1.test.tsx +0 -117
- package/src/blocks/marketing/footers/footer-section-1.tsx +0 -178
- package/src/blocks/marketing/footers/footer-section-3.tsx +0 -192
- package/src/blocks/marketing/footers/footer-section-4.tsx +0 -355
- package/src/blocks/marketing/navbars/navbar-1.tsx +0 -267
- package/src/blocks/marketing/navbars/navbar-2.tsx +0 -351
- package/src/blocks/marketing/navbars/navbar-3.tsx +0 -303
- package/src/blocks/marketing/navbars/navbar-4-like-button.tsx +0 -177
- package/src/blocks/marketing/navbars/navbar-4-product-grid.tsx +0 -219
- package/src/blocks/marketing/testimonials/testimonial-1.tsx +0 -141
- package/src/blocks/marketing/testimonials/testimonial-2.tsx +0 -108
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { Shield, Truck } from 'lucide-react'
|
|
4
|
-
import { Badge } from '../../../ui/badge'
|
|
5
|
-
import { Button } from '../../../ui/button'
|
|
6
|
-
import { Card, CardContent, CardTitle } from '../../../ui/card'
|
|
7
|
-
import { Field, FieldLabel } from '../../../ui/field'
|
|
8
|
-
import { Input } from '../../../ui/input'
|
|
9
|
-
import { Separator } from '../../../ui/separator'
|
|
10
|
-
import { formatMoney, isAvailableMoney } from '../../../lib/money'
|
|
11
|
-
import type { Money } from '@thorprovider/types'
|
|
12
|
-
import type {
|
|
13
|
-
CheckoutForm1OrderSummary,
|
|
14
|
-
CheckoutForm1OrderSummaryItem,
|
|
15
|
-
CheckoutForm1TotalsState,
|
|
16
|
-
} from './checkout-form-1'
|
|
17
|
-
|
|
18
|
-
export interface CheckoutForm1OrderSummaryProps {
|
|
19
|
-
orderSummary: CheckoutForm1OrderSummary
|
|
20
|
-
totalsState?: CheckoutForm1TotalsState
|
|
21
|
-
totalsErrorLabel?: string
|
|
22
|
-
/** Shown when a required total amount is absent or malformed (never as zero). */
|
|
23
|
-
totalsUnavailableLabel?: string
|
|
24
|
-
/** Shown for a line whose supplied line total is absent or malformed. */
|
|
25
|
-
amountUnavailableLabel?: string
|
|
26
|
-
retryLabel?: string
|
|
27
|
-
onRetryTotals?: () => void
|
|
28
|
-
promoCode?: string
|
|
29
|
-
onPromoCodeChange?: (value: string) => void
|
|
30
|
-
onApplyPromo?: (code: string) => void
|
|
31
|
-
promoPending?: boolean
|
|
32
|
-
promoApplyLabel?: string
|
|
33
|
-
promoPlaceholder?: string
|
|
34
|
-
trustNotes?: string[]
|
|
35
|
-
/** Override money formatting (locale, currency). Defaults to `Money.currencyCode`. */
|
|
36
|
-
formatMoney?: (money: Money) => string
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function CheckoutForm1OrderSummary({
|
|
40
|
-
orderSummary,
|
|
41
|
-
totalsState = 'idle',
|
|
42
|
-
totalsErrorLabel = 'No se pudieron actualizar los totales.',
|
|
43
|
-
totalsUnavailableLabel = 'Totales no disponibles.',
|
|
44
|
-
amountUnavailableLabel = '—',
|
|
45
|
-
retryLabel = 'Reintentar',
|
|
46
|
-
onRetryTotals,
|
|
47
|
-
promoCode,
|
|
48
|
-
onPromoCodeChange,
|
|
49
|
-
onApplyPromo,
|
|
50
|
-
promoPending = false,
|
|
51
|
-
promoApplyLabel = 'Aplicar',
|
|
52
|
-
promoPlaceholder = 'Ingresa el código',
|
|
53
|
-
trustNotes,
|
|
54
|
-
formatMoney: formatMoneyProp,
|
|
55
|
-
}: CheckoutForm1OrderSummaryProps) {
|
|
56
|
-
const format = (value: Money | null | undefined) => formatMoney(value, formatMoneyProp)
|
|
57
|
-
const lineAmount = (item: CheckoutForm1OrderSummaryItem): string | null => {
|
|
58
|
-
// A supplied line total that is absent/malformed must not fall back to a
|
|
59
|
-
// numeric guess; only an entirely absent line total uses the legacy field.
|
|
60
|
-
if (item.lineTotal !== undefined) return format(item.lineTotal)
|
|
61
|
-
if (typeof item.price === 'number') return `$${(item.price * item.quantity).toFixed(2)}`
|
|
62
|
-
return null
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const renderTotals = () => {
|
|
66
|
-
if (totalsState === 'pending') {
|
|
67
|
-
return <p className="text-muted-foreground text-sm" aria-live="polite">Actualizando totales…</p>
|
|
68
|
-
}
|
|
69
|
-
if (totalsState === 'error') {
|
|
70
|
-
return (
|
|
71
|
-
<div className="flex flex-col gap-2" role="alert">
|
|
72
|
-
<p className="text-destructive text-sm">{totalsErrorLabel}</p>
|
|
73
|
-
{onRetryTotals ? (
|
|
74
|
-
<Button type="button" variant="outline" size="sm" className="w-fit" onClick={onRetryTotals}>
|
|
75
|
-
{retryLabel}
|
|
76
|
-
</Button>
|
|
77
|
-
) : null}
|
|
78
|
-
</div>
|
|
79
|
-
)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const totals = orderSummary.totals
|
|
83
|
-
if (totals) {
|
|
84
|
-
const subtotalText = format(totals.subtotal)
|
|
85
|
-
const totalText = format(totals.total)
|
|
86
|
-
// Required totals: if either is absent/malformed, show an explicit
|
|
87
|
-
// unavailable state (with recovery) rather than a fabricated zero.
|
|
88
|
-
if (subtotalText === null || totalText === null) {
|
|
89
|
-
return (
|
|
90
|
-
<div className="flex flex-col gap-2" role="status">
|
|
91
|
-
<p className="text-muted-foreground text-sm">{totalsUnavailableLabel}</p>
|
|
92
|
-
{onRetryTotals ? (
|
|
93
|
-
<Button type="button" variant="outline" size="sm" className="w-fit" onClick={onRetryTotals}>
|
|
94
|
-
{retryLabel}
|
|
95
|
-
</Button>
|
|
96
|
-
) : null}
|
|
97
|
-
</div>
|
|
98
|
-
)
|
|
99
|
-
}
|
|
100
|
-
return (
|
|
101
|
-
<div className="flex flex-col gap-2 text-sm">
|
|
102
|
-
<div className="flex items-center justify-between">
|
|
103
|
-
<span className="text-muted-foreground">Subtotal</span>
|
|
104
|
-
<span className="font-medium tabular-nums">{subtotalText}</span>
|
|
105
|
-
</div>
|
|
106
|
-
{isAvailableMoney(totals.discount) ? (
|
|
107
|
-
<div className="flex items-center justify-between">
|
|
108
|
-
<span className="text-muted-foreground">Descuento</span>
|
|
109
|
-
<span className="font-medium tabular-nums">{format(totals.discount)}</span>
|
|
110
|
-
</div>
|
|
111
|
-
) : null}
|
|
112
|
-
{isAvailableMoney(totals.tax) ? (
|
|
113
|
-
<div className="flex items-center justify-between">
|
|
114
|
-
<span className="text-muted-foreground">Impuestos</span>
|
|
115
|
-
<span className="font-medium tabular-nums">{format(totals.tax)}</span>
|
|
116
|
-
</div>
|
|
117
|
-
) : null}
|
|
118
|
-
{isAvailableMoney(totals.shipping) ? (
|
|
119
|
-
<div className="flex items-center justify-between">
|
|
120
|
-
<span className="flex items-center gap-1.5 text-muted-foreground">
|
|
121
|
-
<Truck className="size-4" />
|
|
122
|
-
Envío
|
|
123
|
-
</span>
|
|
124
|
-
<span className="font-medium tabular-nums">{format(totals.shipping)}</span>
|
|
125
|
-
</div>
|
|
126
|
-
) : null}
|
|
127
|
-
<Separator />
|
|
128
|
-
<div className="flex items-center justify-between">
|
|
129
|
-
<span className="font-semibold">Total</span>
|
|
130
|
-
<span className="font-semibold tabular-nums">{totalText}</span>
|
|
131
|
-
</div>
|
|
132
|
-
</div>
|
|
133
|
-
)
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Legacy numeric fallback for consumers that have not moved to `totals`.
|
|
137
|
-
const legacySubtotal = orderSummary.items.reduce(
|
|
138
|
-
(sum, item) => sum + (item.price ?? 0) * item.quantity,
|
|
139
|
-
0
|
|
140
|
-
)
|
|
141
|
-
const legacyTotal =
|
|
142
|
-
legacySubtotal +
|
|
143
|
-
(orderSummary.shipping ?? 0) +
|
|
144
|
-
(orderSummary.tax ?? 0) -
|
|
145
|
-
(orderSummary.discount ?? 0) -
|
|
146
|
-
(orderSummary.promoDiscount ?? 0)
|
|
147
|
-
return (
|
|
148
|
-
<div className="flex flex-col gap-2 text-sm">
|
|
149
|
-
<div className="flex items-center justify-between">
|
|
150
|
-
<span className="text-muted-foreground">Subtotal</span>
|
|
151
|
-
<span className="font-medium tabular-nums">${legacySubtotal.toFixed(2)}</span>
|
|
152
|
-
</div>
|
|
153
|
-
<div className="flex items-center justify-between">
|
|
154
|
-
<span className="text-muted-foreground">Envío</span>
|
|
155
|
-
<span className="font-medium tabular-nums">${(orderSummary.shipping ?? 0).toFixed(2)}</span>
|
|
156
|
-
</div>
|
|
157
|
-
<div className="flex items-center justify-between">
|
|
158
|
-
<span className="text-muted-foreground">Impuestos</span>
|
|
159
|
-
<span className="font-medium tabular-nums">${(orderSummary.tax ?? 0).toFixed(2)}</span>
|
|
160
|
-
</div>
|
|
161
|
-
<Separator />
|
|
162
|
-
<div className="flex items-center justify-between">
|
|
163
|
-
<span className="font-semibold">Total</span>
|
|
164
|
-
<span className="font-semibold tabular-nums">${legacyTotal.toFixed(2)}</span>
|
|
165
|
-
</div>
|
|
166
|
-
</div>
|
|
167
|
-
)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return (
|
|
171
|
-
<Card className="sticky top-8">
|
|
172
|
-
<CardContent className="flex flex-col gap-4 p-6">
|
|
173
|
-
<CardTitle>Order Summary</CardTitle>
|
|
174
|
-
|
|
175
|
-
<div className="flex flex-col gap-4">
|
|
176
|
-
{orderSummary.items.map((item) => {
|
|
177
|
-
const amount = lineAmount(item)
|
|
178
|
-
return (
|
|
179
|
-
<div key={String(item.id)} className="flex items-center gap-3">
|
|
180
|
-
<div className="relative">
|
|
181
|
-
{item.image ? (
|
|
182
|
-
<img
|
|
183
|
-
src={item.image || undefined}
|
|
184
|
-
alt={item.name}
|
|
185
|
-
width={64}
|
|
186
|
-
height={64}
|
|
187
|
-
className="bg-muted size-16 rounded-lg object-cover"
|
|
188
|
-
/>
|
|
189
|
-
) : (
|
|
190
|
-
<div aria-hidden="true" className="bg-muted size-16 rounded-lg" />
|
|
191
|
-
)}
|
|
192
|
-
<Badge
|
|
193
|
-
variant="secondary"
|
|
194
|
-
className="absolute -right-2 -top-2 size-6 justify-center rounded-full p-0 text-xs"
|
|
195
|
-
>
|
|
196
|
-
{item.quantity}
|
|
197
|
-
</Badge>
|
|
198
|
-
</div>
|
|
199
|
-
<div className="flex-1">
|
|
200
|
-
<p className="text-sm font-medium">{item.name}</p>
|
|
201
|
-
{item.variant ? <p className="text-sm text-muted-foreground">{item.variant}</p> : null}
|
|
202
|
-
</div>
|
|
203
|
-
<p className="text-sm font-medium tabular-nums">{amount ?? amountUnavailableLabel}</p>
|
|
204
|
-
</div>
|
|
205
|
-
)
|
|
206
|
-
})}
|
|
207
|
-
</div>
|
|
208
|
-
|
|
209
|
-
<Separator />
|
|
210
|
-
|
|
211
|
-
{onPromoCodeChange ? (
|
|
212
|
-
<Field className="gap-2">
|
|
213
|
-
<FieldLabel htmlFor="checkout-promo-code">Código promocional</FieldLabel>
|
|
214
|
-
<div className="flex gap-2">
|
|
215
|
-
<Input
|
|
216
|
-
id="checkout-promo-code"
|
|
217
|
-
placeholder={promoPlaceholder}
|
|
218
|
-
value={promoCode ?? ''}
|
|
219
|
-
onChange={(e) => onPromoCodeChange(e.target.value)}
|
|
220
|
-
/>
|
|
221
|
-
<Button
|
|
222
|
-
type="button"
|
|
223
|
-
variant="outline"
|
|
224
|
-
disabled={promoPending || !onApplyPromo}
|
|
225
|
-
onClick={() => onApplyPromo?.(promoCode ?? '')}
|
|
226
|
-
>
|
|
227
|
-
{promoApplyLabel}
|
|
228
|
-
</Button>
|
|
229
|
-
</div>
|
|
230
|
-
</Field>
|
|
231
|
-
) : null}
|
|
232
|
-
|
|
233
|
-
<Separator />
|
|
234
|
-
|
|
235
|
-
{renderTotals()}
|
|
236
|
-
|
|
237
|
-
{trustNotes && trustNotes.length > 0 ? (
|
|
238
|
-
<div className="flex flex-col gap-2 text-sm text-muted-foreground">
|
|
239
|
-
{trustNotes.map((note, index) => (
|
|
240
|
-
<span key={`trust:${index}:${note}`} className="flex items-center gap-2">
|
|
241
|
-
<Shield className="size-4" />
|
|
242
|
-
{note}
|
|
243
|
-
</span>
|
|
244
|
-
))}
|
|
245
|
-
</div>
|
|
246
|
-
) : null}
|
|
247
|
-
</CardContent>
|
|
248
|
-
</Card>
|
|
249
|
-
)
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
export default CheckoutForm1OrderSummary
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useState } from 'react'
|
|
4
|
-
import { Eye, EyeOff } from 'lucide-react'
|
|
5
|
-
import { Field, FieldContent, FieldLabel, FieldLegend, FieldSet } from '../../../ui/field'
|
|
6
|
-
import { Input } from '../../../ui/input'
|
|
7
|
-
import {
|
|
8
|
-
InputGroup,
|
|
9
|
-
InputGroupAddon,
|
|
10
|
-
InputGroupButton,
|
|
11
|
-
InputGroupInput,
|
|
12
|
-
} from '../../../ui/input-group'
|
|
13
|
-
import { RadioGroup, RadioGroupItem } from '../../../ui/radio-group'
|
|
14
|
-
import { CheckboxCheck } from './checkbox-check'
|
|
15
|
-
import { CheckoutFieldFeedback, checkoutFieldAria } from './checkout-field-feedback'
|
|
16
|
-
import type { PaymentMethod } from '@thorprovider/types'
|
|
17
|
-
import type {
|
|
18
|
-
CheckoutForm1ChangeHandler,
|
|
19
|
-
CheckoutForm1FieldDescriptions,
|
|
20
|
-
CheckoutForm1FieldErrors,
|
|
21
|
-
CheckoutForm1FormData,
|
|
22
|
-
} from './checkout-form-1'
|
|
23
|
-
|
|
24
|
-
export interface CheckoutForm1PaymentStepProps {
|
|
25
|
-
formData: CheckoutForm1FormData
|
|
26
|
-
onChange: CheckoutForm1ChangeHandler
|
|
27
|
-
onCardNumberChange: (value: string) => void
|
|
28
|
-
onExpiryChange: (value: string) => void
|
|
29
|
-
paymentMethods?: PaymentMethod[]
|
|
30
|
-
noPaymentMethodsLabel?: string
|
|
31
|
-
/** Raw card fields are legacy-only; the launch flow defaults to `false`. */
|
|
32
|
-
collectCardFields?: boolean
|
|
33
|
-
fieldErrors?: CheckoutForm1FieldErrors
|
|
34
|
-
fieldDescriptions?: CheckoutForm1FieldDescriptions
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function CheckoutForm1PaymentStep({
|
|
38
|
-
formData,
|
|
39
|
-
onChange,
|
|
40
|
-
onCardNumberChange,
|
|
41
|
-
onExpiryChange,
|
|
42
|
-
paymentMethods = [],
|
|
43
|
-
noPaymentMethodsLabel = 'No payment methods are available for this region.',
|
|
44
|
-
collectCardFields = false,
|
|
45
|
-
fieldErrors,
|
|
46
|
-
fieldDescriptions,
|
|
47
|
-
}: CheckoutForm1PaymentStepProps) {
|
|
48
|
-
const [showCvv, setShowCvv] = useState(false)
|
|
49
|
-
|
|
50
|
-
const selectedMethod = paymentMethods.find(method => method.id === formData.paymentMethod)
|
|
51
|
-
const isCard = selectedMethod?.type === 'card'
|
|
52
|
-
const showCardFields = collectCardFields && isCard
|
|
53
|
-
|
|
54
|
-
return (
|
|
55
|
-
<FieldSet>
|
|
56
|
-
<FieldLegend>Payment method</FieldLegend>
|
|
57
|
-
{paymentMethods.length === 0 ? (
|
|
58
|
-
<p className="text-muted-foreground text-sm">{noPaymentMethodsLabel}</p>
|
|
59
|
-
) : (
|
|
60
|
-
<RadioGroup value={formData.paymentMethod} onValueChange={value => value && onChange('paymentMethod', value)} className="grid gap-3">
|
|
61
|
-
{paymentMethods.map(method => (
|
|
62
|
-
<Field key={method.id} orientation="horizontal" className="justify-between rounded-lg border p-4">
|
|
63
|
-
<FieldContent>
|
|
64
|
-
<FieldLabel htmlFor={`payment-${method.id}`} className="flex flex-col gap-0.5">
|
|
65
|
-
<span>{method.name}</span>
|
|
66
|
-
{method.description ? (
|
|
67
|
-
<span className="text-muted-foreground text-xs font-normal">{method.description}</span>
|
|
68
|
-
) : null}
|
|
69
|
-
</FieldLabel>
|
|
70
|
-
</FieldContent>
|
|
71
|
-
<RadioGroupItem value={method.id} id={`payment-${method.id}`} />
|
|
72
|
-
</Field>
|
|
73
|
-
))}
|
|
74
|
-
</RadioGroup>
|
|
75
|
-
)}
|
|
76
|
-
|
|
77
|
-
{selectedMethod && !showCardFields ? (
|
|
78
|
-
<div className="bg-muted/50 rounded-lg p-4 text-sm">
|
|
79
|
-
<p className="font-medium">{selectedMethod.name}</p>
|
|
80
|
-
{selectedMethod.description ? <p className="text-muted-foreground mt-1">{selectedMethod.description}</p> : null}
|
|
81
|
-
</div>
|
|
82
|
-
) : null}
|
|
83
|
-
|
|
84
|
-
{showCardFields ? (
|
|
85
|
-
<>
|
|
86
|
-
<Field data-invalid={!!fieldErrors?.cardNumber?.length}>
|
|
87
|
-
<FieldLabel htmlFor="checkout-card-number">Card number</FieldLabel>
|
|
88
|
-
<Input
|
|
89
|
-
id="checkout-card-number"
|
|
90
|
-
inputMode="numeric"
|
|
91
|
-
maxLength={19}
|
|
92
|
-
placeholder="1234 5678 9012 3456"
|
|
93
|
-
value={formData.cardNumber}
|
|
94
|
-
onChange={(e) => onCardNumberChange(e.target.value)}
|
|
95
|
-
{...checkoutFieldAria('checkout-card-number', fieldErrors?.cardNumber)}
|
|
96
|
-
/>
|
|
97
|
-
<CheckoutFieldFeedback
|
|
98
|
-
id="checkout-card-number"
|
|
99
|
-
errors={fieldErrors?.cardNumber}
|
|
100
|
-
description={fieldDescriptions?.cardNumber}
|
|
101
|
-
/>
|
|
102
|
-
</Field>
|
|
103
|
-
<div className="grid gap-4 md:grid-cols-2">
|
|
104
|
-
<Field data-invalid={!!fieldErrors?.expiryDate?.length}>
|
|
105
|
-
<FieldLabel htmlFor="checkout-expiry">Expiry date</FieldLabel>
|
|
106
|
-
<Input
|
|
107
|
-
id="checkout-expiry"
|
|
108
|
-
inputMode="numeric"
|
|
109
|
-
maxLength={5}
|
|
110
|
-
placeholder="MM/YY"
|
|
111
|
-
value={formData.expiryDate}
|
|
112
|
-
onChange={(e) => onExpiryChange(e.target.value)}
|
|
113
|
-
{...checkoutFieldAria('checkout-expiry', fieldErrors?.expiryDate)}
|
|
114
|
-
/>
|
|
115
|
-
<CheckoutFieldFeedback
|
|
116
|
-
id="checkout-expiry"
|
|
117
|
-
errors={fieldErrors?.expiryDate}
|
|
118
|
-
description={fieldDescriptions?.expiryDate}
|
|
119
|
-
/>
|
|
120
|
-
</Field>
|
|
121
|
-
<Field data-invalid={!!fieldErrors?.cvv?.length}>
|
|
122
|
-
<FieldLabel htmlFor="checkout-cvv">CVV</FieldLabel>
|
|
123
|
-
<InputGroup>
|
|
124
|
-
<InputGroupInput
|
|
125
|
-
id="checkout-cvv"
|
|
126
|
-
inputMode="numeric"
|
|
127
|
-
maxLength={4}
|
|
128
|
-
placeholder="123"
|
|
129
|
-
type={showCvv ? 'text' : 'password'}
|
|
130
|
-
value={formData.cvv}
|
|
131
|
-
onChange={(e) => onChange('cvv', e.target.value)}
|
|
132
|
-
{...checkoutFieldAria('checkout-cvv', fieldErrors?.cvv)}
|
|
133
|
-
/>
|
|
134
|
-
<InputGroupAddon inline-end>
|
|
135
|
-
<InputGroupButton
|
|
136
|
-
type="button"
|
|
137
|
-
variant="ghost"
|
|
138
|
-
size="icon-xs"
|
|
139
|
-
aria-label={showCvv ? 'Hide CVV' : 'Show CVV'}
|
|
140
|
-
onClick={() => setShowCvv((s) => !s)}
|
|
141
|
-
>
|
|
142
|
-
{showCvv ? <EyeOff /> : <Eye />}
|
|
143
|
-
</InputGroupButton>
|
|
144
|
-
</InputGroupAddon>
|
|
145
|
-
</InputGroup>
|
|
146
|
-
<CheckoutFieldFeedback
|
|
147
|
-
id="checkout-cvv"
|
|
148
|
-
errors={fieldErrors?.cvv}
|
|
149
|
-
description={fieldDescriptions?.cvv}
|
|
150
|
-
/>
|
|
151
|
-
</Field>
|
|
152
|
-
</div>
|
|
153
|
-
<Field data-invalid={!!fieldErrors?.cardName?.length}>
|
|
154
|
-
<FieldLabel htmlFor="checkout-card-name">Name on card</FieldLabel>
|
|
155
|
-
<Input
|
|
156
|
-
id="checkout-card-name"
|
|
157
|
-
placeholder="John Doe"
|
|
158
|
-
value={formData.cardName}
|
|
159
|
-
onChange={(e) => onChange('cardName', e.target.value)}
|
|
160
|
-
{...checkoutFieldAria('checkout-card-name', fieldErrors?.cardName)}
|
|
161
|
-
/>
|
|
162
|
-
<CheckoutFieldFeedback
|
|
163
|
-
id="checkout-card-name"
|
|
164
|
-
errors={fieldErrors?.cardName}
|
|
165
|
-
description={fieldDescriptions?.cardName}
|
|
166
|
-
/>
|
|
167
|
-
</Field>
|
|
168
|
-
<Field orientation="horizontal" className="gap-2" data-invalid={!!fieldErrors?.saveInfo?.length}>
|
|
169
|
-
<CheckboxCheck
|
|
170
|
-
id="checkout-save-info"
|
|
171
|
-
checked={formData.saveInfo}
|
|
172
|
-
onCheckedChange={(checked) => onChange('saveInfo', checked === true)}
|
|
173
|
-
{...checkoutFieldAria('checkout-save-info', fieldErrors?.saveInfo)}
|
|
174
|
-
/>
|
|
175
|
-
<FieldContent>
|
|
176
|
-
<FieldLabel htmlFor="checkout-save-info">Save payment info for next time</FieldLabel>
|
|
177
|
-
<CheckoutFieldFeedback
|
|
178
|
-
id="checkout-save-info"
|
|
179
|
-
errors={fieldErrors?.saveInfo}
|
|
180
|
-
description={fieldDescriptions?.saveInfo}
|
|
181
|
-
/>
|
|
182
|
-
</FieldContent>
|
|
183
|
-
</Field>
|
|
184
|
-
</>
|
|
185
|
-
) : null}
|
|
186
|
-
</FieldSet>
|
|
187
|
-
)
|
|
188
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { cn } from '../../../lib/utils'
|
|
4
|
-
|
|
5
|
-
const STEP_COUNT = 3
|
|
6
|
-
|
|
7
|
-
export interface CheckoutForm1ProgressProps {
|
|
8
|
-
step: number
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function CheckoutForm1Progress({ step }: CheckoutForm1ProgressProps) {
|
|
12
|
-
return (
|
|
13
|
-
<div className="flex items-center justify-center gap-2">
|
|
14
|
-
{Array.from({ length: STEP_COUNT }, (_, index) => {
|
|
15
|
-
const current = index + 1
|
|
16
|
-
const isDone = current <= step
|
|
17
|
-
return (
|
|
18
|
-
<div key={current} className="flex items-center gap-2">
|
|
19
|
-
{index > 0 && (
|
|
20
|
-
<div aria-hidden="true" className={cn('h-1 w-16', isDone ? 'bg-primary' : 'bg-muted')} />
|
|
21
|
-
)}
|
|
22
|
-
<div
|
|
23
|
-
aria-current={current === step ? 'step' : undefined}
|
|
24
|
-
className={cn(
|
|
25
|
-
'flex size-10 items-center justify-center rounded-full text-sm font-medium',
|
|
26
|
-
isDone ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'
|
|
27
|
-
)}
|
|
28
|
-
>
|
|
29
|
-
{current}
|
|
30
|
-
</div>
|
|
31
|
-
</div>
|
|
32
|
-
)
|
|
33
|
-
})}
|
|
34
|
-
</div>
|
|
35
|
-
)
|
|
36
|
-
}
|
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
-
import { describe, expect, it } from 'vitest'
|
|
3
|
-
import type { PaymentMethod } from '@thorprovider/types'
|
|
4
|
-
import { CheckoutForm1PaymentStep } from './checkout-form-1-payment-step'
|
|
5
|
-
import { CheckoutForm1ShippingStep } from './checkout-form-1-shipping-step'
|
|
6
|
-
import { CheckoutForm1OrderSummary } from './checkout-form-1-order-summary'
|
|
7
|
-
import type { CheckoutForm1FormData } from './checkout-form-1'
|
|
8
|
-
|
|
9
|
-
const formData: CheckoutForm1FormData = {
|
|
10
|
-
email: '',
|
|
11
|
-
firstName: '',
|
|
12
|
-
lastName: '',
|
|
13
|
-
phone: '',
|
|
14
|
-
address: '',
|
|
15
|
-
city: '',
|
|
16
|
-
state: '',
|
|
17
|
-
zipCode: '',
|
|
18
|
-
country: 'US',
|
|
19
|
-
paymentMethod: 'pp_card',
|
|
20
|
-
cardNumber: '',
|
|
21
|
-
expiryDate: '',
|
|
22
|
-
cvv: '',
|
|
23
|
-
cardName: '',
|
|
24
|
-
saveInfo: false,
|
|
25
|
-
sameAsBilling: true,
|
|
26
|
-
newsletter: false,
|
|
27
|
-
promoCode: '',
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const cardMethod: PaymentMethod = {
|
|
31
|
-
id: 'pp_card',
|
|
32
|
-
name: 'Tarjeta',
|
|
33
|
-
type: 'card',
|
|
34
|
-
isEnabled: true,
|
|
35
|
-
requiresSetup: true,
|
|
36
|
-
description: 'Paga con tarjeta',
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const manualMethod: PaymentMethod = {
|
|
40
|
-
id: 'pp_system_default',
|
|
41
|
-
name: 'Pago manual TEST',
|
|
42
|
-
type: 'cash_on_delivery',
|
|
43
|
-
isEnabled: true,
|
|
44
|
-
requiresSetup: false,
|
|
45
|
-
description: 'Pago de prueba, sin tarjeta',
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const noop = () => {}
|
|
49
|
-
|
|
50
|
-
const formatMoney = (money: { amount: string; currencyCode: string }) =>
|
|
51
|
-
`${money.currencyCode} ${money.amount}`
|
|
52
|
-
|
|
53
|
-
describe('CheckoutForm1 payment step — launch flow collects no raw card data', () => {
|
|
54
|
-
it('renders no card fields by default even for a card-type method', () => {
|
|
55
|
-
const html = renderToStaticMarkup(
|
|
56
|
-
<CheckoutForm1PaymentStep
|
|
57
|
-
formData={formData}
|
|
58
|
-
onChange={noop}
|
|
59
|
-
onCardNumberChange={noop}
|
|
60
|
-
onExpiryChange={noop}
|
|
61
|
-
paymentMethods={[cardMethod]}
|
|
62
|
-
/>,
|
|
63
|
-
)
|
|
64
|
-
expect(html).not.toContain('checkout-card-number')
|
|
65
|
-
expect(html).not.toContain('checkout-cvv')
|
|
66
|
-
expect(html).toContain('Paga con tarjeta')
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
it('renders the approved manual TEST provider label only', () => {
|
|
70
|
-
const html = renderToStaticMarkup(
|
|
71
|
-
<CheckoutForm1PaymentStep
|
|
72
|
-
formData={{ ...formData, paymentMethod: 'pp_system_default' }}
|
|
73
|
-
onChange={noop}
|
|
74
|
-
onCardNumberChange={noop}
|
|
75
|
-
onExpiryChange={noop}
|
|
76
|
-
paymentMethods={[manualMethod]}
|
|
77
|
-
/>,
|
|
78
|
-
)
|
|
79
|
-
expect(html).toContain('Pago manual TEST')
|
|
80
|
-
expect(html).toContain('Pago de prueba, sin tarjeta')
|
|
81
|
-
expect(html).not.toContain('checkout-card-number')
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
it('renders card fields only in the explicit legacy mode', () => {
|
|
85
|
-
const html = renderToStaticMarkup(
|
|
86
|
-
<CheckoutForm1PaymentStep
|
|
87
|
-
formData={formData}
|
|
88
|
-
onChange={noop}
|
|
89
|
-
onCardNumberChange={noop}
|
|
90
|
-
onExpiryChange={noop}
|
|
91
|
-
paymentMethods={[cardMethod]}
|
|
92
|
-
collectCardFields
|
|
93
|
-
/>,
|
|
94
|
-
)
|
|
95
|
-
expect(html).toContain('checkout-card-number')
|
|
96
|
-
expect(html).toContain('checkout-cvv')
|
|
97
|
-
})
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
describe('CheckoutForm1 shipping step — controlled eligible options', () => {
|
|
101
|
-
it('renders only the supplied eligible shipping options with their prices', () => {
|
|
102
|
-
const html = renderToStaticMarkup(
|
|
103
|
-
<CheckoutForm1ShippingStep
|
|
104
|
-
formData={formData}
|
|
105
|
-
onChange={noop}
|
|
106
|
-
countries={[{ value: 'US', label: 'Estados Unidos' }]}
|
|
107
|
-
shippingOptions={[
|
|
108
|
-
{ id: 'sm_std', name: 'Estándar', price: { amount: '5.00', currencyCode: 'USD' } },
|
|
109
|
-
{ id: 'sm_exp', name: 'Exprés', price: { amount: '15.00', currencyCode: 'USD' } },
|
|
110
|
-
]}
|
|
111
|
-
selectedShippingOptionId='sm_exp'
|
|
112
|
-
onSelectShippingOption={noop}
|
|
113
|
-
formatMoney={formatMoney}
|
|
114
|
-
/>,
|
|
115
|
-
)
|
|
116
|
-
expect(html).toContain('Estándar')
|
|
117
|
-
expect(html).toContain('Exprés')
|
|
118
|
-
expect(html).toContain('USD 5.00')
|
|
119
|
-
expect(html).toContain('USD 15.00')
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
it('renders no shipping section when no options are supplied', () => {
|
|
123
|
-
const html = renderToStaticMarkup(
|
|
124
|
-
<CheckoutForm1ShippingStep formData={formData} onChange={noop} />,
|
|
125
|
-
)
|
|
126
|
-
expect(html).not.toContain('Shipping method')
|
|
127
|
-
})
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
describe('CheckoutForm1 order summary — authoritative totals and states', () => {
|
|
131
|
-
const items = [
|
|
132
|
-
{
|
|
133
|
-
id: 'line_1',
|
|
134
|
-
name: 'Taladro',
|
|
135
|
-
variant: 'Rojo',
|
|
136
|
-
quantity: 2,
|
|
137
|
-
lineTotal: { amount: '90.00', currencyCode: 'USD' },
|
|
138
|
-
},
|
|
139
|
-
]
|
|
140
|
-
|
|
141
|
-
it('renders backend-authoritative totals verbatim', () => {
|
|
142
|
-
const html = renderToStaticMarkup(
|
|
143
|
-
<CheckoutForm1OrderSummary
|
|
144
|
-
orderSummary={{
|
|
145
|
-
items,
|
|
146
|
-
totals: {
|
|
147
|
-
subtotal: { amount: '90.00', currencyCode: 'USD' },
|
|
148
|
-
tax: { amount: '7.20', currencyCode: 'USD' },
|
|
149
|
-
shipping: { amount: '5.00', currencyCode: 'USD' },
|
|
150
|
-
total: { amount: '102.20', currencyCode: 'USD' },
|
|
151
|
-
},
|
|
152
|
-
}}
|
|
153
|
-
formatMoney={formatMoney}
|
|
154
|
-
/>,
|
|
155
|
-
)
|
|
156
|
-
expect(html).toContain('USD 90.00')
|
|
157
|
-
expect(html).toContain('USD 7.20')
|
|
158
|
-
expect(html).toContain('USD 5.00')
|
|
159
|
-
expect(html).toContain('USD 102.20')
|
|
160
|
-
expect(html).not.toContain('Free shipping on orders over')
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
it('renders a controlled pending state without inventing amounts', () => {
|
|
164
|
-
const html = renderToStaticMarkup(
|
|
165
|
-
<CheckoutForm1OrderSummary orderSummary={{ items }} totalsState='pending' />,
|
|
166
|
-
)
|
|
167
|
-
expect(html).toContain('Actualizando totales')
|
|
168
|
-
expect(html).not.toContain('Total')
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
it('never renders a fake zero for absent or malformed required totals', () => {
|
|
172
|
-
const html = renderToStaticMarkup(
|
|
173
|
-
<CheckoutForm1OrderSummary
|
|
174
|
-
orderSummary={{
|
|
175
|
-
items,
|
|
176
|
-
totals: {
|
|
177
|
-
subtotal: { amount: '', currencyCode: 'USD' },
|
|
178
|
-
total: { amount: 'abc', currencyCode: 'USD' },
|
|
179
|
-
},
|
|
180
|
-
}}
|
|
181
|
-
formatMoney={formatMoney}
|
|
182
|
-
/>,
|
|
183
|
-
)
|
|
184
|
-
expect(html).not.toContain('USD 0')
|
|
185
|
-
expect(html).toContain('Totales no disponibles')
|
|
186
|
-
})
|
|
187
|
-
|
|
188
|
-
it('preserves a legitimate zero total', () => {
|
|
189
|
-
const html = renderToStaticMarkup(
|
|
190
|
-
<CheckoutForm1OrderSummary
|
|
191
|
-
orderSummary={{
|
|
192
|
-
items,
|
|
193
|
-
totals: {
|
|
194
|
-
subtotal: { amount: '0', currencyCode: 'USD' },
|
|
195
|
-
total: { amount: '0', currencyCode: 'USD' },
|
|
196
|
-
},
|
|
197
|
-
}}
|
|
198
|
-
formatMoney={formatMoney}
|
|
199
|
-
/>,
|
|
200
|
-
)
|
|
201
|
-
expect(html).toContain('USD 0')
|
|
202
|
-
expect(html).not.toContain('Totales no disponibles')
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
it('renders a controlled error state with a recovery action', () => {
|
|
206
|
-
const html = renderToStaticMarkup(
|
|
207
|
-
<CheckoutForm1OrderSummary
|
|
208
|
-
orderSummary={{ items }}
|
|
209
|
-
totalsState='error'
|
|
210
|
-
onRetryTotals={noop}
|
|
211
|
-
/>,
|
|
212
|
-
)
|
|
213
|
-
expect(html).toContain('No se pudieron actualizar los totales')
|
|
214
|
-
expect(html).toContain('Reintentar')
|
|
215
|
-
})
|
|
216
|
-
})
|