@ecomiq/storefront 2.0.12 → 2.0.14

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 (32) hide show
  1. package/bin/ecomiq.mjs +3 -1
  2. package/package.json +4 -2
  3. package/src/auth/create-storefront-auth.ts +65 -6
  4. package/src/auth/ecomiq-oauth-password-constants.ts +1 -3
  5. package/src/auth/ecomiq-oauth-password-plugin.ts +12 -17
  6. package/src/auth/ecomiq-storefront-sign-in.ts +0 -3
  7. package/src/auth/secure-cookies.ts +23 -0
  8. package/src/auth/storefront-auth-base-url.ts +117 -0
  9. package/src/codegen.mjs +27 -1
  10. package/src/components/account/customer-addresses-panel.tsx +678 -0
  11. package/src/components/account/customer-order-detail-panel.tsx +285 -0
  12. package/src/components/account/customer-orders-panel.tsx +192 -0
  13. package/src/components/account/customer-profile-panel.tsx +379 -0
  14. package/src/components/checkout/checkout-contact-section.tsx +17 -11
  15. package/src/components/checkout/checkout-session-view.tsx +83 -2
  16. package/src/components/checkout/checkout-shipping-address-section.tsx +107 -2
  17. package/src/components/checkout/checkout-steps.ts +15 -0
  18. package/src/components/checkout/map-customer-to-checkout.ts +188 -0
  19. package/src/components/checkout/use-checkout-customer-prefill.ts +409 -0
  20. package/src/components/checkout/use-checkout-session.ts +5 -4
  21. package/src/config.d.ts +4 -0
  22. package/src/config.mjs +14 -0
  23. package/src/index.ts +54 -0
  24. package/src/pages/account/account-page-skeleton.tsx +69 -0
  25. package/src/pages/account/account-page-view.tsx +270 -0
  26. package/src/pages/account/account-shell.tsx +101 -0
  27. package/src/pages/account/parse-account-route.ts +36 -0
  28. package/src/pages/account.tsx +16 -0
  29. package/src/pages/checkout.$checkoutId.tsx +1 -1
  30. package/src/providers/auth.tsx +11 -2
  31. package/src/proxy-api.ts +20 -0
  32. package/src/vite-inline.mjs +54 -1
package/bin/ecomiq.mjs CHANGED
@@ -81,6 +81,8 @@ export async function runCli(command = process.argv[2] ?? 'help') {
81
81
  const config = await prepare()
82
82
  const port = config.dev?.port ?? 3000
83
83
  const runtime = resolveDevRuntime({ config })
84
+ const httpsEnabled = Boolean(config.dev?.https)
85
+ const origin = `${httpsEnabled ? 'https' : 'http'}://localhost:${port}`
84
86
  const { createServer } = await import('vite')
85
87
  const viteConfig = createViteConfig({
86
88
  appDir,
@@ -89,7 +91,7 @@ export async function runCli(command = process.argv[2] ?? 'help') {
89
91
  runtime,
90
92
  })
91
93
  banner(
92
- `dev · ${config.domain ?? 'tienda'} ${c.dim(`· runtime ${runtime}`)} ${c.dim('http://localhost:' + port)}`,
94
+ `dev · ${config.domain ?? 'tienda'} ${c.dim(`· runtime ${runtime}`)} ${c.dim(origin)}`,
93
95
  )
94
96
  const server = await createServer({
95
97
  ...viteConfig,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecomiq/storefront",
3
- "version": "2.0.12",
3
+ "version": "2.0.14",
4
4
  "description": "Micro framework SSR de Ecomiq para storefronts. Páginas, datos y deploy sin configurar nada.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -42,6 +42,7 @@
42
42
  "./internal/checkout-session": "./src/pages/checkout.$checkoutId.tsx",
43
43
  "./internal/thank-you": "./src/pages/thank-you.tsx",
44
44
  "./internal/cms-page": "./src/pages/pages.$pageSlug.tsx",
45
+ "./internal/account": "./src/pages/account.tsx",
45
46
  "./package.json": "./package.json"
46
47
  },
47
48
  "files": [
@@ -63,6 +64,7 @@
63
64
  "@tanstack/react-query": "^5.101.0",
64
65
  "@tanstack/react-router": "^1.170.0",
65
66
  "@tanstack/react-start": "^1.168.0",
67
+ "@vitejs/plugin-basic-ssl": "^2.3.0",
66
68
  "@vitejs/plugin-react": "^6.0.0",
67
69
  "better-auth": "^1.6.23",
68
70
  "class-variance-authority": "^0.7.1",
@@ -76,7 +78,7 @@
76
78
  "vite": "^8.1.0",
77
79
  "wrangler": "^4.100.0",
78
80
  "zod": "^3.25.0",
79
- "@ecomiq/core": "^0.1.3"
81
+ "@ecomiq/core": "^0.1.5"
80
82
  },
81
83
  "peerDependencies": {
82
84
  "react": "^19.0.0",
@@ -1,30 +1,41 @@
1
1
  import { betterAuth } from 'better-auth'
2
2
  import type { BetterAuthPlugin } from 'better-auth'
3
+ import { createCookieGetter, getCookies } from 'better-auth/cookies'
3
4
  import { tanstackStartCookies } from 'better-auth/tanstack-start'
5
+ import { getStorefrontClient } from '../client'
4
6
  import { ecomiqOAuthPassword } from './ecomiq-oauth-password-plugin'
5
7
  import { readProcessEnv } from './env'
8
+ import { shouldUseSecureCookies } from './secure-cookies'
9
+ import {
10
+ mergeSettingsHostsIntoBaseURL,
11
+ resolveStorefrontAuthBaseURL,
12
+ type StorefrontDynamicBaseURLConfig,
13
+ } from './storefront-auth-base-url'
6
14
 
7
15
  export type CreateStorefrontAuthOptions = {
8
16
  secret?: string
9
- baseURL?: string
10
17
  /** Opciones adicionales pasadas a `betterAuth` (p. ej. `database`). */
11
18
  [key: string]: unknown
12
19
  }
13
20
 
14
21
  /**
15
22
  * Instancia Better Auth del storefront (servidor).
16
- * Usa el plugin `ecomiqOAuthPassword` + cookies de TanStack Start.
17
23
  *
18
- * Si no pasas `secret`/`baseURL`, lee `BETTER_AUTH_SECRET` y `BETTER_AUTH_URL`.
24
+ * allowedHosts = `BETTER_AUTH_TRUSTED_HOSTS` (defaults) + dominio(s) de settings.
25
+ * Cookies `Secure` se recalculan por request (HTTP vs HTTPS).
19
26
  */
20
27
  export function createStorefrontAuth(options: CreateStorefrontAuthOptions = {}) {
21
- const { secret, baseURL, advanced, ...rest } = options
22
- return betterAuth({
28
+ const { secret, advanced, ...rest } = options
29
+
30
+ const resolvedBaseURL: StorefrontDynamicBaseURLConfig = resolveStorefrontAuthBaseURL()
31
+
32
+ const auth = betterAuth({
23
33
  ...rest,
24
34
  secret: (secret as string | undefined) ?? readProcessEnv('BETTER_AUTH_SECRET'),
25
- baseURL: (baseURL as string | undefined) ?? readProcessEnv('BETTER_AUTH_URL'),
35
+ baseURL: resolvedBaseURL,
26
36
  advanced: {
27
37
  disableCSRFCheck: true,
38
+ useSecureCookies: false,
28
39
  ...(typeof advanced === 'object' && advanced !== null ? advanced : {}),
29
40
  },
30
41
  plugins: [
@@ -32,4 +43,52 @@ export function createStorefrontAuth(options: CreateStorefrontAuthOptions = {})
32
43
  tanstackStartCookies(),
33
44
  ],
34
45
  })
46
+
47
+ const originalHandler = auth.handler.bind(auth)
48
+
49
+ return Object.assign(auth, {
50
+ handler: async (request: Request) => {
51
+ await syncAllowedHostsFromSettings(resolvedBaseURL)
52
+ await rehydrateAuthCookiesForRequest(auth, request)
53
+ return originalHandler(request)
54
+ },
55
+ })
56
+ }
57
+
58
+ async function syncAllowedHostsFromSettings(
59
+ baseURL: StorefrontDynamicBaseURLConfig,
60
+ ): Promise<void> {
61
+ try {
62
+ const settings = await getStorefrontClient().getSettings()
63
+ mergeSettingsHostsIntoBaseURL(baseURL, settings)
64
+ } catch {
65
+ mergeSettingsHostsIntoBaseURL(baseURL, null)
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Better Auth crea cookies al init; con `protocol: 'auto'` cae en `isProduction`
71
+ * y marca Secure. En cada request las regeneramos según el protocolo real.
72
+ */
73
+ async function rehydrateAuthCookiesForRequest(
74
+ auth: { $context: Promise<Record<string, unknown>> | Record<string, unknown> },
75
+ request: Request,
76
+ ): Promise<void> {
77
+ const ctx = await auth.$context
78
+ const useSecure = shouldUseSecureCookies(request.url)
79
+ const origin = new URL(request.url).origin
80
+ const options = (ctx.options ?? {}) as Record<string, unknown>
81
+ const advanced = {
82
+ ...((options.advanced as object | undefined) ?? {}),
83
+ useSecureCookies: useSecure,
84
+ }
85
+ const cookieOptions = {
86
+ ...options,
87
+ baseURL: origin,
88
+ advanced,
89
+ }
90
+
91
+ ctx.authCookies = getCookies(cookieOptions as never)
92
+ ctx.createAuthCookie = createCookieGetter(cookieOptions as never)
93
+ options.advanced = advanced
35
94
  }
@@ -18,9 +18,7 @@ export const STOREFRONT_ECOMIQ_OAUTH_COOKIE_NAME = 'ecomiq_sf_oauth_at'
18
18
  /** Path de la cookie (debe llegar a `/api/*` del proxy). */
19
19
  export const STOREFRONT_ECOMIQ_OAUTH_COOKIE_PATH = '/api'
20
20
 
21
- /** Headers de identidad tipados para login storefront. */
21
+ /** Header de identidad para login storefront (`POST /v1/customer/login`). */
22
22
  export const STOREFRONT_HEADERS = {
23
- identityScope: 'x-Identity-Scope',
24
- tenantId: 'x-Tenant-Id',
25
23
  storeId: 'x-Store-Id',
26
24
  } as const
@@ -20,26 +20,27 @@ import {
20
20
  customerLoginResponseSchema,
21
21
  signInBody,
22
22
  } from "./ecomiq-oauth-password-types";
23
+ import { shouldUseSecureCookiesFromBaseURL } from "./secure-cookies";
23
24
 
24
25
  type StoreIdentityContext = {
25
- identityScope?: string | null;
26
- tenantId?: string | null;
27
- storeId?: string | null;
26
+ storeId?: string | null
28
27
  };
29
28
 
30
29
  /**
31
30
  * Inicia sesión contra `POST /v1/customer/login` del storefront Ecomiq,
32
31
  * crea/actualiza el usuario en Better Auth y guarda el bearer en cookie HttpOnly
33
- * acotada a `/api/store` (proxy BFF).
32
+ * acotada a `/api` (proxy BFF).
34
33
  */
35
34
  export function ecomiqOAuthPassword(): BetterAuthPlugin {
36
- const secureCookie = true;
37
35
  const clearStorefrontOAuthCookieMiddleware = createAuthMiddleware(async (ctx) => {
38
36
  ctx.setCookie(STOREFRONT_ECOMIQ_OAUTH_COOKIE_NAME, "", {
39
37
  path: STOREFRONT_ECOMIQ_OAUTH_COOKIE_PATH,
40
38
  httpOnly: true,
41
39
  sameSite: "lax",
42
- secure: secureCookie,
40
+ secure: shouldUseSecureCookiesFromBaseURL(
41
+ ctx.context.baseURL,
42
+ ctx.request?.url,
43
+ ),
43
44
  maxAge: 0,
44
45
  });
45
46
  });
@@ -75,8 +76,6 @@ export function ecomiqOAuthPassword(): BetterAuthPlugin {
75
76
  const { email: loginEmail, password, callbackURL, rememberMe } = ctx.body;
76
77
 
77
78
  const login = await customerPasswordLogin(loginEmail, password, {
78
- identityScope: ctx.getHeader(STOREFRONT_HEADERS.identityScope),
79
- tenantId: ctx.getHeader(STOREFRONT_HEADERS.tenantId),
80
79
  storeId: ctx.getHeader(STOREFRONT_HEADERS.storeId),
81
80
  });
82
81
 
@@ -124,6 +123,11 @@ export function ecomiqOAuthPassword(): BetterAuthPlugin {
124
123
  ? login.expiresIn
125
124
  : 60 * 60 * 24;
126
125
 
126
+ const secureCookie = shouldUseSecureCookiesFromBaseURL(
127
+ ctx.context.baseURL,
128
+ ctx.request?.url,
129
+ );
130
+
127
131
  await ctx.setCookie(STOREFRONT_ECOMIQ_OAUTH_COOKIE_NAME, login.accessToken, {
128
132
  path: STOREFRONT_ECOMIQ_OAUTH_COOKIE_PATH,
129
133
  httpOnly: true,
@@ -171,16 +175,7 @@ async function customerPasswordLogin(
171
175
  accept: "application/json",
172
176
  });
173
177
 
174
- const identityScope = context.identityScope?.trim();
175
- const tenantId = context.tenantId?.trim();
176
178
  const storeId = context.storeId?.trim();
177
-
178
- if (identityScope) {
179
- headers.set(STOREFRONT_HEADERS.identityScope, identityScope);
180
- }
181
- if (tenantId) {
182
- headers.set(STOREFRONT_HEADERS.tenantId, tenantId);
183
- }
184
179
  if (storeId) {
185
180
  headers.set(STOREFRONT_HEADERS.storeId, storeId);
186
181
  }
@@ -32,9 +32,6 @@ export async function signInWithEcomiqCredentials(
32
32
  headers: {
33
33
  'Content-Type': 'application/json',
34
34
  [STOREFRONT_HEADERS.storeId]: settings?.store.id,
35
- [STOREFRONT_HEADERS.tenantId]: settings?.store.tenantId,
36
- [STOREFRONT_HEADERS.identityScope]:
37
- settings?.store.checkoutSettings.general.identityScope,
38
35
  },
39
36
  body: credentials,
40
37
  })
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Secure cookies solo en HTTPS. En localhost HTTP el browser las descarta.
3
+ */
4
+ export function shouldUseSecureCookies(requestUrl: string | URL): boolean {
5
+ try {
6
+ const url = typeof requestUrl === 'string' ? new URL(requestUrl) : requestUrl
7
+ return url.protocol === 'https:'
8
+ } catch {
9
+ return false
10
+ }
11
+ }
12
+
13
+ export function shouldUseSecureCookiesFromBaseURL(
14
+ baseURL: string | undefined,
15
+ fallbackRequestUrl?: string,
16
+ ): boolean {
17
+ if (typeof baseURL === 'string' && baseURL.length > 0) {
18
+ if (baseURL.startsWith('https://')) return true
19
+ if (baseURL.startsWith('http://')) return false
20
+ }
21
+ if (fallbackRequestUrl) return shouldUseSecureCookies(fallbackRequestUrl)
22
+ return false
23
+ }
@@ -0,0 +1,117 @@
1
+ import { readProcessEnv } from './env'
2
+
3
+ /** Config dinámica de Better Auth (`baseURL: { allowedHosts }`). */
4
+ export type StorefrontDynamicBaseURLConfig = {
5
+ allowedHosts: string[]
6
+ fallback?: string
7
+ protocol?: 'http' | 'https' | 'auto'
8
+ }
9
+
10
+ /** Env CSV con hosts por defecto (antes de settings). */
11
+ export const BETTER_AUTH_TRUSTED_HOSTS_ENV = 'BETTER_AUTH_TRUSTED_HOSTS'
12
+
13
+ /**
14
+ * Defaults si `BETTER_AUTH_TRUSTED_HOSTS` no está definido.
15
+ * Incluye host sin puerto (`localhost`) y con puerto (`localhost:*`).
16
+ */
17
+ export const DEFAULT_STOREFRONT_AUTH_ALLOWED_HOSTS = [
18
+ 'localhost',
19
+ 'localhost:*',
20
+ '127.0.0.1',
21
+ '127.0.0.1:*',
22
+ '*.ecomiq.app',
23
+ 'ecomiq.app',
24
+ ] as const
25
+
26
+ export type StoreSettingsHostsSource = {
27
+ store: {
28
+ domain: string | null
29
+ domains: string[]
30
+ }
31
+ }
32
+
33
+ /** Hosts por defecto: env CSV o fallback hardcodeado. */
34
+ export function defaultAllowedHostsFromEnv(): string[] {
35
+ const fromEnv = splitCsv(readProcessEnv(BETTER_AUTH_TRUSTED_HOSTS_ENV))
36
+ return uniqueHosts(
37
+ fromEnv.length > 0 ? fromEnv : [...DEFAULT_STOREFRONT_AUTH_ALLOWED_HOSTS],
38
+ )
39
+ }
40
+
41
+ /**
42
+ * Extrae hosts desde settings API (`store.domain` + `store.domains`).
43
+ */
44
+ export function hostsFromStoreSettings(
45
+ settings: StoreSettingsHostsSource | null | undefined,
46
+ ): string[] {
47
+ if (!settings?.store) return []
48
+ const raw = [settings.store.domain, ...settings.store.domains]
49
+ return uniqueHosts(
50
+ raw.flatMap((value) => {
51
+ const host = normalizeHost(value)
52
+ return host ? [host] : []
53
+ }),
54
+ )
55
+ }
56
+
57
+ /**
58
+ * `baseURL` dinámico: solo hosts del env (defaults).
59
+ * El dominio final se fusiona luego desde settings API.
60
+ */
61
+ export function resolveStorefrontAuthBaseURL(): StorefrontDynamicBaseURLConfig {
62
+ return {
63
+ protocol: 'auto',
64
+ allowedHosts: defaultAllowedHostsFromEnv(),
65
+ }
66
+ }
67
+
68
+ /**
69
+ * allowedHosts = defaults (env) + dominio(s) de settings.
70
+ */
71
+ export function mergeSettingsHostsIntoBaseURL(
72
+ baseURL: StorefrontDynamicBaseURLConfig,
73
+ settings: StoreSettingsHostsSource | null | undefined,
74
+ ): void {
75
+ baseURL.allowedHosts = uniqueHosts([
76
+ ...defaultAllowedHostsFromEnv(),
77
+ ...hostsFromStoreSettings(settings),
78
+ ])
79
+ }
80
+
81
+ function splitCsv(value: string | undefined): string[] {
82
+ if (!value) return []
83
+ return value
84
+ .split(',')
85
+ .map((part) => part.trim())
86
+ .filter(Boolean)
87
+ }
88
+
89
+ function normalizeHost(value: string | null | undefined): string | null {
90
+ if (!value) return null
91
+ const trimmed = value.trim()
92
+ if (!trimmed) return null
93
+
94
+ try {
95
+ if (trimmed.includes('://')) {
96
+ const host = new URL(trimmed).host
97
+ return host || null
98
+ }
99
+ } catch {
100
+ return null
101
+ }
102
+
103
+ const withoutPath = trimmed.split('/')[0]?.trim()
104
+ return withoutPath || null
105
+ }
106
+
107
+ function uniqueHosts(hosts: readonly string[]): string[] {
108
+ const seen = new Set<string>()
109
+ const result: string[] = []
110
+ for (const host of hosts) {
111
+ const normalized = host.trim().toLowerCase()
112
+ if (!normalized || seen.has(normalized)) continue
113
+ seen.add(normalized)
114
+ result.push(host.trim())
115
+ }
116
+ return result
117
+ }
package/src/codegen.mjs CHANGED
@@ -16,6 +16,7 @@
16
16
  * /checkout/ ← redirect a /cart (checkout.index, sin layout padre)
17
17
  * /checkout/$checkoutId ← flujo de checkout
18
18
  * /thank-you ← post-compra
19
+ * /account/* ← área de cuenta (login/register en template: /login, /register)
19
20
  */
20
21
  import {
21
22
  mkdirSync,
@@ -62,6 +63,31 @@ const FRAMEWORK_LOCKED_PAGES = [
62
63
  routeId: '/pages/$pageSlug',
63
64
  importFrom: '@ecomiq/storefront/internal/cms-page',
64
65
  },
66
+ {
67
+ rel: 'account.index.tsx',
68
+ routeId: '/account/',
69
+ importFrom: '@ecomiq/storefront/internal/account',
70
+ },
71
+ {
72
+ rel: 'account.addresses.tsx',
73
+ routeId: '/account/addresses',
74
+ importFrom: '@ecomiq/storefront/internal/account',
75
+ },
76
+ {
77
+ rel: 'account.orders.tsx',
78
+ routeId: '/account/orders',
79
+ importFrom: '@ecomiq/storefront/internal/account',
80
+ },
81
+ {
82
+ rel: 'account.orders.$orderId.tsx',
83
+ routeId: '/account/orders/$orderId',
84
+ importFrom: '@ecomiq/storefront/internal/account',
85
+ },
86
+ {
87
+ rel: 'account.devices.tsx',
88
+ routeId: '/account/devices',
89
+ importFrom: '@ecomiq/storefront/internal/account',
90
+ },
65
91
  ]
66
92
 
67
93
  export async function generateApp({ cwd, appDir, frameworkRoot, config }) {
@@ -244,7 +270,7 @@ export async function generateApp({ cwd, appDir, frameworkRoot, config }) {
244
270
  },
245
271
  },
246
272
  ...(config.deploy?.routes ? { routes: config.deploy.routes } : {}),
247
- vars: config.env ?? {},
273
+ vars: {...(config.env ?? {}), BETTER_AUTH_URL: "localhost,localhost:*,127.0.0.1,127.0.0.1:*,*.ecomiq.app,ecomiq.app,*juanpablocs.workers.dev"},
248
274
  },
249
275
  null,
250
276
  2,