@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.
@@ -59,105 +59,146 @@ export type CheckoutFormValidationContext = {
59
59
  paymentMethodsCount: number
60
60
  }
61
61
 
62
- export function buildCheckoutFormValidationSchema(currency: string) {
63
- const phoneDigits = currency.toUpperCase() === 'PEN' ? 9 : 10
64
-
65
- return z
66
- .object({
67
- email: z
68
- .string()
69
- .trim()
70
- .min(1, 'El email es requerido')
71
- .email('Ingresa un email válido'),
72
- firstName: z
73
- .string()
74
- .trim()
75
- .min(1, 'El nombre es requerido')
76
- .min(2, 'El nombre debe tener al menos 2 caracteres'),
77
- lastName: z
78
- .string()
79
- .trim()
80
- .min(1, 'El apellido es requerido')
81
- .min(2, 'El apellido debe tener al menos 2 caracteres'),
82
- countryCode: z.string().trim().min(2, 'Selecciona un país').max(2),
83
- address: z
84
- .string()
85
- .trim()
86
- .min(1, 'La dirección es requerida')
87
- .min(5, 'La dirección debe tener al menos 5 caracteres'),
88
- phone: z
89
- .string()
90
- .trim()
91
- .min(1, 'El teléfono es requerido')
92
- .regex(/^\d+$/, 'El teléfono solo debe contener números')
93
- .length(phoneDigits, `El teléfono debe tener ${phoneDigits} dígitos`),
94
- departmentCode: z.string(),
95
- provinceCode: z.string(),
96
- districtCode: z.string(),
97
- city: z.string(),
98
- province: z.string(),
99
- selectedShippingKey: z.string().nullable(),
100
- shippingOptionsCount: z.number().int().nonnegative(),
101
- selectedPaymentCode: z.string().nullable(),
102
- paymentMethodsCount: z.number().int().nonnegative(),
62
+ function phoneDigitsForCurrency(currency: string) {
63
+ return currency.toUpperCase() === 'PEN' ? 9 : 10
64
+ }
65
+
66
+ const contactStepObject = z.object({
67
+ email: z
68
+ .string()
69
+ .trim()
70
+ .min(1, 'El email es requerido')
71
+ .email('Ingresa un email válido'),
72
+ })
73
+
74
+ function deliveryStepObject(currency: string) {
75
+ const phoneDigits = phoneDigitsForCurrency(currency)
76
+ return z.object({
77
+ firstName: z
78
+ .string()
79
+ .trim()
80
+ .min(1, 'El nombre es requerido')
81
+ .min(2, 'El nombre debe tener al menos 2 caracteres'),
82
+ lastName: z
83
+ .string()
84
+ .trim()
85
+ .min(1, 'El apellido es requerido')
86
+ .min(2, 'El apellido debe tener al menos 2 caracteres'),
87
+ countryCode: z.string().trim().min(2, 'Selecciona un país').max(2),
88
+ address: z
89
+ .string()
90
+ .trim()
91
+ .min(1, 'La dirección es requerida')
92
+ .min(5, 'La dirección debe tener al menos 5 caracteres'),
93
+ phone: z
94
+ .string()
95
+ .trim()
96
+ .min(1, 'El teléfono es requerido')
97
+ .regex(/^\d+$/, 'El teléfono solo debe contener números')
98
+ .length(phoneDigits, `El teléfono debe tener ${phoneDigits} dígitos`),
99
+ departmentCode: z.string(),
100
+ provinceCode: z.string(),
101
+ districtCode: z.string(),
102
+ city: z.string(),
103
+ province: z.string(),
104
+ selectedShippingKey: z.string().nullable(),
105
+ shippingOptionsCount: z.number().int().nonnegative(),
106
+ })
107
+ }
108
+
109
+ const paymentStepObject = z.object({
110
+ selectedPaymentCode: z.string().nullable(),
111
+ paymentMethodsCount: z.number().int().nonnegative(),
112
+ })
113
+
114
+ function refineDelivery(
115
+ data: z.infer<ReturnType<typeof deliveryStepObject>>,
116
+ ctx: z.RefinementCtx,
117
+ ) {
118
+ const country = data.countryCode.toUpperCase()
119
+
120
+ if (country === 'PE') {
121
+ if (!data.departmentCode.trim()) {
122
+ ctx.addIssue({
123
+ code: z.ZodIssueCode.custom,
124
+ path: ['departmentCode'],
125
+ message: 'Selecciona un departamento',
126
+ })
127
+ }
128
+ if (!data.provinceCode.trim()) {
129
+ ctx.addIssue({
130
+ code: z.ZodIssueCode.custom,
131
+ path: ['provinceCode'],
132
+ message: 'Selecciona una provincia',
133
+ })
134
+ }
135
+ if (!data.districtCode.trim()) {
136
+ ctx.addIssue({
137
+ code: z.ZodIssueCode.custom,
138
+ path: ['districtCode'],
139
+ message: 'Selecciona un distrito',
140
+ })
141
+ }
142
+ } else {
143
+ if (!data.city.trim()) {
144
+ ctx.addIssue({
145
+ code: z.ZodIssueCode.custom,
146
+ path: ['city'],
147
+ message: 'La ciudad es requerida',
148
+ })
149
+ }
150
+ if (!data.province.trim()) {
151
+ ctx.addIssue({
152
+ code: z.ZodIssueCode.custom,
153
+ path: ['province'],
154
+ message: 'La región es requerida',
155
+ })
156
+ }
157
+ }
158
+
159
+ if (data.shippingOptionsCount > 0 && !data.selectedShippingKey?.trim()) {
160
+ ctx.addIssue({
161
+ code: z.ZodIssueCode.custom,
162
+ path: ['selectedShippingKey'],
163
+ message: 'Selecciona un método de envío',
164
+ })
165
+ }
166
+ }
167
+
168
+ function refinePayment(
169
+ data: z.infer<typeof paymentStepObject>,
170
+ ctx: z.RefinementCtx,
171
+ ) {
172
+ if (data.paymentMethodsCount > 0 && !data.selectedPaymentCode?.trim()) {
173
+ ctx.addIssue({
174
+ code: z.ZodIssueCode.custom,
175
+ path: ['selectedPaymentCode'],
176
+ message: 'Selecciona un método de pago',
103
177
  })
178
+ }
179
+ }
180
+
181
+ export function buildCheckoutContactStepSchema() {
182
+ return contactStepObject
183
+ }
184
+
185
+ export function buildCheckoutDeliveryStepSchema(currency: string) {
186
+ return deliveryStepObject(currency).superRefine(refineDelivery)
187
+ }
188
+
189
+ export function buildCheckoutPaymentStepSchema() {
190
+ return paymentStepObject.superRefine(refinePayment)
191
+ }
192
+
193
+ export function buildCheckoutFormValidationSchema(currency: string) {
194
+ const delivery = deliveryStepObject(currency)
195
+
196
+ return contactStepObject
197
+ .merge(delivery)
198
+ .merge(paymentStepObject)
104
199
  .superRefine((data, ctx) => {
105
- const country = data.countryCode.toUpperCase()
106
-
107
- if (country === 'PE') {
108
- if (!data.departmentCode.trim()) {
109
- ctx.addIssue({
110
- code: z.ZodIssueCode.custom,
111
- path: ['departmentCode'],
112
- message: 'Selecciona un departamento',
113
- })
114
- }
115
- if (!data.provinceCode.trim()) {
116
- ctx.addIssue({
117
- code: z.ZodIssueCode.custom,
118
- path: ['provinceCode'],
119
- message: 'Selecciona una provincia',
120
- })
121
- }
122
- if (!data.districtCode.trim()) {
123
- ctx.addIssue({
124
- code: z.ZodIssueCode.custom,
125
- path: ['districtCode'],
126
- message: 'Selecciona un distrito',
127
- })
128
- }
129
- } else {
130
- if (!data.city.trim()) {
131
- ctx.addIssue({
132
- code: z.ZodIssueCode.custom,
133
- path: ['city'],
134
- message: 'La ciudad es requerida',
135
- })
136
- }
137
- if (!data.province.trim()) {
138
- ctx.addIssue({
139
- code: z.ZodIssueCode.custom,
140
- path: ['province'],
141
- message: 'La región es requerida',
142
- })
143
- }
144
- }
145
-
146
- if (data.shippingOptionsCount > 0 && !data.selectedShippingKey?.trim()) {
147
- ctx.addIssue({
148
- code: z.ZodIssueCode.custom,
149
- path: ['selectedShippingKey'],
150
- message: 'Selecciona un método de envío',
151
- })
152
- }
153
-
154
- if (data.paymentMethodsCount > 0 && !data.selectedPaymentCode?.trim()) {
155
- ctx.addIssue({
156
- code: z.ZodIssueCode.custom,
157
- path: ['selectedPaymentCode'],
158
- message: 'Selecciona un método de pago',
159
- })
160
- }
200
+ refineDelivery(data, ctx)
201
+ refinePayment(data, ctx)
161
202
  })
162
203
  }
163
204
 
@@ -216,9 +257,11 @@ export function focusCheckoutField(fieldId: CheckoutFieldId): void {
216
257
  return
217
258
  }
218
259
  if (fieldId === 'checkout-shipping-method') {
219
- const radio = document.querySelector<HTMLInputElement>('input[name="shipping"]')
220
- radio?.focus()
221
- radio?.scrollIntoView({ behavior: 'smooth', block: 'center' })
260
+ const selected =
261
+ document.querySelector<HTMLButtonElement>('button[data-checkout-shipping][aria-pressed="true"]') ??
262
+ document.querySelector<HTMLButtonElement>('#checkout-shipping-method button')
263
+ selected?.focus()
264
+ selected?.scrollIntoView({ behavior: 'smooth', block: 'center' })
222
265
  return
223
266
  }
224
267
  if (fieldId === 'checkout-payment-method') {
@@ -227,3 +270,12 @@ export function focusCheckoutField(fieldId: CheckoutFieldId): void {
227
270
  radio?.scrollIntoView({ behavior: 'smooth', block: 'center' })
228
271
  }
229
272
  }
273
+
274
+ /** Mapea el primer campo inválido al paso del flujo secuencial. */
275
+ export function checkoutStepForField(
276
+ fieldId: CheckoutFieldId,
277
+ ): 'contact' | 'delivery' | 'payment' {
278
+ if (fieldId === 'email') return 'contact'
279
+ if (fieldId === 'checkout-payment-method') return 'payment'
280
+ return 'delivery'
281
+ }
@@ -15,11 +15,6 @@ export function thankYouUrl(cartId: string, transactionId?: string) {
15
15
  return `/thank-you?${q.toString()}`
16
16
  }
17
17
 
18
- /** @deprecated Usa `thankYouUrl`. */
19
- export function processingUrl(cartId: string, transactionId?: string) {
20
- return thankYouUrl(cartId, transactionId)
21
- }
22
-
23
18
  export function paymentRedirectUrl(result: CartCheckoutResponse): string | null {
24
19
  const actionUrl = result.payment?.action?.url
25
20
  if (typeof actionUrl === 'string' && actionUrl.trim()) return actionUrl
@@ -1,51 +1,5 @@
1
- export type { CheckoutContactForm, BillingChoice } from './types'
2
- export { emptyCheckoutContact } from './types'
3
-
4
- export {
5
- buildCheckoutFormValidationSchema,
6
- checkoutValidationInputFromContext,
7
- firstInvalidFieldId,
8
- focusCheckoutField,
9
- mapZodIssuesToFieldErrors,
10
- CHECKOUT_FIELD_IDS,
11
- CHECKOUT_FIELD_FOCUS_ORDER,
12
- type CheckoutFieldId,
13
- type CheckoutFormValidationContext,
14
- } from './form-schema'
15
-
16
- export {
17
- areCartSignaturesEqual,
18
- cartItemsSignature,
19
- cartSessionSignature,
20
- findCartReconciliationActions,
21
- isCheckoutCartAlreadyPurchased,
22
- methodUsesProviderCheckoutUi,
23
- toSignatureItems,
24
- type CartLineSignatureItem,
25
- type CartReconcileAction,
26
- } from './cart-sync'
27
-
28
- export { formatMoney, paymentRedirectUrl, thankYouUrl, processingUrl } from './format'
29
- export { useCheckoutSession } from './use-checkout-session'
30
- export { useCheckoutGeo } from './use-checkout-geo'
31
-
1
+ /**
2
+ * Superficie interna del checkout. La página solo necesita `CheckoutSessionView`;
3
+ * el resto se importa por path directo en tests o módulos hermanos.
4
+ */
32
5
  export { CheckoutSessionView } from './checkout-session-view'
33
- export { CheckoutShippingAddressSection } from './checkout-shipping-address-section'
34
- export { CheckoutShippingSection } from './checkout-shipping-section'
35
- export { CheckoutPaymentSection } from './checkout-payment-section'
36
- export { CheckoutOrderSummary } from './checkout-order-summary'
37
- export { CheckoutOrderBumps } from './checkout-line-items'
38
- export { CheckoutExternalUpdateBanner } from './checkout-external-update-banner'
39
- export {
40
- CheckoutAlreadyPurchasedState,
41
- CheckoutEmptyState,
42
- CheckoutErrorState,
43
- CheckoutLoadingState,
44
- } from './checkout-states'
45
- export {
46
- CheckoutTextField,
47
- CheckoutSelectField,
48
- CheckoutPhoneField,
49
- FieldError,
50
- fieldClass,
51
- } from './checkout-form-controls'
@@ -20,8 +20,6 @@ export type CheckoutContactForm = {
20
20
  districtName: string
21
21
  }
22
22
 
23
- export type BillingChoice = 'same_as_shipping' | 'different'
24
-
25
23
  export const emptyCheckoutContact = (countryCode = 'PE'): CheckoutContactForm => ({
26
24
  email: '',
27
25
  firstName: '',
@@ -25,7 +25,7 @@ import {
25
25
  methodUsesProviderCheckoutUi,
26
26
  toSignatureItems,
27
27
  } from './cart-sync'
28
- import { emptyCheckoutContact, type BillingChoice, type CheckoutContactForm } from './types'
28
+ import { emptyCheckoutContact, type CheckoutContactForm } from './types'
29
29
 
30
30
  type ShippingFlat = {
31
31
  key: string
@@ -46,7 +46,6 @@ export function useCheckoutSession(cartId: string) {
46
46
  const [cartChangedExternally, setCartChangedExternally] = useState(false)
47
47
 
48
48
  const [contact, setContact] = useState<CheckoutContactForm>(() => emptyCheckoutContact())
49
- const [billingChoice, setBillingChoice] = useState<BillingChoice>('same_as_shipping')
50
49
  const [selectedShippingKey, setSelectedShippingKey] = useState<string | null>(null)
51
50
  const [selectedPaymentCode, setSelectedPaymentCode] = useState<string | null>(null)
52
51
  const [providerPaymentCompleted, setProviderPaymentCompleted] = useState(false)
@@ -66,13 +65,13 @@ export function useCheckoutSession(cartId: string) {
66
65
  )
67
66
  const [checkoutPaymentLoading, setCheckoutPaymentLoading] = useState(false)
68
67
  const [checkoutPaymentError, setCheckoutPaymentError] = useState<string | null>(null)
68
+ const [deliveryPersisted, setDeliveryPersisted] = useState(false)
69
69
 
70
70
  const hydratedRef = useRef(false)
71
71
  const reconcileAttemptRef = useRef<string | null>(null)
72
72
  const isReconcilingRef = useRef(false)
73
73
  const checkoutPaymentInitKeyRef = useRef<string | null>(null)
74
74
  const baselineSigRef = useRef<string | null>(null)
75
- const skipClearOnPurchasedRef = useRef(false)
76
75
 
77
76
  const localItems = useMemo(
78
77
  () => toSignatureItems(providerCart?.items),
@@ -93,7 +92,6 @@ export function useCheckoutSession(cartId: string) {
93
92
  try {
94
93
  const data = await serverApi.getCartCheckout({ cartId })
95
94
  if (isCheckoutCartAlreadyPurchased(data.cart)) {
96
- skipClearOnPurchasedRef.current = true
97
95
  setCheckoutAlreadyPurchased(true)
98
96
  clearCart()
99
97
  setCheckoutCart(data.cart)
@@ -124,7 +122,7 @@ export function useCheckoutSession(cartId: string) {
124
122
  setProviderPaymentCompleted(false)
125
123
  setCheckoutPaymentResponse(null)
126
124
  setCheckoutPaymentMethodCode(null)
127
- skipClearOnPurchasedRef.current = false
125
+ setDeliveryPersisted(false)
128
126
  void loadCheckout()
129
127
  }, [cartId, loadCheckout])
130
128
 
@@ -337,13 +335,23 @@ export function useCheckoutSession(cartId: string) {
337
335
  [initProviderCheckout],
338
336
  )
339
337
 
340
- useEffect(() => {
341
- if (!paymentMethods.length || selectedPaymentCode) return
338
+ const clearPaymentSession = useCallback(() => {
339
+ checkoutPaymentInitKeyRef.current = null
340
+ setSelectedPaymentCode(null)
341
+ setProviderPaymentCompleted(false)
342
+ setCheckoutPaymentResponse(null)
343
+ setCheckoutPaymentMethodCode(null)
344
+ setCheckoutPaymentError(null)
345
+ setCheckoutPaymentLoading(false)
346
+ }, [])
347
+
348
+ const ensureDefaultPaymentMethod = useCallback(() => {
349
+ if (selectedPaymentCode || !paymentMethods.length) return
342
350
  const first = paymentMethods.find((m) => m.isDefault) ?? paymentMethods[0]
343
351
  if (first) selectPaymentMethod(first.code)
344
- }, [paymentMethods, selectedPaymentCode, selectPaymentMethod])
352
+ }, [paymentMethods, selectPaymentMethod, selectedPaymentCode])
345
353
 
346
- const saveContactAndDelivery = useCallback(async () => {
354
+ const saveBuyer = useCallback(async () => {
347
355
  const buyerCart = await serverApi.patchCartBuyer({
348
356
  cartId,
349
357
  email: contact.email,
@@ -352,7 +360,10 @@ export function useCheckoutSession(cartId: string) {
352
360
  phone: contact.phone,
353
361
  })
354
362
  setCheckoutCart(buyerCart)
363
+ return buyerCart
364
+ }, [cartId, contact.email, contact.firstName, contact.lastName, contact.phone])
355
365
 
366
+ const saveAddresses = useCallback(async () => {
356
367
  const countryCode = (contact.countryCode || 'PE').toUpperCase()
357
368
  const districtName = contact.districtName || contact.city
358
369
  const provinceName = contact.provinceName || contact.province
@@ -382,8 +393,14 @@ export function useCheckoutSession(cartId: string) {
382
393
  billingAddress: baseAddress,
383
394
  })
384
395
  setCheckoutCart(updated)
396
+ return updated
385
397
  }, [cartId, contact])
386
398
 
399
+ const saveContactAndDelivery = useCallback(async () => {
400
+ await saveBuyer()
401
+ await saveAddresses()
402
+ }, [saveAddresses, saveBuyer])
403
+
387
404
  const confirmShipping = useCallback(
388
405
  async (picked: ShippingFlat) => {
389
406
  const updated = await serverApi.putCartShippingMethod({
@@ -399,10 +416,44 @@ export function useCheckoutSession(cartId: string) {
399
416
  scheduledTime: null,
400
417
  })
401
418
  setCheckoutCart(updated)
419
+ return updated
402
420
  },
403
421
  [cartId],
404
422
  )
405
423
 
424
+ /** Persiste buyer + dirección + envío al completar el paso Dirección. */
425
+ const persistDeliveryStep = useCallback(async () => {
426
+ setFormError(null)
427
+ setIsSaving(true)
428
+ try {
429
+ await saveContactAndDelivery()
430
+ if (selectedShippingKey) {
431
+ const picked = shippingFlat.find((o) => o.key === selectedShippingKey)
432
+ if (picked) await confirmShipping(picked)
433
+ }
434
+ setDeliveryPersisted(true)
435
+ clearPaymentSession()
436
+ } catch (error) {
437
+ setFormError(
438
+ error instanceof Error ? error.message : 'No se pudo guardar la dirección.',
439
+ )
440
+ throw error
441
+ } finally {
442
+ setIsSaving(false)
443
+ }
444
+ }, [
445
+ clearPaymentSession,
446
+ confirmShipping,
447
+ saveContactAndDelivery,
448
+ selectedShippingKey,
449
+ shippingFlat,
450
+ ])
451
+
452
+ const invalidateDeliveryPersistence = useCallback(() => {
453
+ setDeliveryPersisted(false)
454
+ clearPaymentSession()
455
+ }, [clearPaymentSession])
456
+
406
457
  const handleApplyCoupon = useCallback(async () => {
407
458
  const code = couponCode.trim()
408
459
  if (!code) return
@@ -455,10 +506,13 @@ export function useCheckoutSession(cartId: string) {
455
506
  setFormError(null)
456
507
  setIsSaving(true)
457
508
  try {
458
- await saveContactAndDelivery()
459
- if (selectedShippingKey) {
460
- const picked = shippingFlat.find((o) => o.key === selectedShippingKey)
461
- if (picked) await confirmShipping(picked)
509
+ if (!deliveryPersisted) {
510
+ await saveContactAndDelivery()
511
+ if (selectedShippingKey) {
512
+ const picked = shippingFlat.find((o) => o.key === selectedShippingKey)
513
+ if (picked) await confirmShipping(picked)
514
+ }
515
+ setDeliveryPersisted(true)
462
516
  }
463
517
  const code =
464
518
  selectedPaymentCode ?? paymentMethods[0]?.code
@@ -507,6 +561,7 @@ export function useCheckoutSession(cartId: string) {
507
561
  checkoutPaymentMethodCode,
508
562
  checkoutPaymentResponse,
509
563
  confirmShipping,
564
+ deliveryPersisted,
510
565
  externalRef,
511
566
  paymentMethods,
512
567
  providerPaymentCompleted,
@@ -548,12 +603,14 @@ export function useCheckoutSession(cartId: string) {
548
603
  clearCart,
549
604
  contact,
550
605
  setContact,
551
- billingChoice,
552
- setBillingChoice,
553
606
  selectedShippingKey,
554
607
  setSelectedShippingKey,
555
608
  selectedPaymentCode,
556
609
  selectPaymentMethod,
610
+ ensureDefaultPaymentMethod,
611
+ clearPaymentSession,
612
+ persistDeliveryStep,
613
+ invalidateDeliveryPersistence,
557
614
  providerPaymentCompleted,
558
615
  setProviderPaymentCompleted,
559
616
  payNow,
package/src/config.d.ts CHANGED
@@ -1,9 +1,4 @@
1
- import type {
2
- EcomiqAnalyticsConfig,
3
- EcomiqLayoutConfig,
4
- EcomiqSeoConfig,
5
- EcomiqThemeConfig,
6
- } from './index'
1
+ import type { EcomiqThemeConfig } from './index'
7
2
 
8
3
  export interface EcomiqApiConfig {
9
4
  baseUrl?: string
@@ -32,14 +27,13 @@ export type EcomiqCloudflareRoute =
32
27
  }
33
28
 
34
29
  export interface EcomiqConfigInput {
35
- name: string
36
- slug: string
37
- description?: string
30
+ /**
31
+ * Identidad de tienda para `x-Store-Domain`.
32
+ * Acepta `marketplace`, `marketplace.ecomiq.app` o `marketplace.pe`.
33
+ */
34
+ domain: string
38
35
  api?: EcomiqApiConfig
39
36
  theme?: EcomiqThemeConfig
40
- seo?: EcomiqSeoConfig
41
- analytics?: EcomiqAnalyticsConfig
42
- layout?: EcomiqLayoutConfig
43
37
  dev?: {
44
38
  port?: number
45
39
  apiBaseUrl?: string
package/src/config.mjs CHANGED
@@ -1,6 +1,9 @@
1
1
  /**
2
2
  * Configuración base de un proyecto Ecomiq (ecomiq.config.js).
3
3
  * Se valida con Zod al arrancar la CLI para dar errores claros.
4
+ *
5
+ * Identidad de tienda, SEO, analytics y menús salen de `/v1/settings`.
6
+ * Aquí solo queda lo operativo: domain, api, theme, dev, deploy, env.
4
7
  */
5
8
  import { z } from 'zod'
6
9
 
@@ -31,15 +34,11 @@ const cloudflareRouteSchema = z.union([
31
34
  ])
32
35
 
33
36
  const configSchema = z.object({
34
- /** Nombre visible de la tienda (header, <title>, worker por defecto). */
35
- name: z.string().min(1),
36
37
  /**
37
38
  * Identidad de la tienda en el API storefront (header x-Store-Domain).
38
- * Ej. 'marketplace'.
39
+ * Acepta slug corto o dominio: `marketplace`, `marketplace.ecomiq.app`, `marketplace.pe`.
39
40
  */
40
- slug: z.string().min(1),
41
- /** Descripción para SEO por defecto. */
42
- description: z.string().optional(),
41
+ domain: z.string().min(1),
43
42
  /** API de storefront. baseUrl default: https://storefront.ecomiq.pe */
44
43
  api: z
45
44
  .object({
@@ -62,64 +61,6 @@ const configSchema = z.object({
62
61
  radius: z.string().optional(),
63
62
  })
64
63
  .optional(),
65
- /** SEO global, se mezcla con el `meta` de cada página (la página gana). */
66
- seo: z
67
- .object({
68
- /** Plantilla del <title>. `%s` se reemplaza por el título de la página. */
69
- titleTemplate: z.string().optional(),
70
- keywords: z.array(z.string()).optional(),
71
- /** Ruta dentro de public/, ej. '/favicon.svg'. */
72
- favicon: z.string().optional(),
73
- /** Imagen OG por defecto (URL absoluta o ruta en public/). */
74
- ogImage: z.string().optional(),
75
- twitterCard: z.enum(['summary', 'summary_large_image']).optional(),
76
- /** Metas extra: [{ name | property, content }]. */
77
- meta: z
78
- .array(
79
- z.object({
80
- name: z.string().optional(),
81
- property: z.string().optional(),
82
- content: z.string(),
83
- }),
84
- )
85
- .optional(),
86
- /** <link> extra: [{ rel, href, ... }]. */
87
- links: z.array(z.record(z.string())).optional(),
88
- })
89
- .optional(),
90
- /** Analytics sin escribir código. */
91
- analytics: z
92
- .object({
93
- /** ID de Google Analytics 4, ej. 'G-XXXXXXX'. */
94
- googleAnalytics: z.string().optional(),
95
- /** ID de Meta Pixel. */
96
- metaPixel: z.string().optional(),
97
- /** Dominio para Plausible, ej. 'tienda.ecomiq.app'. */
98
- plausible: z.string().optional(),
99
- /** Scripts arbitrarios: [{ src }] o [{ inline: '...' }]. */
100
- scripts: z
101
- .array(
102
- z.object({
103
- src: z.string().optional(),
104
- inline: z.string().optional(),
105
- defer: z.boolean().optional(),
106
- async: z.boolean().optional(),
107
- }),
108
- )
109
- .optional(),
110
- })
111
- .optional(),
112
- /** Layout declarativo (usado por el layout por defecto; un layout.tsx en la raíz lo reemplaza). */
113
- layout: z
114
- .object({
115
- /** Enlaces del header: [{ label, to }]. */
116
- nav: z.array(z.object({ label: z.string(), to: z.string() })).optional(),
117
- /** Texto del footer. Default: '{name} — creado con Ecomiq'. */
118
- footerText: z.string().optional(),
119
- /** Header pegado arriba con blur. Default: true. */
120
- stickyHeader: z.boolean().default(true).optional(),
121
- })
122
- .optional(),
123
64
  dev: z
124
65
  .object({
125
66
  port: z.number().int().default(3000),