@ecomiq/storefront 0.1.0 → 0.1.2

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.
@@ -0,0 +1,178 @@
1
+ import * as zustand from 'zustand';
2
+ import { S as StorefrontSettings } from './collection-anMkdWa7.js';
3
+ import * as React$1 from 'react';
4
+ import React__default from 'react';
5
+ import * as better_auth_react from 'better-auth/react';
6
+ import { createAuthClient } from 'better-auth/react';
7
+ import * as better_auth from 'better-auth';
8
+ import { StorefrontProductDetail, StorefrontVariant, StorefrontSettings as StorefrontSettings$1, StorefrontPageDetail } from '@ecomiq/storefront/react';
9
+
10
+ /** Instancia de `createAuthClient` (better-auth/react) con los plugins de la app. */
11
+ type StorefrontAuthClientInstance = ReturnType<typeof createAuthClient>;
12
+ declare function StorefrontAuthProvider({ authClient, children, }: {
13
+ authClient: StorefrontAuthClientInstance;
14
+ children: React__default.ReactNode;
15
+ }): React__default.JSX.Element;
16
+ declare function useStorefrontAuthClient(): StorefrontAuthClientInstance;
17
+ type StorefrontSessionUser = {
18
+ id: string;
19
+ email: string;
20
+ firstName: string;
21
+ lastName: string;
22
+ name?: string;
23
+ phone?: string;
24
+ };
25
+ /** Caducidad alineada con OAuth/`expires_in` (skew en ms). El bearer ya no viene en JSON de sesión. */
26
+ declare function storefrontSessionEndsSoon(expiresAt: Date | undefined, skewMs?: number): boolean;
27
+ /** @deprecated Usar `storefrontSessionEndsSoon` */
28
+ declare const storefrontAccessTokenNeedsRefresh: typeof storefrontSessionEndsSoon;
29
+ /**
30
+ * Sesión Better Auth + usuario mapeado para UI (headers, drawer).
31
+ */
32
+ declare function useStorefrontSession(): {
33
+ session: {
34
+ data: {
35
+ user: better_auth.StripEmptyObjects<{
36
+ id: string;
37
+ createdAt: Date;
38
+ updatedAt: Date;
39
+ email: string;
40
+ emailVerified: boolean;
41
+ name: string;
42
+ image?: string | null | undefined;
43
+ }>;
44
+ session: better_auth.StripEmptyObjects<{
45
+ id: string;
46
+ createdAt: Date;
47
+ updatedAt: Date;
48
+ userId: string;
49
+ expiresAt: Date;
50
+ token: string;
51
+ ipAddress?: string | null | undefined;
52
+ userAgent?: string | null | undefined;
53
+ }>;
54
+ } | null;
55
+ isPending: boolean;
56
+ isRefetching: boolean;
57
+ error: better_auth_react.BetterFetchError | null;
58
+ refetch: (queryParams?: {
59
+ query?: better_auth.SessionQueryParams;
60
+ } | undefined) => Promise<void>;
61
+ };
62
+ user: StorefrontSessionUser | null;
63
+ sessionExpiresAt: Date | undefined;
64
+ isAuthenticated: boolean;
65
+ isPending: boolean;
66
+ error: better_auth_react.BetterFetchError | null;
67
+ refetch: (queryParams?: {
68
+ query?: better_auth.SessionQueryParams;
69
+ } | undefined) => Promise<void>;
70
+ signOut: () => Promise<{
71
+ data: {
72
+ success: boolean;
73
+ };
74
+ error: null;
75
+ } | {
76
+ data: null;
77
+ error: {
78
+ code?: string | undefined | undefined;
79
+ message?: string | undefined | undefined;
80
+ status: number;
81
+ statusText: string;
82
+ };
83
+ }>;
84
+ };
85
+
86
+ type Drawers = {
87
+ cart: boolean;
88
+ search: boolean;
89
+ menu: boolean;
90
+ filter: boolean;
91
+ [key: string]: boolean;
92
+ };
93
+ type Modals = {
94
+ login: boolean;
95
+ signup: boolean;
96
+ [key: string]: boolean;
97
+ };
98
+ type StoreState = {
99
+ data: StorefrontSettings | null;
100
+ drawers: Drawers;
101
+ modals: Modals;
102
+ openDrawer: (drawer: keyof Drawers) => void;
103
+ closeDrawer: (drawer: keyof Drawers) => void;
104
+ toggleDrawer: (drawer: keyof Drawers) => void;
105
+ closeAllDrawers: () => void;
106
+ openModal: (modal: keyof Modals) => void;
107
+ closeModal: (modal: keyof Modals) => void;
108
+ toggleModal: (modal: keyof Modals) => void;
109
+ closeAllModals: () => void;
110
+ };
111
+ declare const useStore: zustand.UseBoundStore<zustand.StoreApi<StoreState>>;
112
+
113
+ type EcomiqProviderProps = {
114
+ store: StorefrontSettings | null;
115
+ /** Si lo omites, catálogo y carrito funcionan; login requiere configurar Better Auth (ver README). */
116
+ authClient?: StorefrontAuthClientInstance;
117
+ children: React__default.ReactNode;
118
+ /**
119
+ * Modales de login/registro y toasts del shell Ecomiq (requiere `@ecomiq/ui` en el monorepo).
120
+ * En npm / tiendas custom deja `false` y monta tu propia UI.
121
+ */
122
+ withShellUi?: boolean;
123
+ };
124
+ declare function EcomiqProvider({ store, authClient, children, withShellUi, }: EcomiqProviderProps): React__default.JSX.Element;
125
+
126
+ type EcomiqSignInCredentials = {
127
+ email: string;
128
+ password: string;
129
+ };
130
+ /**
131
+ * Inicia sesión con email/contraseña vía plugin `ecomiqOAuthPassword` (Better Auth).
132
+ * Usa la tienda de `settings` (p. ej. `useStore().data`) para los headers de identidad.
133
+ */
134
+ declare function signInWithEcomiqCredentials(authClient: StorefrontAuthClientInstance, settings: StorefrontSettings | null | undefined, credentials: EcomiqSignInCredentials): Promise<{
135
+ data: unknown;
136
+ error: null;
137
+ }>;
138
+
139
+ /** Option group derived from variants (e.g. Color: [Black, White], Size: [6x8, 8x11]) */
140
+ type OptionGroup = {
141
+ key: string;
142
+ name: string;
143
+ values: string[];
144
+ };
145
+ /** Selected options per group (e.g. { Color: "Black", Size: "8 x 11 inch" }) */
146
+ type SelectedOptions = Record<string, string>;
147
+ type ProductVariantSelectorRenderProps = {
148
+ product: StorefrontProductDetail;
149
+ selectedVariant: StorefrontVariant | null;
150
+ displayPrice: number;
151
+ compareAtPrice: number | null | undefined;
152
+ isAvailable: boolean;
153
+ selectedOptions: SelectedOptions;
154
+ };
155
+
156
+ type ProductVariantSelectorProps = {
157
+ product: StorefrontProductDetail;
158
+ currency?: string;
159
+ features?: string[];
160
+ onVariantChange?: (variant: StorefrontVariant | null, price: number) => void;
161
+ children?: (props: {
162
+ product: StorefrontProductDetail;
163
+ selectedVariant: StorefrontVariant | null;
164
+ displayPrice: number;
165
+ compareAtPrice: number | null | undefined;
166
+ isAvailable: boolean;
167
+ selectedOptions: SelectedOptions;
168
+ }) => React.ReactNode;
169
+ };
170
+ declare function ProductVariantSelector({ product, currency, features, onVariantChange, children, }: ProductVariantSelectorProps): React$1.JSX.Element;
171
+
172
+ /**
173
+ * Busca una página CMS en `settings.cms.pages` (payload de `getPublicStore` / settings)
174
+ * y la valida como `StorefrontPageDetail` para `DynamicPage`.
175
+ */
176
+ declare function findStorefrontCmsPageBySlug(settings: StorefrontSettings$1 | null | undefined, slug: string): StorefrontPageDetail | null;
177
+
178
+ export { EcomiqProvider as E, type OptionGroup as O, ProductVariantSelector as P, StorefrontAuthProvider as S, storefrontSessionEndsSoon as a, useStorefrontSession as b, type StorefrontAuthClientInstance as c, type StorefrontSessionUser as d, type EcomiqProviderProps as e, signInWithEcomiqCredentials as f, type EcomiqSignInCredentials as g, findStorefrontCmsPageBySlug as h, useStore as i, type ProductVariantSelectorProps as j, type SelectedOptions as k, type ProductVariantSelectorRenderProps as l, storefrontAccessTokenNeedsRefresh as s, useStorefrontAuthClient as u };
@@ -0,0 +1,30 @@
1
+ import { z } from 'zod';
2
+
3
+ declare const EcomiqConfigSchema: z.ZodObject<{
4
+ /**
5
+ * Slug de tienda en Ecomiq (p. ej. `mi-tienda` para `mi-tienda.ecomiq.app`).
6
+ * Si está definido, tiene prioridad sobre el `Host` de la request (útil en `localhost`).
7
+ */
8
+ slugStore: z.ZodOptional<z.ZodString>;
9
+ }, "strip", z.ZodTypeAny, {
10
+ slugStore?: string | undefined;
11
+ }, {
12
+ slugStore?: string | undefined;
13
+ }>;
14
+ type EcomiqConfig = z.infer<typeof EcomiqConfigSchema>;
15
+ /**
16
+ * Carga `ecomiq.config.js` (o `.cjs` / `.mjs`) desde la raíz del proyecto (`process.cwd()`).
17
+ * Resultado en caché por proceso.
18
+ */
19
+ declare function loadEcomiqConfig(cwd?: string): EcomiqConfig | null;
20
+ /** Limpia la caché (tests o recarga en dev). */
21
+ declare function clearEcomiqConfigCache(): void;
22
+ /**
23
+ * Identificador de tienda para settings (API / KV).
24
+ * 1. `slugStore` en `ecomiq.config.*` si existe.
25
+ * 2. Si no, deriva del header `Host` (`resolveStorefrontSettingsHost`).
26
+ */
27
+ declare function resolvePublicStoreKey(hostHeader: string | null | undefined, cwd?: string): string;
28
+ declare function hasEcomiqSlugStoreConfig(cwd?: string): boolean;
29
+
30
+ export { type EcomiqConfig, EcomiqConfigSchema, clearEcomiqConfigCache, hasEcomiqSlugStoreConfig, loadEcomiqConfig, resolvePublicStoreKey };
package/dist/index.js ADDED
@@ -0,0 +1,103 @@
1
+ import { existsSync } from 'fs';
2
+ import { createRequire } from 'module';
3
+ import { join } from 'path';
4
+ import { z } from 'zod';
5
+ import '@opennextjs/cloudflare';
6
+ import '@ecomiq/storefront/core';
7
+
8
+ // src/core/config/ecomiq-config.ts
9
+ var ECOMIQ_APP_APEX = "ecomiq.app";
10
+ var ECOMIQ_APP_SUFFIX = [`.${ECOMIQ_APP_APEX}`, `.localhost`];
11
+ function normalizeStoreHost(hostHeader) {
12
+ let h = hostHeader.trim().toLowerCase();
13
+ const colon = h.indexOf(":");
14
+ if (colon !== -1) h = h.slice(0, colon);
15
+ if (h.startsWith("www.")) h = h.slice(4);
16
+ return h;
17
+ }
18
+ function resolveStorefrontSettingsHost(hostHeader) {
19
+ const host = normalizeStoreHost(hostHeader);
20
+ if (!host) {
21
+ throw Object.assign(new Error("Host inv\xE1lido para tienda"), { status: 400 });
22
+ }
23
+ if (host === ECOMIQ_APP_APEX) {
24
+ throw Object.assign(new Error("Host apex ecomiq.app sin slug de tienda"), { status: 400 });
25
+ }
26
+ if (ECOMIQ_APP_SUFFIX.some((suffix) => host.endsWith(suffix))) {
27
+ const slug = host.slice(0, -(ECOMIQ_APP_SUFFIX.find((suffix) => host.endsWith(suffix))?.length ?? 0));
28
+ if (!slug) {
29
+ throw Object.assign(new Error("Host sin slug para ecomiq.app"), { status: 400 });
30
+ }
31
+ return slug;
32
+ }
33
+ return host;
34
+ }
35
+
36
+ // src/core/config/ecomiq-config.ts
37
+ var CONFIG_FILENAMES = [
38
+ "ecomiq.config.js",
39
+ "ecomiq.config.cjs",
40
+ "ecomiq.config.mjs"
41
+ ];
42
+ var EcomiqConfigSchema = z.object({
43
+ /**
44
+ * Slug de tienda en Ecomiq (p. ej. `mi-tienda` para `mi-tienda.ecomiq.app`).
45
+ * Si está definido, tiene prioridad sobre el `Host` de la request (útil en `localhost`).
46
+ */
47
+ slugStore: z.string().min(1).optional()
48
+ });
49
+ var cachedConfig;
50
+ function parseExportedConfig(raw) {
51
+ const candidate = raw && typeof raw === "object" && "default" in raw ? raw.default : raw;
52
+ const parsed = EcomiqConfigSchema.safeParse(candidate);
53
+ if (!parsed.success) {
54
+ console.warn(
55
+ "[ecomiq] ecomiq.config.* inv\xE1lido:",
56
+ parsed.error.flatten().fieldErrors
57
+ );
58
+ return null;
59
+ }
60
+ const slugStore = parsed.data.slugStore?.trim();
61
+ return slugStore ? { slugStore } : {};
62
+ }
63
+ function loadEcomiqConfig(cwd = process.cwd()) {
64
+ if (cachedConfig !== void 0) return cachedConfig;
65
+ for (const filename of CONFIG_FILENAMES) {
66
+ const configPath = join(cwd, filename);
67
+ if (!existsSync(configPath)) continue;
68
+ try {
69
+ const requireFromApp = createRequire(join(cwd, "package.json"));
70
+ const loaded = requireFromApp(configPath);
71
+ const parsed = parseExportedConfig(loaded);
72
+ cachedConfig = parsed;
73
+ return cachedConfig;
74
+ } catch (error) {
75
+ console.warn(`[ecomiq] No se pudo cargar ${configPath}:`, error);
76
+ }
77
+ }
78
+ cachedConfig = null;
79
+ return null;
80
+ }
81
+ function clearEcomiqConfigCache() {
82
+ cachedConfig = void 0;
83
+ }
84
+ function resolvePublicStoreKey(hostHeader, cwd) {
85
+ const slugFromConfig = loadEcomiqConfig(cwd)?.slugStore?.trim();
86
+ if (slugFromConfig) return slugFromConfig;
87
+ if (!hostHeader?.trim()) {
88
+ throw Object.assign(
89
+ new Error(
90
+ "Host is required, o define slugStore en ecomiq.config.js en la ra\xEDz del proyecto"
91
+ ),
92
+ { status: 400 }
93
+ );
94
+ }
95
+ return resolveStorefrontSettingsHost(hostHeader);
96
+ }
97
+ function hasEcomiqSlugStoreConfig(cwd) {
98
+ return Boolean(loadEcomiqConfig(cwd)?.slugStore?.trim());
99
+ }
100
+
101
+ export { EcomiqConfigSchema, clearEcomiqConfigCache, hasEcomiqSlugStoreConfig, loadEcomiqConfig, resolvePublicStoreKey };
102
+ //# sourceMappingURL=index.js.map
103
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/client/storefront-settings-kv.service.ts","../src/core/config/ecomiq-config.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { getCloudflareContext } from \"@opennextjs/cloudflare\";\nimport { StorefrontSettingsSchema } from \"@ecomiq/storefront/core\";\nimport type { StorefrontSettings } from \"@ecomiq/storefront/core\";\n\n/** Debe coincidir con el `binding` en wrangler.jsonc (`kv_namespaces`). */\nexport const ECOMIQ_CLIENTS_STORE_KV_BINDING = \"ecomiq_clients_store\" as const;\n\nconst ECOMIQ_APP_APEX = \"ecomiq.app\";\nconst ECOMIQ_APP_SUFFIX = [`.${ECOMIQ_APP_APEX}`, `.localhost`];\n\ntype KvJsonGet = {\n get(key: string, options: { type: \"json\" }): Promise<unknown>;\n};\n\n/** Host sin puerto, minúsculas, sin `www.`. */\nexport function normalizeStoreHost(hostHeader: string): string {\n let h = hostHeader.trim().toLowerCase();\n const colon = h.indexOf(\":\");\n if (colon !== -1) h = h.slice(0, colon);\n if (h.startsWith(\"www.\")) h = h.slice(4);\n return h;\n}\n\n/**\n * Identificador de tienda a partir del `Host`: slug en `*.ecomiq.app`, FQDN en dominio propio.\n * Misma semántica que el sufijo de `buildStorefrontKvSnapshotKey` (sin el prefijo KV).\n */\nexport function resolveStorefrontSettingsHost(hostHeader: string): string {\n const host = normalizeStoreHost(hostHeader);\n if (!host) {\n throw Object.assign(new Error(\"Host inválido para tienda\"), { status: 400 });\n }\n if (host === ECOMIQ_APP_APEX) {\n throw Object.assign(new Error(\"Host apex ecomiq.app sin slug de tienda\"), { status: 400 });\n }\n if (ECOMIQ_APP_SUFFIX.some(suffix => host.endsWith(suffix))) {\n const slug = host.slice(0, -(ECOMIQ_APP_SUFFIX.find(suffix => host.endsWith(suffix))?.length ?? 0));\n if (!slug) {\n throw Object.assign(new Error(\"Host sin slug para ecomiq.app\"), { status: 400 });\n }\n return slug;\n }\n return host;\n}\n\n/**\n * Clave KV del snapshot de tienda.\n * - `*.ecomiq.app` → `storefront:snapshots:store:<slug>` (todo lo que va antes de `.ecomiq.app`, p. ej. `plata950`).\n * - Dominio propio (p. ej. `plata950.com`) → `storefront:snapshots:store:<dominio completo>`.\n */\nexport function buildStorefrontKvSnapshotKeyFromStoreKey(storeKey: string): string {\n const key = storeKey.trim();\n if (!key) {\n throw Object.assign(new Error(\"Store key vacío para KV\"), { status: 400 });\n }\n return `storefront:snapshots:store:${key}`;\n}\n\nexport function buildStorefrontKvSnapshotKey(hostHeader: string): string {\n return buildStorefrontKvSnapshotKeyFromStoreKey(\n resolveStorefrontSettingsHost(hostHeader),\n );\n}\n\nfunction getKvNamespace(env: unknown): KvJsonGet | undefined {\n if (!env || typeof env !== \"object\") return undefined;\n const ns = (env as Record<string, unknown>)[ECOMIQ_CLIENTS_STORE_KV_BINDING];\n if (!ns || typeof ns !== \"object\" || typeof (ns as KvJsonGet).get !== \"function\") {\n return undefined;\n }\n return ns as KvJsonGet;\n}\n\n/**\n * Lee settings desde KV con clave ya resuelta (slug o dominio).\n */\nexport async function getStorefrontSettingsFromKvByStoreKey(\n storeKey: string,\n): Promise<StorefrontSettings> {\n const { env } = await getCloudflareContext({ async: true });\n const kv = getKvNamespace(env);\n if (!kv) {\n const err = new Error(\n `Binding KV \"${ECOMIQ_CLIENTS_STORE_KV_BINDING}\" no encontrado. Configura kv_namespaces en wrangler.`\n );\n throw Object.assign(err, { status: 500 });\n }\n\n const cacheKey = buildStorefrontKvSnapshotKeyFromStoreKey(storeKey);\n const raw = await kv.get(cacheKey, { type: \"json\" });\n if (raw == null) {\n const err = new Error(\"Store settings not found in KV\");\n throw Object.assign(err, { status: 404 });\n }\n\n return StorefrontSettingsSchema.parse(raw);\n}\n\n/**\n * Lee settings desde KV (`ecomiq_clients_store`) usando la clave según el host.\n */\nexport async function getStorefrontSettingsFromKv(hostHeader: string): Promise<StorefrontSettings> {\n return getStorefrontSettingsFromKvByStoreKey(\n resolveStorefrontSettingsHost(hostHeader),\n );\n}\n","import { existsSync } from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport { join } from \"node:path\";\nimport { z } from \"zod\";\nimport { resolveStorefrontSettingsHost } from \"../client/storefront-settings-kv.service\";\n\nconst CONFIG_FILENAMES = [\n \"ecomiq.config.js\",\n \"ecomiq.config.cjs\",\n \"ecomiq.config.mjs\",\n] as const;\n\nexport const EcomiqConfigSchema = z.object({\n /**\n * Slug de tienda en Ecomiq (p. ej. `mi-tienda` para `mi-tienda.ecomiq.app`).\n * Si está definido, tiene prioridad sobre el `Host` de la request (útil en `localhost`).\n */\n slugStore: z.string().min(1).optional(),\n});\n\nexport type EcomiqConfig = z.infer<typeof EcomiqConfigSchema>;\n\nlet cachedConfig: EcomiqConfig | null | undefined;\n\nfunction parseExportedConfig(raw: unknown): EcomiqConfig | null {\n const candidate =\n raw && typeof raw === \"object\" && \"default\" in (raw as object)\n ? (raw as { default: unknown }).default\n : raw;\n const parsed = EcomiqConfigSchema.safeParse(candidate);\n if (!parsed.success) {\n console.warn(\n \"[ecomiq] ecomiq.config.* inválido:\",\n parsed.error.flatten().fieldErrors,\n );\n return null;\n }\n const slugStore = parsed.data.slugStore?.trim();\n return slugStore ? { slugStore } : {};\n}\n\n/**\n * Carga `ecomiq.config.js` (o `.cjs` / `.mjs`) desde la raíz del proyecto (`process.cwd()`).\n * Resultado en caché por proceso.\n */\nexport function loadEcomiqConfig(cwd = process.cwd()): EcomiqConfig | null {\n if (cachedConfig !== undefined) return cachedConfig;\n\n for (const filename of CONFIG_FILENAMES) {\n const configPath = join(cwd, filename);\n if (!existsSync(configPath)) continue;\n\n try {\n const requireFromApp = createRequire(join(cwd, \"package.json\"));\n const loaded = requireFromApp(configPath) as unknown;\n const parsed = parseExportedConfig(loaded);\n cachedConfig = parsed;\n return cachedConfig;\n } catch (error) {\n console.warn(`[ecomiq] No se pudo cargar ${configPath}:`, error);\n }\n }\n\n cachedConfig = null;\n return null;\n}\n\n/** Limpia la caché (tests o recarga en dev). */\nexport function clearEcomiqConfigCache(): void {\n cachedConfig = undefined;\n}\n\n/**\n * Identificador de tienda para settings (API / KV).\n * 1. `slugStore` en `ecomiq.config.*` si existe.\n * 2. Si no, deriva del header `Host` (`resolveStorefrontSettingsHost`).\n */\nexport function resolvePublicStoreKey(\n hostHeader: string | null | undefined,\n cwd?: string,\n): string {\n const slugFromConfig = loadEcomiqConfig(cwd)?.slugStore?.trim();\n if (slugFromConfig) return slugFromConfig;\n\n if (!hostHeader?.trim()) {\n throw Object.assign(\n new Error(\n \"Host is required, o define slugStore en ecomiq.config.js en la raíz del proyecto\",\n ),\n { status: 400 },\n );\n }\n\n return resolveStorefrontSettingsHost(hostHeader);\n}\n\nexport function hasEcomiqSlugStoreConfig(cwd?: string): boolean {\n return Boolean(loadEcomiqConfig(cwd)?.slugStore?.trim());\n}\n"]}
@@ -1,11 +1,10 @@
1
1
  /**
2
- * Estilos base del SDK para consumidores npm (sin @ecomiq/ui).
3
- * La app debe aportar su propio sistema de diseño / primitivos UI.
2
+ * Estilos mínimos para consumidores npm.
3
+ * La app define su propio design system y componentes.
4
4
  */
5
5
  @import "tailwindcss";
6
6
 
7
- @source "../react/";
8
- @source "../react/components/cms-sections-renderer.tsx";
7
+ @source "../react/shell/components/product-variant-selector/";
9
8
 
10
9
  @layer base {
11
10
  * {