@ecomiq/storefront 2.1.4 → 3.0.1
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/LICENSE +21 -0
- package/README.md +377 -76
- package/dist/account-Dx4e3syi.js +258 -0
- package/dist/account-forms-AVk71ko7.js +39 -0
- package/dist/account-forms.d.ts +13 -0
- package/dist/account.d.ts +240 -0
- package/dist/address-defaults-BqjRhS82.js +14 -0
- package/dist/address-defaults.d.ts +11 -0
- package/dist/analytics/analytics-CG1Qagwl.js +148 -0
- package/dist/analytics/analytics.d.ts +1 -0
- package/dist/analytics/config-DJr-GRhs.js +62 -0
- package/dist/analytics/config.d.ts +22 -0
- package/dist/analytics/consent-B13rBpBs.js +57 -0
- package/dist/analytics/consent-banner-9QvRRy8v.js +239 -0
- package/dist/analytics/consent-banner.d.ts +20 -0
- package/dist/analytics/cookies-pLozllyB.js +100 -0
- package/dist/analytics/cookies.d.ts +6 -0
- package/dist/analytics/destinations-iECrlLIr.js +144 -0
- package/dist/analytics/events-Dt81tIp1.js +102 -0
- package/dist/analytics/events.d.ts +51 -0
- package/dist/analytics/tracker-p4yaoOyk.js +51 -0
- package/dist/api-cache-DsuIn5hX.js +159 -0
- package/dist/bank-accounts-DlEczQQ5.js +81 -0
- package/dist/bank-accounts.d.ts +4 -0
- package/dist/captcha-DAqaIx8m.js +117 -0
- package/dist/captcha.d.ts +34 -0
- package/dist/cart-D7DxZ2lD.js +30 -0
- package/dist/cart-actions-Bsub_FFG.js +116 -0
- package/dist/cart-actions.d.ts +15 -0
- package/dist/cart-drawer-B7PPsR69.js +22 -0
- package/dist/cart-drawer.d.ts +9 -0
- package/dist/cart-form-y9P1r_EX.js +81 -0
- package/dist/cart-form.d.ts +24 -0
- package/dist/cart-summary-ur2VWKr-.js +89 -0
- package/dist/cart-summary.d.ts +31 -0
- package/dist/cart.d.ts +4 -0
- package/dist/catalog-7gJ-l5hW.js +67 -0
- package/dist/catalog-hooks-B7ZP4wYY.js +312 -0
- package/dist/catalog-hooks.d.ts +111 -0
- package/dist/catalog.d.ts +5 -0
- package/dist/checkout-DVWSXANi.js +474 -0
- package/dist/checkout-form-CbOIdENC.js +131 -0
- package/dist/checkout-form.d.ts +20 -0
- package/dist/checkout.d.ts +86 -0
- package/dist/complaint-book-BvwLvfx0.js +117 -0
- package/dist/complaint-book.d.ts +62 -0
- package/dist/culqi-authentication-BGr20vAJ.js +204 -0
- package/dist/culqi-authentication.d.ts +15 -0
- package/dist/culqi-checkout-CTXpKgp_.js +55 -0
- package/dist/culqi-three-ds-HP3mfrvV.js +33 -0
- package/dist/culqi-three-ds.d.ts +29 -0
- package/dist/customer-avatar-CJXSDroK.js +12 -0
- package/dist/customer-avatar.d.ts +5 -0
- package/dist/customer-cookies-DHOgFAMV.js +45 -0
- package/dist/customer-csrf-C1oDX-Py.js +17 -0
- package/dist/customer-csrf.d.ts +3 -0
- package/dist/customer-document-fields-C1K-_DAD.js +70 -0
- package/dist/customer-document-fields.d.ts +9 -0
- package/dist/customer-login-form-C7AC6vXd.js +144 -0
- package/dist/customer-login-form.d.ts +17 -0
- package/dist/customer-registration-form-BEm_NEfP.js +93 -0
- package/dist/customer-registration-form.d.ts +8 -0
- package/dist/document-url-CGsAl5Tf.js +9 -0
- package/dist/documents-T1mzc_j8.js +38 -0
- package/dist/documents.d.ts +16 -0
- package/dist/entries/account.d.ts +11 -0
- package/dist/entries/account.js +10 -0
- package/dist/entries/analytics.d.ts +6 -0
- package/dist/entries/analytics.js +5 -0
- package/dist/entries/cart.d.ts +8 -0
- package/dist/entries/cart.js +7 -0
- package/dist/entries/checkout.d.ts +12 -0
- package/dist/entries/checkout.js +11 -0
- package/dist/entries/order.d.ts +11 -0
- package/dist/entries/order.js +11 -0
- package/dist/favicon-YpyhVJ9H.js +28 -0
- package/dist/favicon.d.ts +4 -0
- package/dist/form-BtQcbXlJ.js +18 -0
- package/dist/form-contracts-BNpHaTGJ.js +166 -0
- package/dist/form-contracts.d.ts +42 -0
- package/dist/form.d.ts +3 -0
- package/dist/hidden-CCWtOWKR.js +3 -0
- package/dist/hidden.d.ts +4 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.js +50 -0
- package/dist/invoice-fields-BG0HjL6T.js +103 -0
- package/dist/invoice-fields.d.ts +39 -0
- package/dist/links-kFxBZfdb.js +96 -0
- package/dist/links.d.ts +21 -0
- package/dist/mercadopago-checkout-DC7RXa8k.js +27 -0
- package/dist/mercadopago-dialog-UTQev0MG.js +180 -0
- package/dist/mercadopago-dialog.d.ts +21 -0
- package/dist/money-DtGOtDri.js +32 -0
- package/dist/money.d.ts +1 -0
- package/dist/optimistic-cart-BveVOHuh.js +53 -0
- package/dist/optimistic-cart.d.ts +16 -0
- package/dist/order-display-DTt5WBbc.js +112 -0
- package/dist/order-display.d.ts +20 -0
- package/dist/order-draft-DxV1Uhm3.js +38 -0
- package/dist/order-draft.d.ts +16 -0
- package/dist/order-pages-Dm6l5m0m.js +38 -0
- package/dist/order-pages.d.ts +12 -0
- package/dist/order-payment-continuation-D_d5PGA4.js +212 -0
- package/dist/order-payment-continuation.d.ts +10 -0
- package/dist/order-payment-in-review-x4gdLFHZ.js +65 -0
- package/dist/order-payment-in-review.d.ts +5 -0
- package/dist/order-status-BJXLJwxq.js +162 -0
- package/dist/order-status.d.ts +89 -0
- package/dist/order-view-9IlBg5AF.js +62 -0
- package/dist/order-view.d.ts +12 -0
- package/dist/page-action-Ctkf4xC6.js +7 -0
- package/dist/page-seo-a5FJl8x5.js +262 -0
- package/dist/page-seo.d.ts +34 -0
- package/dist/pages-Ca_Gt6mq.js +150 -0
- package/dist/pages.d.ts +45 -0
- package/dist/payment-dialog-frame-B-Q_xQ94.js +57 -0
- package/dist/payment-logo-BoPaGJc_.js +46 -0
- package/dist/payment-logo-svgs-By9lDTLK.js +20 -0
- package/dist/payment-logo.d.ts +11 -0
- package/dist/payment-logos-BYvdwYyJ.js +23 -0
- package/dist/payment-methods-B8ocTgFZ.js +73 -0
- package/dist/payment-methods.d.ts +58 -0
- package/dist/payment-return-5cjypwIw.js +7 -0
- package/dist/place-CmW4hBWY.js +13 -0
- package/dist/place.d.ts +4 -0
- package/dist/preview-BiIBif3C.js +427 -0
- package/dist/preview-address-Cf0sJ-9p.js +21 -0
- package/dist/preview-cart-Dg2CVktS.js +376 -0
- package/dist/product-preview-BspMAecJ.js +51 -0
- package/dist/product-preview.d.ts +5 -0
- package/dist/product-variants-BNDHvs1y.js +46 -0
- package/dist/product-variants.d.ts +12 -0
- package/dist/public-pages-XtwlaLwt.js +194 -0
- package/dist/public-pages.d.ts +67 -0
- package/dist/registration-CpDcv8ql.js +91 -0
- package/dist/registration.d.ts +12 -0
- package/dist/request-security-CRsRMgwj.js +76 -0
- package/dist/rich-text-M-Ju8RL4.js +9 -0
- package/dist/rich-text.d.ts +5 -0
- package/dist/route-text-DcJw2SsK.js +9 -0
- package/dist/route-text.d.ts +4 -0
- package/dist/section-preview-CmsMl3yu.js +83 -0
- package/dist/section-preview.d.ts +12 -0
- package/dist/sections/account-addresses.d.ts +18 -0
- package/dist/sections/account-addresses.js +375 -0
- package/dist/sections/account-order.d.ts +12 -0
- package/dist/sections/account-order.js +592 -0
- package/dist/sections/account-orders.d.ts +8 -0
- package/dist/sections/account-orders.js +359 -0
- package/dist/sections/account-profile.d.ts +16 -0
- package/dist/sections/account-profile.js +123 -0
- package/dist/sections/checkout.d.ts +24 -0
- package/dist/sections/checkout.js +963 -0
- package/dist/sections/complaint-book.d.ts +8 -0
- package/dist/sections/complaint-book.js +538 -0
- package/dist/sections/design-system.d.ts +1 -0
- package/dist/sections/design-system.js +310 -0
- package/dist/sections/draft-invoice.d.ts +20 -0
- package/dist/sections/draft-invoice.js +158 -0
- package/dist/sections/order-tracking.d.ts +19 -0
- package/dist/sections/order-tracking.js +141 -0
- package/dist/sections-CNUMWTF_.js +77 -0
- package/dist/sections.d.ts +19 -0
- package/dist/seo-GpDRhAAA.js +78 -0
- package/dist/session-CKvmFJyg.js +379 -0
- package/dist/session.d.ts +24 -0
- package/dist/settings-B0NO2A6Y.js +78 -0
- package/dist/settings.d.ts +51 -0
- package/dist/source-marker-DhqSkW7n.js +4 -0
- package/dist/sources-B-3JFJfb.js +46 -0
- package/dist/sources.d.ts +7 -0
- package/dist/store-8Q-_LFrQ.js +152 -0
- package/dist/store-contact--hY6CoFb.js +19 -0
- package/dist/store-contact.d.ts +6 -0
- package/dist/store.d.ts +17 -0
- package/dist/storefront-language-DrMjVbmk.js +29 -0
- package/dist/storefront-language.d.ts +14 -0
- package/dist/storefront-text-D25ae1qf.js +11 -0
- package/dist/storefront-text-en-ljjjSS4q.js +1030 -0
- package/dist/storefront-text-pt-BtHaJLcp.js +1030 -0
- package/dist/storefront-text.d.ts +4 -0
- package/dist/stripe-dialog-Ev9s3Z62.js +396 -0
- package/dist/stripe-dialog.d.ts +30 -0
- package/dist/telemetry-HuZXWBPi.js +114 -0
- package/dist/telemetry.d.ts +10 -0
- package/dist/templates-CAqoNyTW.js +40 -0
- package/dist/templates.d.ts +15 -0
- package/dist/urls-CpGQPxvf.js +71 -0
- package/dist/urls.d.ts +18 -0
- package/dist/use-checkout-CZcxXJXi.js +42 -0
- package/dist/use-checkout-payment-9R-zacrJ.js +80 -0
- package/dist/use-checkout-payment.d.ts +18 -0
- package/dist/use-checkout.d.ts +36 -0
- package/dist/use-culqi-checkout-CrjLAipU.js +109 -0
- package/dist/use-order-payment-BKIz_IO0.js +45 -0
- package/dist/use-order-payment.d.ts +18 -0
- package/dist/use-payment-token-CErx-tN-.js +105 -0
- package/dist/vite-payment-logos-kziu7wKb.js +27 -0
- package/dist/vite-payment-logos.d.ts +24 -0
- package/dist/vite-public-assets-DW2fgtVb.js +209 -0
- package/dist/vite-public-assets.d.ts +29 -0
- package/dist/vite-sections-DTBr0-Kg.js +464 -0
- package/dist/vite-sections.d.ts +21 -0
- package/dist/vite-theme-ui-BMO-CDi3.js +41 -0
- package/dist/vite-theme-ui.d.ts +10 -0
- package/dist/vite.d.ts +36 -0
- package/dist/vite.js +127 -0
- package/dist/voucher-payment-CVFDZ1L8.js +66 -0
- package/dist/worker-CNi-gzCu.js +86 -0
- package/dist/worker.d.ts +27 -0
- package/package.json +100 -76
- package/src/sections/account-addresses.tsx +215 -0
- package/src/sections/account-order.tsx +424 -0
- package/src/sections/account-orders.tsx +259 -0
- package/src/sections/account-profile.tsx +70 -0
- package/src/sections/checkout.tsx +586 -0
- package/src/sections/complaint-book.tsx +325 -0
- package/src/sections/design-system.tsx +146 -0
- package/src/sections/draft-invoice.tsx +105 -0
- package/src/sections/order-tracking.tsx +81 -0
- package/src/vite-client.d.ts +4 -0
- package/bin/ecomiq.mjs +0 -179
- package/public/design-bridge.js +0 -318
- package/src/api-proxy.ts +0 -194
- package/src/auth/create-storefront-auth.ts +0 -94
- package/src/auth/ecomiq-oauth-password-client.ts +0 -26
- package/src/auth/ecomiq-oauth-password-constants.ts +0 -24
- package/src/auth/ecomiq-oauth-password-plugin.ts +0 -294
- package/src/auth/ecomiq-oauth-password-types.ts +0 -24
- package/src/auth/ecomiq-storefront-sign-in.ts +0 -44
- package/src/auth/env.ts +0 -7
- package/src/auth/secure-cookies.ts +0 -23
- package/src/auth/storefront-auth-base-url.ts +0 -117
- package/src/auth/storefront-auth.ts +0 -7
- package/src/cart-session.ts +0 -239
- package/src/client.ts +0 -48
- package/src/codegen.mjs +0 -801
- package/src/components/account/customer-addresses-panel.tsx +0 -678
- package/src/components/account/customer-order-detail-panel.tsx +0 -285
- package/src/components/account/customer-orders-panel.tsx +0 -192
- package/src/components/account/customer-profile-panel.tsx +0 -379
- package/src/components/checkout/cart-sync.ts +0 -143
- package/src/components/checkout/checkout-contact-section.tsx +0 -76
- package/src/components/checkout/checkout-external-update-banner.tsx +0 -15
- package/src/components/checkout/checkout-form-controls.tsx +0 -139
- package/src/components/checkout/checkout-line-items.tsx +0 -43
- package/src/components/checkout/checkout-order-summary.tsx +0 -165
- package/src/components/checkout/checkout-payment-section.tsx +0 -212
- package/src/components/checkout/checkout-session-view.tsx +0 -564
- package/src/components/checkout/checkout-shipping-address-section.tsx +0 -400
- package/src/components/checkout/checkout-shipping-section.tsx +0 -98
- package/src/components/checkout/checkout-states.tsx +0 -73
- package/src/components/checkout/checkout-step-card.tsx +0 -101
- package/src/components/checkout/checkout-steps.ts +0 -111
- package/src/components/checkout/form-schema.ts +0 -281
- package/src/components/checkout/format.ts +0 -15
- package/src/components/checkout/index.ts +0 -5
- package/src/components/checkout/map-customer-to-checkout.ts +0 -188
- package/src/components/checkout/types.ts +0 -41
- package/src/components/checkout/use-checkout-customer-prefill.ts +0 -409
- package/src/components/checkout/use-checkout-geo.ts +0 -50
- package/src/components/checkout/use-checkout-session.ts +0 -640
- package/src/components/hero.tsx +0 -291
- package/src/config.d.ts +0 -94
- package/src/config.mjs +0 -146
- package/src/format-money.ts +0 -31
- package/src/health.ts +0 -90
- package/src/hooks/index.ts +0 -9
- package/src/hooks/recently-viewed.ts +0 -125
- package/src/index.ts +0 -349
- package/src/link.tsx +0 -2
- package/src/pages/account/account-page-skeleton.tsx +0 -69
- package/src/pages/account/account-page-view.tsx +0 -270
- package/src/pages/account/account-shell.tsx +0 -101
- package/src/pages/account/parse-account-route.ts +0 -36
- package/src/pages/account.tsx +0 -16
- package/src/pages/api.$.ts +0 -20
- package/src/pages/api.auth.$.ts +0 -16
- package/src/pages/checkout.$checkoutId.tsx +0 -60
- package/src/pages/checkout.tsx +0 -31
- package/src/pages/pages.$pageSlug.tsx +0 -74
- package/src/pages/thank-you.tsx +0 -53
- package/src/providers/auth.tsx +0 -96
- package/src/providers/cart.tsx +0 -275
- package/src/providers/ecomiq.ts +0 -137
- package/src/providers/index.ts +0 -26
- package/src/proxy-api.ts +0 -20
- package/src/public-config.ts +0 -24
- package/src/review-submission.ts +0 -89
- package/src/runtime/context.ts +0 -2
- package/src/runtime/default-layout.tsx +0 -64
- package/src/runtime/head.ts +0 -149
- package/src/runtime/page.tsx +0 -143
- package/src/runtime/postcss-browser-stub.mjs +0 -5
- package/src/server-config-store.ts +0 -35
- package/src/server.ts +0 -286
- package/src/styles.css +0 -76
- package/src/ui/badge.tsx +0 -32
- package/src/ui/button.tsx +0 -31
- package/src/ui/card.tsx +0 -31
- package/src/ui/carousel.tsx +0 -218
- package/src/ui/cn.ts +0 -6
- package/src/ui/index.ts +0 -8
- package/src/ui/separator.tsx +0 -6
- package/src/ui/skeleton.tsx +0 -6
- package/src/ui/storefront-error.tsx +0 -49
- package/src/utils.ts +0 -36
- package/src/vite-inline.mjs +0 -263
- package/templates/__root.tsx +0 -83
- package/templates/router.tsx +0 -19
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import "./source-marker-DhqSkW7n.js";
|
|
2
|
+
import { store } from "./store-8Q-_LFrQ.js";
|
|
3
|
+
import { data, useLoaderData } from "react-router";
|
|
4
|
+
import { Suspense, createElement, use } from "react";
|
|
5
|
+
// src/sections.ts
|
|
6
|
+
const SECTIONS = "ecomiqSections";
|
|
7
|
+
function canonical(value) {
|
|
8
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
9
|
+
if (typeof value !== "object" || value === null) return value;
|
|
10
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])]));
|
|
11
|
+
}
|
|
12
|
+
function sectionKey(section, props) {
|
|
13
|
+
const sourced = String(props["data-ecomiq-sources"] ?? "").split(" ");
|
|
14
|
+
const own = Object.entries(props).filter(([key, value]) => key !== "children" && !key.startsWith("data-") && !sourced.includes(key) && value !== void 0);
|
|
15
|
+
return `${section}:${JSON.stringify(canonical(Object.fromEntries(own)))}`;
|
|
16
|
+
}
|
|
17
|
+
function loadSections(args, entries) {
|
|
18
|
+
const client = store(args.context);
|
|
19
|
+
const unique = new Map(entries.map((entry) => [sectionKey(entry.section, entry.props), entry]));
|
|
20
|
+
return Object.fromEntries([...unique].map(([key, entry]) => {
|
|
21
|
+
return [key, Promise.resolve().then(() => entry.load(entry.props, {
|
|
22
|
+
...args,
|
|
23
|
+
client
|
|
24
|
+
})).catch((error) => {
|
|
25
|
+
console.error("[ecomiq] section load failed", entry.section, error);
|
|
26
|
+
return null;
|
|
27
|
+
})];
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
function withSectionBoundary(Component) {
|
|
31
|
+
return function SectionBoundary(props) {
|
|
32
|
+
const fallback = createElement("div", {
|
|
33
|
+
"aria-hidden": true,
|
|
34
|
+
style: { minHeight: "24rem" }
|
|
35
|
+
});
|
|
36
|
+
return createElement(Suspense, { fallback }, createElement(Component, props));
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function withResponseInit(value) {
|
|
40
|
+
return typeof value === "object" && value !== null && value.type === "DataWithResponseInit";
|
|
41
|
+
}
|
|
42
|
+
function plain(value) {
|
|
43
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
44
|
+
}
|
|
45
|
+
function withSections(result, sections, extra = {}) {
|
|
46
|
+
if (result instanceof Response) return result;
|
|
47
|
+
if (withResponseInit(result)) return data(withSections(result.data, sections, extra), result.init ?? void 0);
|
|
48
|
+
if (result === void 0 || result === null) return {
|
|
49
|
+
...extra,
|
|
50
|
+
[SECTIONS]: sections
|
|
51
|
+
};
|
|
52
|
+
if (!plain(result)) return result;
|
|
53
|
+
const own = result[SECTIONS] ?? {};
|
|
54
|
+
return {
|
|
55
|
+
...result,
|
|
56
|
+
...extra,
|
|
57
|
+
[SECTIONS]: {
|
|
58
|
+
...own,
|
|
59
|
+
...sections
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function loadedSections(loaded) {
|
|
64
|
+
if (!plain(loaded)) return void 0;
|
|
65
|
+
const own = loaded[SECTIONS];
|
|
66
|
+
if (own !== void 0) return own;
|
|
67
|
+
const nested = loaded.data;
|
|
68
|
+
return plain(nested) ? nested[SECTIONS] : void 0;
|
|
69
|
+
}
|
|
70
|
+
function useSection(load, props) {
|
|
71
|
+
const loaded = useLoaderData();
|
|
72
|
+
const section = load.section;
|
|
73
|
+
if (section === void 0) return void 0;
|
|
74
|
+
const value = loadedSections(loaded)?.[sectionKey(section, props)];
|
|
75
|
+
return value instanceof Promise ? use(value) : value;
|
|
76
|
+
}
|
|
77
|
+
export { loadSections, sectionKey, useSection, withSectionBoundary, withSections };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { LoaderFunctionArgs } from "react-router";
|
|
2
|
+
import { StorefrontClient } from "@ecomiq/core";
|
|
3
|
+
import { ComponentType } from "react";
|
|
4
|
+
export type SectionLoadArgs = LoaderFunctionArgs & {
|
|
5
|
+
readonly client: StorefrontClient;
|
|
6
|
+
};
|
|
7
|
+
export type SectionEntry = {
|
|
8
|
+
readonly section: string;
|
|
9
|
+
readonly load: (props: never, args: SectionLoadArgs) => Promise<unknown>;
|
|
10
|
+
readonly props: object;
|
|
11
|
+
};
|
|
12
|
+
export type SectionData = {
|
|
13
|
+
readonly [key: string]: unknown;
|
|
14
|
+
};
|
|
15
|
+
export declare function sectionKey(section: string, props: object): string;
|
|
16
|
+
export declare function loadSections(args: LoaderFunctionArgs, entries: readonly SectionEntry[]): SectionData;
|
|
17
|
+
export declare function withSectionBoundary<Props extends object>(Component: ComponentType<Props>): ComponentType<Props>;
|
|
18
|
+
export declare function withSections(result: unknown, sections: SectionData, extra?: object): unknown;
|
|
19
|
+
export declare function useSection<Props extends object, Data>(load: (props: Props, args: SectionLoadArgs) => Promise<Data>, props: Props): Data | undefined;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { store } from "./store-8Q-_LFrQ.js";
|
|
2
|
+
import { absoluteUrl, canonicalOrigin } from "./urls-CpGQPxvf.js";
|
|
3
|
+
// src/seo.ts
|
|
4
|
+
const PRIVATE_PATHS = [
|
|
5
|
+
"/cart",
|
|
6
|
+
"/checkout",
|
|
7
|
+
"/account",
|
|
8
|
+
"/search"
|
|
9
|
+
];
|
|
10
|
+
const CACHE = "public, max-age=30";
|
|
11
|
+
function escape(value) {
|
|
12
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
13
|
+
}
|
|
14
|
+
async function robots(request, context) {
|
|
15
|
+
const settings = await store(context).getSettings();
|
|
16
|
+
const lines = ["User-agent: *"];
|
|
17
|
+
if (settings.cms.settings.seo.robotsIndex) {
|
|
18
|
+
for (const path of PRIVATE_PATHS) lines.push(`Disallow: ${path}`);
|
|
19
|
+
lines.push("", `Sitemap: ${absoluteUrl(canonicalOrigin(request, settings), "/sitemap.xml")}`);
|
|
20
|
+
} else lines.push("Disallow: /");
|
|
21
|
+
return new Response(`${lines.join("\n")}\n`, { headers: {
|
|
22
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
23
|
+
"Cache-Control": CACHE
|
|
24
|
+
} });
|
|
25
|
+
}
|
|
26
|
+
function sitemapResponse(body) {
|
|
27
|
+
return new Response(`<?xml version="1.0" encoding="UTF-8"?>\n${body}\n`, { headers: {
|
|
28
|
+
"Content-Type": "application/xml; charset=utf-8",
|
|
29
|
+
"Cache-Control": CACHE
|
|
30
|
+
} });
|
|
31
|
+
}
|
|
32
|
+
async function sitemap(request, context) {
|
|
33
|
+
const pageParam = new URL(request.url).searchParams.get("page");
|
|
34
|
+
const pageNumber = pageParam === null ? 1 : Number(pageParam);
|
|
35
|
+
if (pageParam !== null && (!/^[1-9]\d*$/.test(pageParam) || !Number.isSafeInteger(pageNumber))) return new Response("Página inválida", { status: 400 });
|
|
36
|
+
const client = store(context);
|
|
37
|
+
const [settings, page] = await Promise.all([client.getSettings(), client.getSitemap(pageNumber)]);
|
|
38
|
+
if (pageNumber > page.totalPages) return new Response(null, { status: 404 });
|
|
39
|
+
const origin = canonicalOrigin(request, settings);
|
|
40
|
+
if (pageParam === null && page.totalPages > 1) return sitemapResponse(`<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${Array.from({ length: page.totalPages }, (_, index) => {
|
|
41
|
+
const url = new URL(absoluteUrl(origin, "/sitemap.xml"));
|
|
42
|
+
url.searchParams.set("page", String(index + 1));
|
|
43
|
+
return `<sitemap><loc>${escape(url.href)}</loc></sitemap>`;
|
|
44
|
+
}).join("\n")}\n</sitemapindex>`);
|
|
45
|
+
const items = [];
|
|
46
|
+
if (pageNumber === 1) {
|
|
47
|
+
items.push({
|
|
48
|
+
url: "/",
|
|
49
|
+
updatedOn: null
|
|
50
|
+
});
|
|
51
|
+
for (const cmsPage of settings.cms.pages) {
|
|
52
|
+
if (cmsPage.channel !== "storefront" || !cmsPage.hasPublishedVersion || cmsPage.slug === "home") continue;
|
|
53
|
+
items.push({
|
|
54
|
+
url: `/pages/${cmsPage.slug}`,
|
|
55
|
+
updatedOn: cmsPage.publishedOn ?? cmsPage.updatedOn
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
items.push(...page.items);
|
|
60
|
+
return sitemapResponse(`<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${items.map((item) => {
|
|
61
|
+
let entry = `<url><loc>${escape(absoluteUrl(origin, item.url))}</loc>`;
|
|
62
|
+
if (item.updatedOn !== null) entry += `<lastmod>${escape(item.updatedOn)}</lastmod>`;
|
|
63
|
+
return `${entry}</url>`;
|
|
64
|
+
}).join("\n")}\n</urlset>`);
|
|
65
|
+
}
|
|
66
|
+
async function seoResponse(request, context) {
|
|
67
|
+
if (request.method !== "GET" && request.method !== "HEAD") return null;
|
|
68
|
+
const path = new URL(request.url).pathname;
|
|
69
|
+
let response = null;
|
|
70
|
+
if (path === "/robots.txt") response = await robots(request, context);
|
|
71
|
+
if (path === "/sitemap.xml") response = await sitemap(request, context);
|
|
72
|
+
if (response === null || request.method === "GET") return response;
|
|
73
|
+
return new Response(null, {
|
|
74
|
+
status: response.status,
|
|
75
|
+
headers: response.headers
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
export { seoResponse };
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { loadStorefrontTranslations, storefrontText } from "./storefront-text-D25ae1qf.js";
|
|
2
|
+
import { captchaResponse } from "./captcha-DAqaIx8m.js";
|
|
3
|
+
import { base64Url, customerCookies, customerSessionId, equalSecret } from "./customer-cookies-DHOgFAMV.js";
|
|
4
|
+
import { safeNext } from "./form-BtQcbXlJ.js";
|
|
5
|
+
import { PREVIEW_TOKEN } from "./preview-BiIBif3C.js";
|
|
6
|
+
import { customerClientHeaders, customerCsrfTokenForRequest, rejectCrossOriginAction } from "./request-security-CRsRMgwj.js";
|
|
7
|
+
import { store } from "./store-8Q-_LFrQ.js";
|
|
8
|
+
import { requestUrl } from "./urls-CpGQPxvf.js";
|
|
9
|
+
import { worker } from "./worker-CNi-gzCu.js";
|
|
10
|
+
import { data, redirect } from "react-router";
|
|
11
|
+
import { ApiError } from "@ecomiq/core";
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
// src/session.ts
|
|
14
|
+
const currentRequests = /* @__PURE__ */ new WeakMap();
|
|
15
|
+
const registrationRequests = /* @__PURE__ */ new WeakMap();
|
|
16
|
+
const PRIVATE_HEADERS = {
|
|
17
|
+
"Cache-Control": "private, no-store",
|
|
18
|
+
"Referrer-Policy": "no-referrer"
|
|
19
|
+
};
|
|
20
|
+
const TransactionSchema = z.object({
|
|
21
|
+
state: z.string().min(1),
|
|
22
|
+
nonce: z.string().min(1),
|
|
23
|
+
verifier: z.string().min(43).max(128),
|
|
24
|
+
redirectUri: z.string().url(),
|
|
25
|
+
next: z.string(),
|
|
26
|
+
createdAt: z.number(),
|
|
27
|
+
loginMode: z.enum(["hosted", "storefront"]),
|
|
28
|
+
authorizationEndpoint: z.string().url(),
|
|
29
|
+
returnUrl: z.string(),
|
|
30
|
+
browserKey: z.string(),
|
|
31
|
+
email: z.string().default(""),
|
|
32
|
+
challengeId: z.string().optional(),
|
|
33
|
+
needsSecondFactor: z.boolean().default(false),
|
|
34
|
+
retryAt: z.number().default(0)
|
|
35
|
+
});
|
|
36
|
+
const ChallengeSchema = z.object({ challengeId: z.string().min(1) });
|
|
37
|
+
const ChallengeErrorSchema = z.object({
|
|
38
|
+
message: z.string(),
|
|
39
|
+
needsSecondFactor: z.boolean().optional()
|
|
40
|
+
});
|
|
41
|
+
function validLoginTransaction(transaction, configuration, origin) {
|
|
42
|
+
if (Date.now() - transaction.createdAt > 6e5 || transaction.createdAt > Date.now() || transaction.redirectUri !== new URL("/account/callback", origin).href || transaction.redirectUri !== configuration.redirectUri || transaction.authorizationEndpoint !== configuration.authorizationEndpoint || transaction.loginMode !== configuration.loginMode) return false;
|
|
43
|
+
const authorize = new URL(transaction.returnUrl, configuration.authority);
|
|
44
|
+
return authorize.origin === new URL(configuration.authorizationEndpoint).origin && authorize.pathname === new URL(configuration.authorizationEndpoint).pathname && authorize.searchParams.get("client_id") === configuration.clientId && authorize.searchParams.get("scope") === configuration.scope;
|
|
45
|
+
}
|
|
46
|
+
async function loginFormData(transaction, configuration, context, error = null, status = 200, clearSession = false) {
|
|
47
|
+
let step = "email";
|
|
48
|
+
if (transaction.challengeId) step = "code";
|
|
49
|
+
if (transaction.needsSecondFactor) step = "secondFactor";
|
|
50
|
+
const cookies = customerCookies(context.get(worker).env);
|
|
51
|
+
const headers = new Headers(PRIVATE_HEADERS);
|
|
52
|
+
headers.append("Set-Cookie", await cookies.transaction.serialize(transaction));
|
|
53
|
+
if (clearSession) headers.append("Set-Cookie", await cookies.session.serialize("", { maxAge: 0 }));
|
|
54
|
+
return data({
|
|
55
|
+
step,
|
|
56
|
+
email: transaction.email,
|
|
57
|
+
captcha: configuration.captcha,
|
|
58
|
+
retryAt: transaction.retryAt,
|
|
59
|
+
error
|
|
60
|
+
}, {
|
|
61
|
+
status,
|
|
62
|
+
headers
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
async function customerCsrfToken(request, context) {
|
|
66
|
+
return customerCsrfTokenForRequest(request, context.get(worker).env);
|
|
67
|
+
}
|
|
68
|
+
function previewing(request, env) {
|
|
69
|
+
return env.ECOMIQ_PREVIEW === "1" && !/^\/account\/(login|callback)(\/|$)/.test(requestUrl(request).pathname);
|
|
70
|
+
}
|
|
71
|
+
async function readCustomer(request, context) {
|
|
72
|
+
const env = context.get(worker).env;
|
|
73
|
+
if (previewing(request, env)) return {
|
|
74
|
+
sessionId: PREVIEW_TOKEN,
|
|
75
|
+
name: "Ana"
|
|
76
|
+
};
|
|
77
|
+
const sessionId = await customerSessionId(request, env);
|
|
78
|
+
if (!sessionId) return null;
|
|
79
|
+
try {
|
|
80
|
+
const session = await store(context).getCustomerSession({ headers: { "X-Customer-Session": sessionId } });
|
|
81
|
+
if (Date.parse(session.expiresAt) <= Date.now()) return null;
|
|
82
|
+
return {
|
|
83
|
+
sessionId,
|
|
84
|
+
name: session.customer.firstName || session.customer.email
|
|
85
|
+
};
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (error instanceof ApiError && (error.status === 401 || error.status === 403)) return null;
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function customer(request, context) {
|
|
92
|
+
let pending = currentRequests.get(request);
|
|
93
|
+
if (!pending) {
|
|
94
|
+
pending = readCustomer(request, context);
|
|
95
|
+
currentRequests.set(request, pending);
|
|
96
|
+
}
|
|
97
|
+
return pending;
|
|
98
|
+
}
|
|
99
|
+
async function requireCustomer(request, context) {
|
|
100
|
+
const current = await customer(request, context);
|
|
101
|
+
if (current) {
|
|
102
|
+
await requireCustomerRegistration(request, context, current);
|
|
103
|
+
return current;
|
|
104
|
+
}
|
|
105
|
+
const url = requestUrl(request);
|
|
106
|
+
throw redirect(`/account/login?next=${encodeURIComponent(url.pathname + url.search)}`, { headers: PRIVATE_HEADERS });
|
|
107
|
+
}
|
|
108
|
+
/** Loads customer data alongside the session and registration checks and returns it only after both pass. */
|
|
109
|
+
async function requireCustomerWith(request, context, load) {
|
|
110
|
+
const env = context.get(worker).env;
|
|
111
|
+
const sessionId = previewing(request, env) ? PREVIEW_TOKEN : await customerSessionId(request, env);
|
|
112
|
+
if (!sessionId) return load(customerHeaders(await requireCustomer(request, context)));
|
|
113
|
+
const headers = customerHeaders({
|
|
114
|
+
sessionId,
|
|
115
|
+
name: ""
|
|
116
|
+
});
|
|
117
|
+
if (sessionId !== "ecomiq-editor-preview" && !registrationRequests.has(request)) {
|
|
118
|
+
const registration = store(context).getCustomerRegistration(headers);
|
|
119
|
+
registration.catch(() => void 0);
|
|
120
|
+
registrationRequests.set(request, registration);
|
|
121
|
+
}
|
|
122
|
+
const pending = load(headers);
|
|
123
|
+
pending.catch(() => void 0);
|
|
124
|
+
await requireCustomer(request, context);
|
|
125
|
+
return pending;
|
|
126
|
+
}
|
|
127
|
+
/** Enforces profile completion for authenticated account and checkout requests. */
|
|
128
|
+
async function requireCustomerRegistration(request, context, current) {
|
|
129
|
+
if (current.sessionId === "ecomiq-editor-preview") return;
|
|
130
|
+
let pending = registrationRequests.get(request);
|
|
131
|
+
if (!pending) {
|
|
132
|
+
pending = store(context).getCustomerRegistration(customerHeaders(current));
|
|
133
|
+
registrationRequests.set(request, pending);
|
|
134
|
+
}
|
|
135
|
+
if ((await pending).requiresCompletion) {
|
|
136
|
+
const url = requestUrl(request);
|
|
137
|
+
throw redirect(`/account/register?next=${encodeURIComponent(safeNext(url.pathname + url.search))}`, {
|
|
138
|
+
status: 303,
|
|
139
|
+
headers: PRIVATE_HEADERS
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function customerHeaders(current) {
|
|
144
|
+
return { headers: { "X-Customer-Session": current.sessionId } };
|
|
145
|
+
}
|
|
146
|
+
function identityTransportUrl(endpoint, context) {
|
|
147
|
+
const transportUrl = context.get(worker).env.ECOMIQ_AUTH_URL;
|
|
148
|
+
if (!transportUrl) return endpoint;
|
|
149
|
+
const transport = new URL(transportUrl);
|
|
150
|
+
if (!["http:", "https:"].includes(transport.protocol) || transport.username || transport.password || transport.pathname !== "/" || transport.search || transport.hash) throw new Error("ECOMIQ_AUTH_URL debe ser el origen de Identity.");
|
|
151
|
+
const target = new URL(endpoint);
|
|
152
|
+
target.protocol = transport.protocol;
|
|
153
|
+
target.host = transport.host;
|
|
154
|
+
return target.href;
|
|
155
|
+
}
|
|
156
|
+
function randomSecret() {
|
|
157
|
+
return base64Url(crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(32)));
|
|
158
|
+
}
|
|
159
|
+
async function loginFailure(message, status, headers, context) {
|
|
160
|
+
const language = (await store(context).getSettings().catch(() => null))?.store.language ?? "es";
|
|
161
|
+
const translations = await loadStorefrontTranslations(language);
|
|
162
|
+
const t = (source) => storefrontText(translations, source);
|
|
163
|
+
headers.set("Content-Type", "text/html; charset=utf-8");
|
|
164
|
+
headers.set("Content-Security-Policy", "default-src 'none'; base-uri 'none'; frame-ancestors 'none'");
|
|
165
|
+
return new Response(`<!doctype html><html lang="${language}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${t("No pudimos iniciar sesión")}</title></head><body><main><h1>${t("No pudimos iniciar sesión")}</h1><p>${t(message)}</p><p><a href="/account/login">${t("Intentar de nuevo")}</a></p><p><a href="/">${t("Volver a la tienda")}</a></p></main></body></html>`, {
|
|
166
|
+
status,
|
|
167
|
+
headers
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
async function startCustomerLogin(request, context) {
|
|
171
|
+
const url = new URL(request.url);
|
|
172
|
+
const next = safeNext(url.searchParams.get("next"));
|
|
173
|
+
const current = await customer(request, context);
|
|
174
|
+
if (current) {
|
|
175
|
+
if ((await store(context).getCustomerRegistration(customerHeaders(current))).requiresCompletion) return redirect(`/account/register?next=${encodeURIComponent(next)}`, { headers: PRIVATE_HEADERS });
|
|
176
|
+
return redirect(next, { headers: PRIVATE_HEADERS });
|
|
177
|
+
}
|
|
178
|
+
const clearSession = await customerSessionId(request, context.get(worker).env) !== null;
|
|
179
|
+
const configuration = await store(context).upsertCustomerAuthConfiguration(url.origin);
|
|
180
|
+
const redirectUri = new URL("/account/callback", url.origin).href;
|
|
181
|
+
if (configuration.redirectUri !== redirectUri) throw new Error("El dominio de acceso no corresponde a esta tienda.");
|
|
182
|
+
const saved = TransactionSchema.safeParse(await customerCookies(context.get(worker).env).transaction.parse(request.headers.get("Cookie")));
|
|
183
|
+
if (configuration.loginMode === "storefront" && saved.success && validLoginTransaction(saved.data, configuration, url.origin)) return loginFormData(saved.data, configuration, context, null, 200, clearSession);
|
|
184
|
+
const verifier = randomSecret();
|
|
185
|
+
const challenge = base64Url(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier))));
|
|
186
|
+
const state = randomSecret();
|
|
187
|
+
const nonce = randomSecret();
|
|
188
|
+
const authorize = new URL(configuration.authorizationEndpoint);
|
|
189
|
+
authorize.search = new URLSearchParams({
|
|
190
|
+
client_id: configuration.clientId,
|
|
191
|
+
response_type: "code",
|
|
192
|
+
redirect_uri: redirectUri,
|
|
193
|
+
scope: configuration.scope,
|
|
194
|
+
code_challenge: challenge,
|
|
195
|
+
code_challenge_method: "S256",
|
|
196
|
+
state,
|
|
197
|
+
nonce
|
|
198
|
+
}).toString();
|
|
199
|
+
const transaction = {
|
|
200
|
+
state,
|
|
201
|
+
nonce,
|
|
202
|
+
verifier,
|
|
203
|
+
redirectUri,
|
|
204
|
+
next,
|
|
205
|
+
createdAt: Date.now(),
|
|
206
|
+
loginMode: configuration.loginMode,
|
|
207
|
+
authorizationEndpoint: configuration.authorizationEndpoint,
|
|
208
|
+
returnUrl: authorize.pathname + authorize.search,
|
|
209
|
+
browserKey: randomSecret(),
|
|
210
|
+
email: "",
|
|
211
|
+
needsSecondFactor: false,
|
|
212
|
+
retryAt: 0
|
|
213
|
+
};
|
|
214
|
+
if (configuration.loginMode === "storefront") return loginFormData(transaction, configuration, context, null, 200, clearSession);
|
|
215
|
+
const cookies = customerCookies(context.get(worker).env);
|
|
216
|
+
const headers = new Headers(PRIVATE_HEADERS);
|
|
217
|
+
headers.append("Set-Cookie", await cookies.transaction.serialize(transaction));
|
|
218
|
+
if (clearSession) headers.append("Set-Cookie", await cookies.session.serialize("", { maxAge: 0 }));
|
|
219
|
+
return redirect(authorize.href, { headers });
|
|
220
|
+
}
|
|
221
|
+
/** Receives only browser form fields; the signed transaction owns all OAuth values. */
|
|
222
|
+
async function submitCustomerLogin(request, context) {
|
|
223
|
+
const rejected = rejectCrossOriginAction(request);
|
|
224
|
+
if (rejected) return rejected;
|
|
225
|
+
const origin = new URL(request.url).origin;
|
|
226
|
+
const configuration = await store(context).upsertCustomerAuthConfiguration(origin);
|
|
227
|
+
const cookies = customerCookies(context.get(worker).env);
|
|
228
|
+
const saved = TransactionSchema.safeParse(await cookies.transaction.parse(request.headers.get("Cookie")));
|
|
229
|
+
if (configuration.loginMode !== "storefront" || !saved.success || !validLoginTransaction(saved.data, configuration, origin)) return redirect("/account/login", {
|
|
230
|
+
status: 303,
|
|
231
|
+
headers: {
|
|
232
|
+
...PRIVATE_HEADERS,
|
|
233
|
+
"Set-Cookie": await cookies.transaction.serialize("", { maxAge: 0 })
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
const transaction = saved.data;
|
|
237
|
+
const form = await request.formData();
|
|
238
|
+
const field = (name) => {
|
|
239
|
+
const value = form.get(name);
|
|
240
|
+
return typeof value === "string" ? value.trim() : "";
|
|
241
|
+
};
|
|
242
|
+
const intent = field("intent");
|
|
243
|
+
if (intent === "reset") {
|
|
244
|
+
transaction.email = "";
|
|
245
|
+
transaction.challengeId = void 0;
|
|
246
|
+
transaction.needsSecondFactor = false;
|
|
247
|
+
return loginFormData(transaction, configuration, context);
|
|
248
|
+
}
|
|
249
|
+
if (intent === "send" || intent === "resend") {
|
|
250
|
+
if (transaction.retryAt > Date.now()) return loginFormData(transaction, configuration, context, "Espera un minuto antes de solicitar otro código.", 429);
|
|
251
|
+
const email = intent === "resend" ? transaction.email : field("email");
|
|
252
|
+
if (!z.string().email().max(254).safeParse(email).success) return loginFormData(transaction, configuration, context, "Ingresa un correo electrónico válido.", 400);
|
|
253
|
+
if (!configuration.captcha.isConfigured) return loginFormData(transaction, configuration, context, "La tienda debe completar la configuración de verificación. Intenta más tarde.", 503);
|
|
254
|
+
const captchaToken = captchaResponse(form, configuration.captcha.provider);
|
|
255
|
+
if (configuration.captcha.provider !== "off" && !captchaToken) return loginFormData(transaction, configuration, context, "Completa la verificación antes de continuar.", 400);
|
|
256
|
+
try {
|
|
257
|
+
const challengeUrl = identityTransportUrl(new URL("/v1/customer-auth/challenges", configuration.authority).href, context);
|
|
258
|
+
const client = await customerClientHeaders(request, context.get(worker).env, new URL(challengeUrl));
|
|
259
|
+
const response = await fetch(challengeUrl, {
|
|
260
|
+
method: "POST",
|
|
261
|
+
redirect: "manual",
|
|
262
|
+
headers: {
|
|
263
|
+
"Content-Type": "application/json",
|
|
264
|
+
...client
|
|
265
|
+
},
|
|
266
|
+
body: JSON.stringify({
|
|
267
|
+
returnUrl: transaction.returnUrl,
|
|
268
|
+
browserKey: transaction.browserKey,
|
|
269
|
+
email,
|
|
270
|
+
captchaToken: captchaToken || void 0
|
|
271
|
+
})
|
|
272
|
+
});
|
|
273
|
+
if (!response.ok) return loginIdentityError(response, transaction, configuration, context);
|
|
274
|
+
const challenge = ChallengeSchema.parse(await response.json());
|
|
275
|
+
transaction.email = email;
|
|
276
|
+
transaction.challengeId = challenge.challengeId;
|
|
277
|
+
transaction.needsSecondFactor = false;
|
|
278
|
+
transaction.retryAt = Date.now() + 6e4;
|
|
279
|
+
return loginFormData(transaction, configuration, context);
|
|
280
|
+
} catch (error) {
|
|
281
|
+
console.error("[ecomiq] customer login code failed", error);
|
|
282
|
+
return loginFormData(transaction, configuration, context, "No pudimos enviar el código. Intenta de nuevo en unos minutos.", 503);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (intent !== "verify" || !transaction.challengeId) return loginFormData(transaction, configuration, context, "Solicita un código para continuar.", 400);
|
|
286
|
+
const code = field("code");
|
|
287
|
+
const secondFactor = field("secondFactor");
|
|
288
|
+
if (!transaction.needsSecondFactor && !/^\d{6}$/.test(code)) return loginFormData(transaction, configuration, context, "Ingresa el código de seis dígitos que enviamos a tu correo.", 400);
|
|
289
|
+
if (transaction.needsSecondFactor && !secondFactor) return loginFormData(transaction, configuration, context, "Ingresa el código de tu aplicación de autenticación o uno de recuperación.", 400);
|
|
290
|
+
try {
|
|
291
|
+
const authorize = new URL(transaction.returnUrl, configuration.authority);
|
|
292
|
+
const authorizeUrl = identityTransportUrl(transaction.authorizationEndpoint, context);
|
|
293
|
+
const client = await customerClientHeaders(request, context.get(worker).env, new URL(authorizeUrl));
|
|
294
|
+
const response = await fetch(authorizeUrl, {
|
|
295
|
+
method: "POST",
|
|
296
|
+
redirect: "manual",
|
|
297
|
+
headers: {
|
|
298
|
+
...client,
|
|
299
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
300
|
+
"X-Customer-Challenge": transaction.challengeId,
|
|
301
|
+
"X-Customer-Browser-Key": transaction.browserKey,
|
|
302
|
+
"X-Customer-Code": code,
|
|
303
|
+
"X-Customer-Second-Factor": secondFactor
|
|
304
|
+
},
|
|
305
|
+
body: authorize.searchParams
|
|
306
|
+
});
|
|
307
|
+
if (response.status !== 302) return loginIdentityError(response, transaction, configuration, context);
|
|
308
|
+
const location = response.headers.get("Location");
|
|
309
|
+
if (!location) throw new Error("Missing customer callback");
|
|
310
|
+
const callback = new URL(location);
|
|
311
|
+
const expected = new URL(transaction.redirectUri);
|
|
312
|
+
if (callback.origin !== expected.origin || callback.pathname !== expected.pathname || callback.hash || !equalSecret(callback.searchParams.get("state") ?? "", transaction.state)) throw new Error("Invalid customer callback");
|
|
313
|
+
return completeCustomerLogin(new Request(callback, { headers: { Cookie: request.headers.get("Cookie") ?? "" } }), context);
|
|
314
|
+
} catch (error) {
|
|
315
|
+
console.error("[ecomiq] customer login verification failed", error);
|
|
316
|
+
return loginFormData(transaction, configuration, context, "No pudimos completar el acceso. Intenta de nuevo en unos minutos.", 503);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
async function loginIdentityError(response, transaction, configuration, context) {
|
|
320
|
+
const error = ChallengeErrorSchema.safeParse(await response.json().catch(() => null));
|
|
321
|
+
let message = "No pudimos completar el acceso. Intenta de nuevo en unos minutos.";
|
|
322
|
+
if (error.success && response.status < 500) {
|
|
323
|
+
message = error.data.message;
|
|
324
|
+
if (error.data.needsSecondFactor) transaction.needsSecondFactor = true;
|
|
325
|
+
}
|
|
326
|
+
if (response.status === 429) transaction.retryAt = Date.now() + 6e4;
|
|
327
|
+
const status = response.status >= 400 && response.status < 500 ? response.status : 503;
|
|
328
|
+
return loginFormData(transaction, configuration, context, message, status);
|
|
329
|
+
}
|
|
330
|
+
async function completeCustomerLogin(request, context) {
|
|
331
|
+
const url = new URL(request.url);
|
|
332
|
+
const cookies = customerCookies(context.get(worker).env);
|
|
333
|
+
const transaction = TransactionSchema.safeParse(await cookies.transaction.parse(request.headers.get("Cookie")));
|
|
334
|
+
const headers = new Headers(PRIVATE_HEADERS);
|
|
335
|
+
headers.append("Set-Cookie", await cookies.transaction.serialize("", { maxAge: 0 }));
|
|
336
|
+
if (!transaction.success || !equalSecret(url.searchParams.get("state") ?? "", transaction.data.state) || Date.now() - transaction.data.createdAt > 6e5 || transaction.data.createdAt > Date.now() || transaction.data.redirectUri !== new URL("/account/callback", url.origin).href) return loginFailure("El acceso venció o no corresponde a esta tienda. Vuelve a iniciar sesión.", 400, headers, context);
|
|
337
|
+
const code = url.searchParams.get("code");
|
|
338
|
+
if (url.searchParams.has("error") || !code) return loginFailure("No se completó el acceso. Vuelve a iniciar sesión desde la tienda.", 400, headers, context);
|
|
339
|
+
try {
|
|
340
|
+
const session = await store(context).createCustomerSession({
|
|
341
|
+
code,
|
|
342
|
+
codeVerifier: transaction.data.verifier,
|
|
343
|
+
redirectUri: transaction.data.redirectUri,
|
|
344
|
+
nonce: transaction.data.nonce
|
|
345
|
+
});
|
|
346
|
+
headers.append("Set-Cookie", await cookies.session.serialize(session.sessionId, { expires: new Date(session.expiresAt) }));
|
|
347
|
+
const next = safeNext(transaction.data.next);
|
|
348
|
+
if ((await store(context).getCustomerRegistration({ headers: { "X-Customer-Session": session.sessionId } })).requiresCompletion) return redirect(`/account/register?next=${encodeURIComponent(next)}`, { headers });
|
|
349
|
+
return redirect(next, { headers });
|
|
350
|
+
} catch (error) {
|
|
351
|
+
if (error instanceof ApiError && error.status >= 400 && error.status < 500) return loginFailure("No pudimos habilitar tu cuenta en esta tienda. Vuelve a iniciar sesión o contacta a la tienda.", error.status, headers, context);
|
|
352
|
+
return loginFailure("No pudimos completar el acceso. Intenta iniciar sesión de nuevo en unos minutos.", 503, headers, context);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
async function logout(request, context) {
|
|
356
|
+
const env = context.get(worker).env;
|
|
357
|
+
const origin = new URL(request.url).origin;
|
|
358
|
+
const configuration = await store(context).upsertCustomerAuthConfiguration(origin);
|
|
359
|
+
if (configuration.redirectUri !== new URL("/account/callback", origin).href) throw new Error("El dominio de salida no corresponde a esta tienda.");
|
|
360
|
+
const storeId = /^ecomiq-customer-(\d+)$/.exec(configuration.clientId)?.[1];
|
|
361
|
+
if (!storeId) throw new Error("La configuración de salida de la tienda no es válida.");
|
|
362
|
+
const sessionId = await customerSessionId(request, env);
|
|
363
|
+
if (sessionId) try {
|
|
364
|
+
await store(context).deleteCustomerSession({ headers: { "X-Customer-Session": sessionId } });
|
|
365
|
+
} catch (error) {
|
|
366
|
+
if (!(error instanceof ApiError) || error.status !== 401 && error.status !== 404) throw error;
|
|
367
|
+
}
|
|
368
|
+
const hostedLogout = new URL("/customer/logout", configuration.authority);
|
|
369
|
+
hostedLogout.searchParams.set("storeId", storeId);
|
|
370
|
+
hostedLogout.searchParams.set("origin", origin);
|
|
371
|
+
return redirect(hostedLogout.href, {
|
|
372
|
+
status: 303,
|
|
373
|
+
headers: {
|
|
374
|
+
...PRIVATE_HEADERS,
|
|
375
|
+
"Set-Cookie": await customerCookies(env).session.serialize("", { maxAge: 0 })
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
export { completeCustomerLogin, customer, customerCsrfToken, customerHeaders, logout, requireCustomer, requireCustomerRegistration, requireCustomerWith, startCustomerLogin, submitCustomerLogin };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { RouterContextProvider, data } from "react-router";
|
|
2
|
+
import { CustomerAuthConfiguration, CustomerRequestOptions } from "@ecomiq/core";
|
|
3
|
+
export type Customer = {
|
|
4
|
+
sessionId: string;
|
|
5
|
+
name: string;
|
|
6
|
+
};
|
|
7
|
+
export type CustomerLoginData = {
|
|
8
|
+
step: "email" | "code" | "secondFactor";
|
|
9
|
+
email: string;
|
|
10
|
+
captcha: CustomerAuthConfiguration["captcha"];
|
|
11
|
+
retryAt: number;
|
|
12
|
+
error: string | null;
|
|
13
|
+
};
|
|
14
|
+
export declare function customerCsrfToken(request: Request, context: Readonly<RouterContextProvider>): Promise<string>;
|
|
15
|
+
export declare function customer(request: Request, context: Readonly<RouterContextProvider>): Promise<Customer | null>;
|
|
16
|
+
export declare function requireCustomer(request: Request, context: Readonly<RouterContextProvider>): Promise<Customer>;
|
|
17
|
+
/** Enforces profile completion for authenticated account and checkout requests. */
|
|
18
|
+
export declare function requireCustomerRegistration(request: Request, context: Readonly<RouterContextProvider>, current: Customer): Promise<void>;
|
|
19
|
+
export declare function customerHeaders(current: Customer): CustomerRequestOptions;
|
|
20
|
+
export declare function startCustomerLogin(request: Request, context: Readonly<RouterContextProvider>): Promise<Response | ReturnType<typeof data<CustomerLoginData>>>;
|
|
21
|
+
/** Receives only browser form fields; the signed transaction owns all OAuth values. */
|
|
22
|
+
export declare function submitCustomerLogin(request: Request, context: Readonly<RouterContextProvider>): Promise<Response | ReturnType<typeof data<CustomerLoginData>>>;
|
|
23
|
+
export declare function completeCustomerLogin(request: Request, context: Readonly<RouterContextProvider>): Promise<Response>;
|
|
24
|
+
export declare function logout(request: Request, context: Readonly<RouterContextProvider>): Promise<Response>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// src/settings.ts
|
|
2
|
+
function link(url, _label, reference) {
|
|
3
|
+
return reference ? {
|
|
4
|
+
url,
|
|
5
|
+
reference
|
|
6
|
+
} : url;
|
|
7
|
+
}
|
|
8
|
+
function select(value, _label, _options) {
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
const setting = {
|
|
12
|
+
text: (value, _label) => value,
|
|
13
|
+
richtext: (html, _label) => html,
|
|
14
|
+
image: (url, _label) => url,
|
|
15
|
+
video: (url, _label) => url,
|
|
16
|
+
link,
|
|
17
|
+
collection: (handle, _label) => handle,
|
|
18
|
+
category: (handle, _label) => handle,
|
|
19
|
+
product: (handle, _label) => handle,
|
|
20
|
+
products: (handles, _label) => handles,
|
|
21
|
+
number: (value, _label) => value,
|
|
22
|
+
range: (value, _label, _limits) => value,
|
|
23
|
+
toggle: (value, _label) => value,
|
|
24
|
+
color: (value, _label) => value,
|
|
25
|
+
select,
|
|
26
|
+
scheme: (id, _label) => id
|
|
27
|
+
};
|
|
28
|
+
function plain(value) {
|
|
29
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
30
|
+
}
|
|
31
|
+
function section(defaults, props) {
|
|
32
|
+
const values = { ...defaults };
|
|
33
|
+
for (const key of Object.keys(defaults)) {
|
|
34
|
+
const value = props[key];
|
|
35
|
+
if (value === void 0) continue;
|
|
36
|
+
const base = defaults[key];
|
|
37
|
+
values[key] = plain(base) && plain(value) ? {
|
|
38
|
+
...base,
|
|
39
|
+
...value
|
|
40
|
+
} : value;
|
|
41
|
+
}
|
|
42
|
+
return values;
|
|
43
|
+
}
|
|
44
|
+
async function pickProducts(client, source) {
|
|
45
|
+
const limit = source.limit ?? 8;
|
|
46
|
+
if (source.products && source.products.length > 0) {
|
|
47
|
+
const slugs = source.products.slice(0, limit);
|
|
48
|
+
return (await client.getProducts({
|
|
49
|
+
slugs,
|
|
50
|
+
pageSize: slugs.length,
|
|
51
|
+
includeFilters: false
|
|
52
|
+
})).products;
|
|
53
|
+
}
|
|
54
|
+
if (source.collection) {
|
|
55
|
+
const collections = await client.getCollections().catch(() => []);
|
|
56
|
+
const slug = source.collection.trim().toLowerCase();
|
|
57
|
+
const collection = collections.find((item) => item.slug.toLowerCase() === slug);
|
|
58
|
+
if (collection === void 0) return [];
|
|
59
|
+
return (await client.getProducts({
|
|
60
|
+
collectionId: collection.id,
|
|
61
|
+
pageSize: limit,
|
|
62
|
+
sort: source.sort,
|
|
63
|
+
includeFilters: false
|
|
64
|
+
})).products;
|
|
65
|
+
}
|
|
66
|
+
if (source.category) return (await client.getProducts({
|
|
67
|
+
categories: [source.category],
|
|
68
|
+
pageSize: limit,
|
|
69
|
+
sort: source.sort,
|
|
70
|
+
includeFilters: false
|
|
71
|
+
})).products;
|
|
72
|
+
return (await client.getProducts({
|
|
73
|
+
pageSize: limit,
|
|
74
|
+
sort: source.sort,
|
|
75
|
+
includeFilters: false
|
|
76
|
+
})).products;
|
|
77
|
+
}
|
|
78
|
+
export { pickProducts, section, setting };
|