@ecomiq/storefront 2.0.8 → 2.0.11
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/bin/ecomiq.mjs +1 -1
- package/package.json +2 -2
- package/src/api-proxy.ts +2 -2
- package/src/client.ts +2 -2
- package/src/codegen.mjs +3 -8
- package/src/components/checkout/checkout-contact-section.tsx +70 -0
- package/src/components/checkout/checkout-order-summary.tsx +11 -2
- package/src/components/checkout/checkout-payment-section.tsx +4 -7
- package/src/components/checkout/checkout-session-view.tsx +311 -68
- package/src/components/checkout/checkout-shipping-address-section.tsx +119 -90
- package/src/components/checkout/checkout-shipping-section.tsx +22 -14
- package/src/components/checkout/checkout-step-card.tsx +101 -0
- package/src/components/checkout/checkout-steps.ts +96 -0
- package/src/components/checkout/form-schema.ts +152 -100
- package/src/components/checkout/format.ts +0 -5
- package/src/components/checkout/index.ts +4 -50
- package/src/components/checkout/types.ts +0 -2
- package/src/components/checkout/use-checkout-session.ts +72 -15
- package/src/config.d.ts +6 -12
- package/src/config.mjs +5 -64
- package/src/index.ts +8 -4
- package/src/public-config.ts +10 -30
- package/src/runtime/default-layout.tsx +5 -21
- package/src/runtime/head.ts +70 -47
- package/src/runtime/page.tsx +45 -13
- package/src/server-config-store.ts +1 -1
- package/templates/__root.tsx +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useState } from 'react'
|
|
2
1
|
import type { Country, Department, District, Province } from '@ecomiq/core'
|
|
2
|
+
import { Button } from '../../ui'
|
|
3
3
|
import type { CheckoutContactForm } from './types'
|
|
4
4
|
import type { CheckoutFieldId } from './form-schema'
|
|
5
5
|
import {
|
|
@@ -9,6 +9,10 @@ import {
|
|
|
9
9
|
FieldError,
|
|
10
10
|
fieldClass,
|
|
11
11
|
} from './checkout-form-controls'
|
|
12
|
+
import {
|
|
13
|
+
CheckoutShippingSection,
|
|
14
|
+
type CheckoutShippingOption,
|
|
15
|
+
} from './checkout-shipping-section'
|
|
12
16
|
|
|
13
17
|
const DIAL_BY_COUNTRY: Record<string, string> = {
|
|
14
18
|
PE: '+51',
|
|
@@ -24,7 +28,7 @@ function dialCodeFor(countryCode: string) {
|
|
|
24
28
|
return DIAL_BY_COUNTRY[countryCode.toUpperCase()] ?? '+51'
|
|
25
29
|
}
|
|
26
30
|
|
|
27
|
-
/**
|
|
31
|
+
/** Paso 2: dirección de entrega + método de envío + facturación. */
|
|
28
32
|
export function CheckoutShippingAddressSection({
|
|
29
33
|
contact,
|
|
30
34
|
fieldErrors,
|
|
@@ -32,9 +36,16 @@ export function CheckoutShippingAddressSection({
|
|
|
32
36
|
departments,
|
|
33
37
|
provinces,
|
|
34
38
|
districts,
|
|
35
|
-
|
|
39
|
+
shippingOptions,
|
|
40
|
+
selectedShippingKey,
|
|
41
|
+
shippingLoading,
|
|
42
|
+
shippingError,
|
|
43
|
+
isSaving,
|
|
44
|
+
money,
|
|
36
45
|
onPatch,
|
|
46
|
+
onSelectShipping,
|
|
37
47
|
onClearError,
|
|
48
|
+
onContinue,
|
|
38
49
|
}: {
|
|
39
50
|
contact: CheckoutContactForm
|
|
40
51
|
fieldErrors: Partial<Record<CheckoutFieldId, string>>
|
|
@@ -42,22 +53,21 @@ export function CheckoutShippingAddressSection({
|
|
|
42
53
|
departments: Department[]
|
|
43
54
|
provinces: Province[]
|
|
44
55
|
districts: District[]
|
|
45
|
-
|
|
56
|
+
shippingOptions: CheckoutShippingOption[]
|
|
57
|
+
selectedShippingKey: string | null
|
|
58
|
+
shippingLoading: boolean
|
|
59
|
+
shippingError: string | null
|
|
60
|
+
isSaving?: boolean
|
|
61
|
+
money: (value: number) => string
|
|
46
62
|
onPatch: (patch: Partial<CheckoutContactForm>) => void
|
|
63
|
+
onSelectShipping: (key: string) => void
|
|
47
64
|
onClearError: (id: CheckoutFieldId) => void
|
|
65
|
+
onContinue: () => void | Promise<void>
|
|
48
66
|
}) {
|
|
49
67
|
const isPe = contact.countryCode.toUpperCase() === 'PE'
|
|
50
|
-
const [saveNextTime, setSaveNextTime] = useState(false)
|
|
51
68
|
|
|
52
69
|
return (
|
|
53
|
-
<
|
|
54
|
-
<h2 className="text-xl font-semibold tracking-tight">Dirección de envío</h2>
|
|
55
|
-
{sessionLabel ? (
|
|
56
|
-
<p className="text-sm text-muted-foreground">
|
|
57
|
-
Sesión como <span className="font-medium text-foreground">{sessionLabel}</span>
|
|
58
|
-
</p>
|
|
59
|
-
) : null}
|
|
60
|
-
|
|
70
|
+
<div className="space-y-6">
|
|
61
71
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
62
72
|
<CheckoutTextField
|
|
63
73
|
id="firstName"
|
|
@@ -83,75 +93,68 @@ export function CheckoutShippingAddressSection({
|
|
|
83
93
|
/>
|
|
84
94
|
</div>
|
|
85
95
|
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
autoComplete="email"
|
|
98
|
-
/>
|
|
99
|
-
<CheckoutPhoneField
|
|
100
|
-
id="phone"
|
|
101
|
-
label="Teléfono"
|
|
102
|
-
error={fieldErrors.phone}
|
|
103
|
-
value={contact.phone}
|
|
104
|
-
dialCode={dialCodeFor(contact.countryCode)}
|
|
105
|
-
onChange={(v) => {
|
|
106
|
-
onClearError('phone')
|
|
107
|
-
onPatch({ phone: v })
|
|
108
|
-
}}
|
|
109
|
-
/>
|
|
110
|
-
</div>
|
|
96
|
+
<CheckoutPhoneField
|
|
97
|
+
id="phone"
|
|
98
|
+
label="Teléfono"
|
|
99
|
+
error={fieldErrors.phone}
|
|
100
|
+
value={contact.phone}
|
|
101
|
+
dialCode={dialCodeFor(contact.countryCode)}
|
|
102
|
+
onChange={(v) => {
|
|
103
|
+
onClearError('phone')
|
|
104
|
+
onPatch({ phone: v })
|
|
105
|
+
}}
|
|
106
|
+
/>
|
|
111
107
|
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
108
|
+
<CheckoutTextField
|
|
109
|
+
id="address"
|
|
110
|
+
label="Dirección"
|
|
111
|
+
error={fieldErrors.address}
|
|
112
|
+
value={contact.address}
|
|
113
|
+
onChange={(v) => {
|
|
114
|
+
onClearError('address')
|
|
115
|
+
onPatch({ address: v })
|
|
116
|
+
}}
|
|
117
|
+
autoComplete="street-address"
|
|
118
|
+
/>
|
|
119
|
+
|
|
120
|
+
<CheckoutTextField
|
|
121
|
+
id="address2"
|
|
122
|
+
label="Información adicional de referencia"
|
|
123
|
+
value={contact.address2}
|
|
124
|
+
onChange={(v) => onPatch({ address2: v })}
|
|
125
|
+
autoComplete="address-line2"
|
|
126
|
+
/>
|
|
127
|
+
|
|
128
|
+
<div className="min-w-0">
|
|
129
|
+
<label htmlFor="countryCode" className="text-sm font-medium">
|
|
130
|
+
País
|
|
131
|
+
</label>
|
|
132
|
+
<select
|
|
133
|
+
id="countryCode"
|
|
134
|
+
className={fieldClass(fieldErrors.countryCode)}
|
|
135
|
+
value={contact.countryCode}
|
|
136
|
+
onChange={(e) => {
|
|
137
|
+
onClearError('countryCode')
|
|
138
|
+
onPatch({
|
|
139
|
+
countryCode: e.target.value,
|
|
140
|
+
departmentCode: '',
|
|
141
|
+
provinceCode: '',
|
|
142
|
+
districtCode: '',
|
|
143
|
+
departmentName: '',
|
|
144
|
+
provinceName: '',
|
|
145
|
+
districtName: '',
|
|
146
|
+
city: '',
|
|
147
|
+
province: '',
|
|
148
|
+
})
|
|
152
149
|
}}
|
|
153
|
-
|
|
154
|
-
|
|
150
|
+
>
|
|
151
|
+
{(countries.length ? countries : [{ code: 'PE', name: 'Perú' }]).map((c) => (
|
|
152
|
+
<option key={c.code} value={c.code}>
|
|
153
|
+
{c.name}
|
|
154
|
+
</option>
|
|
155
|
+
))}
|
|
156
|
+
</select>
|
|
157
|
+
<FieldError message={fieldErrors.countryCode} />
|
|
155
158
|
</div>
|
|
156
159
|
|
|
157
160
|
{isPe ? (
|
|
@@ -252,15 +255,41 @@ export function CheckoutShippingAddressSection({
|
|
|
252
255
|
/>
|
|
253
256
|
) : null}
|
|
254
257
|
|
|
255
|
-
<
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
258
|
+
<CheckoutShippingSection
|
|
259
|
+
options={shippingOptions}
|
|
260
|
+
selectedKey={selectedShippingKey}
|
|
261
|
+
loading={shippingLoading}
|
|
262
|
+
error={shippingError}
|
|
263
|
+
needsPostalCode={!contact.postalCode.trim()}
|
|
264
|
+
fieldError={fieldErrors['checkout-shipping-method']}
|
|
265
|
+
money={money}
|
|
266
|
+
onSelect={onSelectShipping}
|
|
267
|
+
onClearError={onClearError}
|
|
268
|
+
/>
|
|
269
|
+
|
|
270
|
+
<div className="space-y-3">
|
|
271
|
+
<h3 className="text-base font-semibold tracking-tight">Dirección de facturación</h3>
|
|
272
|
+
<label className="flex items-center gap-2.5 text-sm text-foreground">
|
|
273
|
+
<input
|
|
274
|
+
type="checkbox"
|
|
275
|
+
className="size-4 rounded border-border accent-foreground"
|
|
276
|
+
checked
|
|
277
|
+
disabled
|
|
278
|
+
readOnly
|
|
279
|
+
/>
|
|
280
|
+
Misma dirección que la de entrega
|
|
281
|
+
</label>
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
<Button
|
|
285
|
+
type="button"
|
|
286
|
+
className="w-full uppercase tracking-wide"
|
|
287
|
+
size="lg"
|
|
288
|
+
disabled={isSaving}
|
|
289
|
+
onClick={() => void onContinue()}
|
|
290
|
+
>
|
|
291
|
+
{isSaving ? 'Guardando…' : 'Enviar y continuar'}
|
|
292
|
+
</Button>
|
|
293
|
+
</div>
|
|
265
294
|
)
|
|
266
295
|
}
|
|
@@ -40,8 +40,8 @@ export function CheckoutShippingSection({
|
|
|
40
40
|
onClearError: (id: CheckoutFieldId) => void
|
|
41
41
|
}) {
|
|
42
42
|
return (
|
|
43
|
-
<
|
|
44
|
-
<
|
|
43
|
+
<div className="space-y-4">
|
|
44
|
+
<h3 className="text-base font-semibold tracking-tight">Envío</h3>
|
|
45
45
|
{loading ? <p className="text-sm text-muted-foreground">Cargando opciones…</p> : null}
|
|
46
46
|
{error ? <p className="text-sm text-destructive">{error}</p> : null}
|
|
47
47
|
{needsPostalCode ? (
|
|
@@ -50,17 +50,19 @@ export function CheckoutShippingSection({
|
|
|
50
50
|
</p>
|
|
51
51
|
) : null}
|
|
52
52
|
|
|
53
|
-
<div className="grid gap-3
|
|
53
|
+
<div id="checkout-shipping-method" className="grid gap-3">
|
|
54
54
|
{options.map((opt) => {
|
|
55
55
|
const selected = selectedKey === opt.key
|
|
56
56
|
return (
|
|
57
57
|
<button
|
|
58
58
|
key={opt.key}
|
|
59
59
|
type="button"
|
|
60
|
+
data-checkout-shipping={opt.key}
|
|
61
|
+
aria-pressed={selected}
|
|
60
62
|
className={cn(
|
|
61
|
-
'relative flex min-h-[
|
|
63
|
+
'relative flex min-h-[4.5rem] items-center justify-between gap-4 rounded-lg border bg-background px-4 py-3.5 text-left transition-colors',
|
|
62
64
|
selected
|
|
63
|
-
? 'border-
|
|
65
|
+
? 'border-foreground ring-1 ring-foreground'
|
|
64
66
|
: 'border-border [@media(hover:hover)]:hover:border-foreground/30',
|
|
65
67
|
)}
|
|
66
68
|
onClick={() => {
|
|
@@ -68,23 +70,29 @@ export function CheckoutShippingSection({
|
|
|
68
70
|
onSelect(opt.key)
|
|
69
71
|
}}
|
|
70
72
|
>
|
|
73
|
+
<span className="min-w-0 pr-8">
|
|
74
|
+
<span className="block text-sm font-semibold">{opt.rate.name}</span>
|
|
75
|
+
<span className="mt-1 block text-xs text-muted-foreground">
|
|
76
|
+
{shippingSubtitle(opt.rate)}
|
|
77
|
+
</span>
|
|
78
|
+
</span>
|
|
79
|
+
<span className="shrink-0 text-right text-sm font-semibold tabular-nums">
|
|
80
|
+
{opt.rate.price === 0 ? (
|
|
81
|
+
'GRATIS'
|
|
82
|
+
) : (
|
|
83
|
+
money(opt.rate.price)
|
|
84
|
+
)}
|
|
85
|
+
</span>
|
|
71
86
|
{selected ? (
|
|
72
|
-
<span className="absolute right-3 top-3 flex size-5 items-center justify-center rounded-full bg-
|
|
87
|
+
<span className="absolute right-3 top-3 flex size-5 items-center justify-center rounded-full bg-foreground text-background">
|
|
73
88
|
<Check className="size-3" strokeWidth={3} aria-hidden="true" />
|
|
74
89
|
</span>
|
|
75
90
|
) : null}
|
|
76
|
-
<span className="pr-8 text-sm font-semibold">{opt.rate.name}</span>
|
|
77
|
-
<span className="mt-1 text-xs text-muted-foreground">
|
|
78
|
-
{shippingSubtitle(opt.rate)}
|
|
79
|
-
</span>
|
|
80
|
-
<span className="mt-auto pt-3 text-sm font-semibold tabular-nums">
|
|
81
|
-
{opt.rate.price === 0 ? 'Gratis' : money(opt.rate.price)}
|
|
82
|
-
</span>
|
|
83
91
|
</button>
|
|
84
92
|
)
|
|
85
93
|
})}
|
|
86
94
|
</div>
|
|
87
95
|
<FieldError message={fieldError} />
|
|
88
|
-
</
|
|
96
|
+
</div>
|
|
89
97
|
)
|
|
90
98
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { ComponentType, ReactNode, SVGProps } from 'react'
|
|
2
|
+
import { cn } from '../../ui'
|
|
3
|
+
import type { CheckoutStepId } from './checkout-steps'
|
|
4
|
+
import { checkoutStepStatus, type CheckoutStepFlow } from './checkout-steps'
|
|
5
|
+
|
|
6
|
+
type StepIcon = ComponentType<SVGProps<SVGSVGElement>>
|
|
7
|
+
|
|
8
|
+
export function CheckoutStepCard({
|
|
9
|
+
step,
|
|
10
|
+
flow,
|
|
11
|
+
title,
|
|
12
|
+
icon: Icon,
|
|
13
|
+
summary,
|
|
14
|
+
onEdit,
|
|
15
|
+
children,
|
|
16
|
+
}: {
|
|
17
|
+
step: CheckoutStepId
|
|
18
|
+
flow: CheckoutStepFlow
|
|
19
|
+
title: string
|
|
20
|
+
icon: StepIcon
|
|
21
|
+
summary?: ReactNode
|
|
22
|
+
onEdit?: () => void
|
|
23
|
+
children?: ReactNode
|
|
24
|
+
}) {
|
|
25
|
+
const status = checkoutStepStatus(flow, step)
|
|
26
|
+
const locked = status === 'locked'
|
|
27
|
+
const complete = status === 'complete'
|
|
28
|
+
const active = status === 'active'
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<section
|
|
32
|
+
aria-labelledby={`checkout-step-${step}-title`}
|
|
33
|
+
aria-disabled={locked || undefined}
|
|
34
|
+
className={cn(
|
|
35
|
+
'overflow-hidden rounded-xl border bg-card shadow-sm transition-opacity',
|
|
36
|
+
locked && 'opacity-55',
|
|
37
|
+
)}
|
|
38
|
+
>
|
|
39
|
+
<header
|
|
40
|
+
className={cn(
|
|
41
|
+
'flex items-center gap-3 border-b px-4 py-3.5 sm:px-5',
|
|
42
|
+
locked && 'border-transparent',
|
|
43
|
+
complete && 'border-border',
|
|
44
|
+
active && 'border-border',
|
|
45
|
+
)}
|
|
46
|
+
>
|
|
47
|
+
<Icon
|
|
48
|
+
className={cn(
|
|
49
|
+
'size-5 shrink-0',
|
|
50
|
+
locked ? 'text-muted-foreground/70' : 'text-foreground',
|
|
51
|
+
)}
|
|
52
|
+
aria-hidden="true"
|
|
53
|
+
/>
|
|
54
|
+
<h2
|
|
55
|
+
id={`checkout-step-${step}-title`}
|
|
56
|
+
className={cn(
|
|
57
|
+
'min-w-0 flex-1 text-base font-semibold tracking-tight',
|
|
58
|
+
locked && 'text-muted-foreground',
|
|
59
|
+
)}
|
|
60
|
+
>
|
|
61
|
+
{title}
|
|
62
|
+
</h2>
|
|
63
|
+
{complete && onEdit ? (
|
|
64
|
+
<button
|
|
65
|
+
type="button"
|
|
66
|
+
className="text-xs font-semibold uppercase tracking-[0.08em] text-foreground underline-offset-4 hover:underline"
|
|
67
|
+
onClick={onEdit}
|
|
68
|
+
>
|
|
69
|
+
Editar
|
|
70
|
+
</button>
|
|
71
|
+
) : null}
|
|
72
|
+
</header>
|
|
73
|
+
|
|
74
|
+
{complete && summary ? (
|
|
75
|
+
<div className="space-y-3 px-4 py-4 sm:px-5">{summary}</div>
|
|
76
|
+
) : null}
|
|
77
|
+
|
|
78
|
+
{active ? <div className="space-y-5 px-4 py-5 sm:px-5">{children}</div> : null}
|
|
79
|
+
</section>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function CheckoutStepSummaryBlock({
|
|
84
|
+
children,
|
|
85
|
+
className,
|
|
86
|
+
}: {
|
|
87
|
+
children: ReactNode
|
|
88
|
+
className?: string
|
|
89
|
+
}) {
|
|
90
|
+
return (
|
|
91
|
+
<div
|
|
92
|
+
className={cn(
|
|
93
|
+
'relative rounded-lg border border-border bg-background py-3 pl-4 pr-3',
|
|
94
|
+
'before:absolute before:inset-y-2 before:left-0 before:w-0.5 before:rounded-full before:bg-foreground',
|
|
95
|
+
className,
|
|
96
|
+
)}
|
|
97
|
+
>
|
|
98
|
+
{children}
|
|
99
|
+
</div>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export const CHECKOUT_STEPS = ['contact', 'delivery', 'payment'] as const
|
|
2
|
+
|
|
3
|
+
export type CheckoutStepId = (typeof CHECKOUT_STEPS)[number]
|
|
4
|
+
|
|
5
|
+
export type CheckoutStepFlow = {
|
|
6
|
+
active: CheckoutStepId
|
|
7
|
+
completed: Record<CheckoutStepId, boolean>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function createCheckoutStepFlow(
|
|
11
|
+
active: CheckoutStepId = 'contact',
|
|
12
|
+
): CheckoutStepFlow {
|
|
13
|
+
return {
|
|
14
|
+
active,
|
|
15
|
+
completed: {
|
|
16
|
+
contact: false,
|
|
17
|
+
delivery: false,
|
|
18
|
+
payment: false,
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function checkoutStepIndex(step: CheckoutStepId): number {
|
|
24
|
+
return CHECKOUT_STEPS.indexOf(step)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isCheckoutStepLocked(
|
|
28
|
+
flow: CheckoutStepFlow,
|
|
29
|
+
step: CheckoutStepId,
|
|
30
|
+
): boolean {
|
|
31
|
+
return checkoutStepIndex(step) > checkoutStepIndex(flow.active)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function isCheckoutStepActive(
|
|
35
|
+
flow: CheckoutStepFlow,
|
|
36
|
+
step: CheckoutStepId,
|
|
37
|
+
): boolean {
|
|
38
|
+
return flow.active === step
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function isCheckoutStepComplete(
|
|
42
|
+
flow: CheckoutStepFlow,
|
|
43
|
+
step: CheckoutStepId,
|
|
44
|
+
): boolean {
|
|
45
|
+
return flow.completed[step] && flow.active !== step
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Completa el paso activo y avanza al siguiente (si existe). */
|
|
49
|
+
export function completeCheckoutStep(
|
|
50
|
+
flow: CheckoutStepFlow,
|
|
51
|
+
step: CheckoutStepId,
|
|
52
|
+
): CheckoutStepFlow {
|
|
53
|
+
if (flow.active !== step) return flow
|
|
54
|
+
const idx = checkoutStepIndex(step)
|
|
55
|
+
const next = CHECKOUT_STEPS[idx + 1]
|
|
56
|
+
return {
|
|
57
|
+
active: next ?? step,
|
|
58
|
+
completed: {
|
|
59
|
+
...flow.completed,
|
|
60
|
+
[step]: true,
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Reabre un paso ya completado para editarlo e invalida todos los posteriores.
|
|
67
|
+
*/
|
|
68
|
+
export function editCheckoutStep(
|
|
69
|
+
flow: CheckoutStepFlow,
|
|
70
|
+
step: CheckoutStepId,
|
|
71
|
+
): CheckoutStepFlow {
|
|
72
|
+
const idx = checkoutStepIndex(step)
|
|
73
|
+
if (idx < 0) return flow
|
|
74
|
+
if (checkoutStepIndex(flow.active) < idx && !flow.completed[step]) {
|
|
75
|
+
return flow
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const completed = { ...flow.completed }
|
|
79
|
+
for (const id of CHECKOUT_STEPS) {
|
|
80
|
+
if (checkoutStepIndex(id) >= idx) completed[id] = false
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
active: step,
|
|
85
|
+
completed,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function checkoutStepStatus(
|
|
90
|
+
flow: CheckoutStepFlow,
|
|
91
|
+
step: CheckoutStepId,
|
|
92
|
+
): 'active' | 'complete' | 'locked' {
|
|
93
|
+
if (isCheckoutStepActive(flow, step)) return 'active'
|
|
94
|
+
if (isCheckoutStepComplete(flow, step)) return 'complete'
|
|
95
|
+
return 'locked'
|
|
96
|
+
}
|