@thorprovider/blocks 0.1.3 → 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 (178) hide show
  1. package/CHANGELOG.md +155 -0
  2. package/README.md +228 -68
  3. package/docs/standards/data-contracts.md +13 -2
  4. package/package.json +26 -10
  5. package/scripts/fix-esm-extensions.mjs +40 -0
  6. package/src/blocks/application/login-forms/login-page.test.tsx +190 -0
  7. package/src/blocks/application/login-forms/login-page.tsx +240 -0
  8. package/src/blocks/application/password-recovery/forgot-password-page-1.tsx +6 -8
  9. package/src/blocks/application/signup-forms/signup-page-1.tsx +127 -129
  10. package/src/blocks/application/verification/verify-1.tsx +2 -2
  11. package/src/blocks/application/viewport-ownership.test.tsx +41 -0
  12. package/src/blocks/e-commerce/category-filters/category-listing-filter-fields.tsx +218 -0
  13. package/src/blocks/e-commerce/category-filters/{category-filter-2.test.tsx → category-listing.test.tsx} +287 -199
  14. package/src/blocks/e-commerce/category-filters/category-listing.tsx +934 -0
  15. package/src/blocks/e-commerce/checkout-forms/checkout-contact-step.tsx +149 -0
  16. package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-country-field.tsx → checkout-country-field.tsx} +10 -8
  17. package/src/blocks/e-commerce/checkout-forms/checkout-order-summary.tsx +230 -0
  18. package/src/blocks/e-commerce/checkout-forms/checkout-payment-step.tsx +100 -0
  19. package/src/blocks/e-commerce/checkout-forms/checkout-progress.tsx +62 -0
  20. package/src/blocks/e-commerce/checkout-forms/checkout-review-step.tsx +59 -0
  21. package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-shipping-step.tsx → checkout-shipping-step.tsx} +65 -41
  22. package/src/blocks/e-commerce/checkout-forms/checkout.test.tsx +478 -0
  23. package/src/blocks/e-commerce/checkout-forms/checkout.tsx +550 -0
  24. package/src/blocks/e-commerce/navigation/storefront-navbar-account-menu.test.tsx +56 -0
  25. package/src/blocks/{marketing/navbars/navbar-4-account-menu.tsx → e-commerce/navigation/storefront-navbar-account-menu.tsx} +3 -3
  26. package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.test.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.test.tsx} +22 -22
  27. package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.tsx} +3 -3
  28. package/src/blocks/{marketing/navbars/navbar-4-mobile-nav.tsx → e-commerce/navigation/storefront-navbar-mobile-nav.tsx} +4 -12
  29. package/src/blocks/{marketing/navbars/navbar-4-search-bar.tsx → e-commerce/navigation/storefront-navbar-search-bar.tsx} +3 -3
  30. package/src/blocks/{marketing/navbars/navbar-4.test.tsx → e-commerce/navigation/storefront-navbar.test.tsx} +67 -36
  31. package/src/blocks/{marketing/navbars/navbar-4.tsx → e-commerce/navigation/storefront-navbar.tsx} +43 -25
  32. package/src/blocks/e-commerce/product-categories/category-card-grid.test.tsx +122 -0
  33. package/src/blocks/e-commerce/product-categories/category-card.test.tsx +68 -0
  34. package/src/blocks/e-commerce/product-categories/category-card.tsx +172 -0
  35. package/src/blocks/e-commerce/product-categories/category-grid.test.tsx +67 -0
  36. package/src/blocks/e-commerce/product-categories/category-shared.tsx +109 -0
  37. package/src/blocks/e-commerce/product-categories/product-category-grid.tsx +170 -0
  38. package/src/blocks/e-commerce/product-categories/product-category.test.tsx +266 -0
  39. package/src/blocks/e-commerce/product-categories/product-category.tsx +462 -0
  40. package/src/blocks/e-commerce/product-list/product-card.test.tsx +263 -0
  41. package/src/blocks/e-commerce/product-list/product-card.tsx +447 -0
  42. package/src/blocks/e-commerce/product-list/product-grid.test.tsx +162 -0
  43. package/src/blocks/e-commerce/product-list/product-grid.tsx +114 -0
  44. package/src/blocks/e-commerce/product-overview/product-overview-gallery.tsx +71 -0
  45. package/src/blocks/e-commerce/product-overview/{product-overview-5-lightbox-tab.tsx → product-overview-lightbox-tab.tsx} +35 -23
  46. package/src/blocks/e-commerce/product-overview/{product-overview-5-reviews-tab.tsx → product-overview-reviews-tab.tsx} +40 -17
  47. package/src/blocks/e-commerce/product-overview/{product-overview-5-tabs.tsx → product-overview-tabs.tsx} +8 -7
  48. package/src/blocks/e-commerce/product-overview/product-overview.test.tsx +283 -0
  49. package/src/blocks/e-commerce/product-overview/product-overview.tsx +1186 -0
  50. package/src/blocks/e-commerce/reviews/review-rating-2.tsx +5 -6
  51. package/src/blocks/e-commerce/shopping-carts/shopping-cart.test.tsx +399 -0
  52. package/src/blocks/e-commerce/shopping-carts/{shopping-cart-4.tsx → shopping-cart.tsx} +119 -85
  53. package/src/blocks/e-commerce/storefront-hero/storefront-hero.test.tsx +412 -0
  54. package/src/blocks/e-commerce/storefront-hero/storefront-hero.tsx +1283 -0
  55. package/src/blocks/marketing/features/feature.test.tsx +153 -0
  56. package/src/blocks/marketing/features/feature.tsx +195 -0
  57. package/src/blocks/marketing/footers/footer-section-accessibility.test.tsx +118 -0
  58. package/src/blocks/marketing/footers/footer-section-actions.test.tsx +131 -0
  59. package/src/blocks/marketing/footers/footer-section-data.test.tsx +127 -0
  60. package/src/blocks/marketing/footers/footer-section-native-links.test.tsx +100 -0
  61. package/src/blocks/marketing/footers/footer-section-public-api.test.tsx +11 -0
  62. package/src/blocks/marketing/footers/footer-section.test.tsx +46 -0
  63. package/src/blocks/marketing/footers/footer-section.tsx +532 -0
  64. package/src/blocks/marketing/navbars/navbar-public-api.test.tsx +57 -0
  65. package/src/blocks/marketing/navbars/navbar.mega.test.tsx +149 -0
  66. package/src/blocks/marketing/navbars/navbar.test.tsx +155 -0
  67. package/src/blocks/marketing/navbars/navbar.tsx +735 -0
  68. package/src/blocks/marketing/testimonials/testimonial-section.test.tsx +171 -0
  69. package/src/blocks/marketing/testimonials/testimonial-section.tsx +237 -0
  70. package/src/index.ts +156 -128
  71. package/src/lib/contracts-curated.test.ts +47 -29
  72. package/src/lib/contracts.test.ts +198 -2
  73. package/src/lib/contracts.ts +410 -16
  74. package/src/ui/button-group.tsx +1 -1
  75. package/src/ui/carousel.tsx +1 -1
  76. package/src/ui/dialog.tsx +1 -1
  77. package/src/ui/field.tsx +2 -2
  78. package/src/ui/input-group.tsx +3 -3
  79. package/src/ui/pagination.tsx +1 -1
  80. package/src/ui/sidebar.tsx +7 -7
  81. package/src/ui/toggle-group.tsx +1 -1
  82. package/tsconfig.build.json +12 -0
  83. package/tsup.config.ts +11 -7
  84. package/.turbo/turbo-build.log +0 -24
  85. package/.turbo/turbo-test.log +0 -47
  86. package/.turbo/turbo-type-check.log +0 -0
  87. package/dist/.tsbuildinfo +0 -1
  88. package/dist/contracts.d.mts +0 -1732
  89. package/dist/contracts.d.ts +0 -1732
  90. package/dist/contracts.js +0 -423
  91. package/dist/contracts.js.map +0 -1
  92. package/dist/contracts.mjs +0 -365
  93. package/dist/contracts.mjs.map +0 -1
  94. package/dist/index.d.mts +0 -3068
  95. package/dist/index.d.ts +0 -3068
  96. package/dist/index.js +0 -17834
  97. package/dist/index.js.map +0 -1
  98. package/dist/index.mjs +0 -17740
  99. package/dist/index.mjs.map +0 -1
  100. package/src/blocks/application/login-forms/login-page-1.tsx +0 -246
  101. package/src/blocks/application/login-forms/login-page-2.tsx +0 -207
  102. package/src/blocks/e-commerce/category-filters/category-filter-1.tsx +0 -260
  103. package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +0 -222
  104. package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +0 -921
  105. package/src/blocks/e-commerce/category-filters/category-filter-4-chip-rail.tsx +0 -57
  106. package/src/blocks/e-commerce/category-filters/category-filter-4-drawer.tsx +0 -124
  107. package/src/blocks/e-commerce/category-filters/category-filter-4-fields.tsx +0 -170
  108. package/src/blocks/e-commerce/category-filters/category-filter-4-sidebar.tsx +0 -52
  109. package/src/blocks/e-commerce/category-filters/category-filter-4-sort-sheet.tsx +0 -57
  110. package/src/blocks/e-commerce/category-filters/category-filter-4-state.ts +0 -68
  111. package/src/blocks/e-commerce/category-filters/category-filter-4.tsx +0 -298
  112. package/src/blocks/e-commerce/checkout-forms/card-formatting.test.ts +0 -47
  113. package/src/blocks/e-commerce/checkout-forms/card-formatting.ts +0 -9
  114. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-contact-step.tsx +0 -96
  115. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +0 -252
  116. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +0 -188
  117. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-progress.tsx +0 -36
  118. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.test.tsx +0 -216
  119. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +0 -338
  120. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-order-summary.tsx +0 -130
  121. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-payment-card.tsx +0 -175
  122. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-shipping-card.tsx +0 -190
  123. package/src/blocks/e-commerce/checkout-forms/checkout-form-2.tsx +0 -312
  124. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-information-step.tsx +0 -91
  125. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-order-summary.tsx +0 -110
  126. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-payment-step.tsx +0 -137
  127. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-progress.tsx +0 -68
  128. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-review-step.tsx +0 -79
  129. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-shipping-step.tsx +0 -172
  130. package/src/blocks/e-commerce/checkout-forms/checkout-form-3.tsx +0 -392
  131. package/src/blocks/e-commerce/product-categories/avatar-group.tsx +0 -86
  132. package/src/blocks/e-commerce/product-categories/product-category-1.tsx +0 -126
  133. package/src/blocks/e-commerce/product-categories/product-category-2.test.tsx +0 -51
  134. package/src/blocks/e-commerce/product-categories/product-category-2.tsx +0 -120
  135. package/src/blocks/e-commerce/product-categories/product-category-4.tsx +0 -129
  136. package/src/blocks/e-commerce/product-categories/product-category-5.tsx +0 -218
  137. package/src/blocks/e-commerce/product-list/product-card-1.test.tsx +0 -88
  138. package/src/blocks/e-commerce/product-list/product-card-1.tsx +0 -192
  139. package/src/blocks/e-commerce/product-list/product-card-2.tsx +0 -71
  140. package/src/blocks/e-commerce/product-list/product-card-3-reaction-toggle.tsx +0 -166
  141. package/src/blocks/e-commerce/product-list/product-card-3.tsx +0 -215
  142. package/src/blocks/e-commerce/product-list/product-card-5-checkbox-check.tsx +0 -98
  143. package/src/blocks/e-commerce/product-list/product-card-5-reaction-toggle.tsx +0 -171
  144. package/src/blocks/e-commerce/product-list/product-card-5.tsx +0 -367
  145. package/src/blocks/e-commerce/product-overview/product-overview-1.test.tsx +0 -182
  146. package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +0 -370
  147. package/src/blocks/e-commerce/product-overview/product-overview-2.tsx +0 -272
  148. package/src/blocks/e-commerce/product-overview/product-overview-5-gallery.tsx +0 -59
  149. package/src/blocks/e-commerce/product-overview/product-overview-5.tsx +0 -274
  150. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1-spinning-counter.tsx +0 -142
  151. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1.tsx +0 -291
  152. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-item-row.tsx +0 -110
  153. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-recommendations.tsx +0 -51
  154. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-spinning-counter.tsx +0 -146
  155. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2.tsx +0 -295
  156. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3-spinning-counter.tsx +0 -142
  157. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3.tsx +0 -342
  158. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4-spinning-counter.tsx +0 -146
  159. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.test.tsx +0 -229
  160. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.test.tsx +0 -72
  161. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +0 -263
  162. package/src/blocks/e-commerce/storefront-hero/storefront-hero-2.tsx +0 -283
  163. package/src/blocks/e-commerce/storefront-hero/storefront-hero-5.tsx +0 -318
  164. package/src/blocks/e-commerce/storefront-hero/storefront-hero-6.tsx +0 -205
  165. package/src/blocks/marketing/features/feature-1.tsx +0 -97
  166. package/src/blocks/marketing/features/feature-2.tsx +0 -82
  167. package/src/blocks/marketing/footers/footer-section-1.test.tsx +0 -117
  168. package/src/blocks/marketing/footers/footer-section-1.tsx +0 -178
  169. package/src/blocks/marketing/footers/footer-section-3.tsx +0 -192
  170. package/src/blocks/marketing/footers/footer-section-4.tsx +0 -355
  171. package/src/blocks/marketing/navbars/navbar-1.tsx +0 -267
  172. package/src/blocks/marketing/navbars/navbar-2.tsx +0 -351
  173. package/src/blocks/marketing/navbars/navbar-3.tsx +0 -303
  174. package/src/blocks/marketing/navbars/navbar-4-account-menu.test.tsx +0 -38
  175. package/src/blocks/marketing/navbars/navbar-4-like-button.tsx +0 -177
  176. package/src/blocks/marketing/navbars/navbar-4-product-grid.tsx +0 -219
  177. package/src/blocks/marketing/testimonials/testimonial-1.tsx +0 -141
  178. 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
- })