@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.
Files changed (91) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +156 -40
  3. package/package.json +1 -1
  4. package/src/blocks/application/login-forms/login-page.test.tsx +190 -0
  5. package/src/blocks/application/login-forms/login-page.tsx +240 -0
  6. package/src/blocks/application/password-recovery/forgot-password-page-1.tsx +6 -8
  7. package/src/blocks/application/signup-forms/signup-page-1.tsx +127 -129
  8. package/src/blocks/application/verification/verify-1.tsx +2 -2
  9. package/src/blocks/application/viewport-ownership.test.tsx +41 -0
  10. package/src/blocks/e-commerce/category-filters/category-listing-filter-fields.tsx +218 -0
  11. package/src/blocks/e-commerce/category-filters/{category-filter-2.test.tsx → category-listing.test.tsx} +287 -199
  12. package/src/blocks/e-commerce/category-filters/category-listing.tsx +934 -0
  13. package/src/blocks/e-commerce/checkout-forms/checkout-contact-step.tsx +149 -0
  14. package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-country-field.tsx → checkout-country-field.tsx} +10 -8
  15. package/src/blocks/e-commerce/checkout-forms/checkout-order-summary.tsx +230 -0
  16. package/src/blocks/e-commerce/checkout-forms/checkout-payment-step.tsx +100 -0
  17. package/src/blocks/e-commerce/checkout-forms/checkout-progress.tsx +62 -0
  18. package/src/blocks/e-commerce/checkout-forms/checkout-review-step.tsx +59 -0
  19. package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-shipping-step.tsx → checkout-shipping-step.tsx} +65 -41
  20. package/src/blocks/e-commerce/checkout-forms/checkout.test.tsx +478 -0
  21. package/src/blocks/e-commerce/checkout-forms/checkout.tsx +550 -0
  22. package/src/blocks/{marketing/navbars/navbar-4-account-menu.test.tsx → e-commerce/navigation/storefront-navbar-account-menu.test.tsx} +8 -8
  23. package/src/blocks/{marketing/navbars/navbar-4-account-menu.tsx → e-commerce/navigation/storefront-navbar-account-menu.tsx} +3 -3
  24. package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.test.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.test.tsx} +22 -22
  25. package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.tsx} +3 -3
  26. package/src/blocks/{marketing/navbars/navbar-4-mobile-nav.tsx → e-commerce/navigation/storefront-navbar-mobile-nav.tsx} +4 -12
  27. package/src/blocks/{marketing/navbars/navbar-4-search-bar.tsx → e-commerce/navigation/storefront-navbar-search-bar.tsx} +3 -3
  28. package/src/blocks/{marketing/navbars/navbar-4.test.tsx → e-commerce/navigation/storefront-navbar.test.tsx} +40 -40
  29. package/src/blocks/{marketing/navbars/navbar-4.tsx → e-commerce/navigation/storefront-navbar.tsx} +17 -17
  30. package/src/blocks/e-commerce/product-list/product-card.tsx +63 -40
  31. package/src/blocks/e-commerce/shopping-carts/shopping-cart.test.tsx +2 -2
  32. package/src/blocks/marketing/footers/footer-section-accessibility.test.tsx +118 -0
  33. package/src/blocks/marketing/footers/footer-section-actions.test.tsx +131 -0
  34. package/src/blocks/marketing/footers/footer-section-data.test.tsx +127 -0
  35. package/src/blocks/marketing/footers/footer-section-native-links.test.tsx +100 -0
  36. package/src/blocks/marketing/footers/footer-section-public-api.test.tsx +11 -0
  37. package/src/blocks/marketing/footers/footer-section.test.tsx +46 -0
  38. package/src/blocks/marketing/footers/footer-section.tsx +532 -0
  39. package/src/blocks/marketing/navbars/navbar-public-api.test.tsx +57 -0
  40. package/src/blocks/marketing/navbars/navbar.mega.test.tsx +149 -0
  41. package/src/blocks/marketing/navbars/navbar.test.tsx +155 -0
  42. package/src/blocks/marketing/navbars/navbar.tsx +735 -0
  43. package/src/blocks/marketing/testimonials/testimonial-section.test.tsx +171 -0
  44. package/src/blocks/marketing/testimonials/testimonial-section.tsx +237 -0
  45. package/src/index.ts +80 -81
  46. package/src/lib/contracts-curated.test.ts +5 -5
  47. package/src/lib/contracts.test.ts +55 -0
  48. package/src/lib/contracts.ts +64 -2
  49. package/tsup.config.ts +1 -1
  50. package/src/blocks/application/login-forms/login-page-1.tsx +0 -246
  51. package/src/blocks/application/login-forms/login-page-2.tsx +0 -207
  52. package/src/blocks/e-commerce/category-filters/category-filter-1.tsx +0 -260
  53. package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +0 -222
  54. package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +0 -921
  55. package/src/blocks/e-commerce/category-filters/category-filter-4-chip-rail.tsx +0 -57
  56. package/src/blocks/e-commerce/category-filters/category-filter-4-drawer.tsx +0 -124
  57. package/src/blocks/e-commerce/category-filters/category-filter-4-fields.tsx +0 -170
  58. package/src/blocks/e-commerce/category-filters/category-filter-4-sidebar.tsx +0 -52
  59. package/src/blocks/e-commerce/category-filters/category-filter-4-sort-sheet.tsx +0 -57
  60. package/src/blocks/e-commerce/category-filters/category-filter-4-state.ts +0 -68
  61. package/src/blocks/e-commerce/category-filters/category-filter-4.tsx +0 -298
  62. package/src/blocks/e-commerce/checkout-forms/card-formatting.test.ts +0 -47
  63. package/src/blocks/e-commerce/checkout-forms/card-formatting.ts +0 -9
  64. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-contact-step.tsx +0 -96
  65. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +0 -252
  66. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +0 -188
  67. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-progress.tsx +0 -36
  68. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.test.tsx +0 -216
  69. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +0 -338
  70. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-order-summary.tsx +0 -130
  71. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-payment-card.tsx +0 -175
  72. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-shipping-card.tsx +0 -190
  73. package/src/blocks/e-commerce/checkout-forms/checkout-form-2.tsx +0 -312
  74. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-information-step.tsx +0 -91
  75. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-order-summary.tsx +0 -110
  76. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-payment-step.tsx +0 -137
  77. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-progress.tsx +0 -68
  78. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-review-step.tsx +0 -79
  79. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-shipping-step.tsx +0 -172
  80. package/src/blocks/e-commerce/checkout-forms/checkout-form-3.tsx +0 -392
  81. package/src/blocks/marketing/footers/footer-section-1.test.tsx +0 -117
  82. package/src/blocks/marketing/footers/footer-section-1.tsx +0 -178
  83. package/src/blocks/marketing/footers/footer-section-3.tsx +0 -192
  84. package/src/blocks/marketing/footers/footer-section-4.tsx +0 -355
  85. package/src/blocks/marketing/navbars/navbar-1.tsx +0 -267
  86. package/src/blocks/marketing/navbars/navbar-2.tsx +0 -351
  87. package/src/blocks/marketing/navbars/navbar-3.tsx +0 -303
  88. package/src/blocks/marketing/navbars/navbar-4-like-button.tsx +0 -177
  89. package/src/blocks/marketing/navbars/navbar-4-product-grid.tsx +0 -219
  90. package/src/blocks/marketing/testimonials/testimonial-1.tsx +0 -141
  91. 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
- })