From e6e99c068e08c1e20094a26b15234a20a9b2f141 Mon Sep 17 00:00:00 2001 From: Guilherme Tavano Date: Thu, 31 Aug 2023 13:33:16 -0300 Subject: [PATCH 1/3] add proxy to shopify stores --- shopify/hooks/context.ts | 6 +-- shopify/hooks/useCart.ts | 16 +++++- shopify/loaders/proxy.ts | 109 +++++++++++++++++++++++++++++++++++++++ shopify/manifest.gen.ts | 6 ++- shopify/mod.ts | 5 ++ shopify/utils/types.ts | 40 +++++++------- 6 files changed, 155 insertions(+), 27 deletions(-) create mode 100644 shopify/loaders/proxy.ts diff --git a/shopify/hooks/context.ts b/shopify/hooks/context.ts index 7fb1f4a1c..7e6ea27e8 100644 --- a/shopify/hooks/context.ts +++ b/shopify/hooks/context.ts @@ -46,11 +46,7 @@ const enqueue = ( }; const load = async (signal: AbortSignal) => { - const { cart } = await Runtime.invoke({ - cart: { - key: "shopify/loaders/cart.ts", - }, - }, { signal }); + const cart = await Runtime.shopify.loaders.cart({}, {signal}) return { cart, diff --git a/shopify/hooks/useCart.ts b/shopify/hooks/useCart.ts index ecdd10175..1dc62b1ce 100644 --- a/shopify/hooks/useCart.ts +++ b/shopify/hooks/useCart.ts @@ -1,7 +1,21 @@ +import type { AnalyticsItem } from "../../commerce/types.ts"; import { Runtime } from "../runtime.ts"; -import { Cart } from "../utils/types.ts"; +import { Cart, Item } from "../utils/types.ts"; import { state as storeState } from "./context.ts"; +export const itemToAnalyticsItem = ( + item: Item & { quantity: number }, + index: number, +): AnalyticsItem => ({ + item_id: item.id, + item_name: item.merchandise.product.title, + discount: item.cost.compareAtAmountPerQuantity ? item.cost.compareAtAmountPerQuantity.amount - item.cost.amountPerQuantity?.amount : 0, + item_variant: item.merchandise.title, + price: item.cost.amountPerQuantity.amount, + index, + quantity: item.quantity, +}); + const { cart, loading } = storeState; const wrap = diff --git a/shopify/loaders/proxy.ts b/shopify/loaders/proxy.ts new file mode 100644 index 000000000..ebe22011e --- /dev/null +++ b/shopify/loaders/proxy.ts @@ -0,0 +1,109 @@ +import { Route } from "../../website/flags/audience.ts"; +import { AppContext } from "../mod.ts"; + +const PATHS_TO_PROXY = [ + "/checkouts/*", + "/cart/*", + "/account", + "/account/*", + "/password", + "/password/*", + "/challenge", + "/challenge/*", +]; +const decoSiteMapUrl = "/sitemap/deco.xml"; + +const buildProxyRoutes = ( + { publicUrl, extraPaths, includeSiteMap, generateDecoSiteMap }: { + publicUrl?: string; + extraPaths: string[]; + includeSiteMap?: string[]; + generateDecoSiteMap?: boolean; + }, +) => { + + if (!publicUrl) { + return []; + } + + try { + const hostname = (new URL( + publicUrl?.startsWith("http") ? publicUrl : `https://${publicUrl}`, + )).hostname; + + // Rejects TLD mystore.com, keep this if Shopify doesn't support + if (!hostname || hostname.split(".").length <= 2) { + throw new Error(`Invalid hostname from '${publicUrl}'`); + } + + // TODO @lucis: Fix the proxy, MITM + + const urlToProxy = `https://${hostname}`; + const hostToUse = hostname; + + const routeFromPath = (pathTemplate: string): Route => ({ + pathTemplate, + handler: { + value: { + __resolveType: "website/handlers/proxy.ts", + url: urlToProxy, + host: hostToUse, + }, + }, + }); + const routesFromPaths = [...PATHS_TO_PROXY, ...extraPaths].map( + routeFromPath, + ); + + const [include, routes] = generateDecoSiteMap + ? [[...(includeSiteMap ?? []), decoSiteMapUrl], [{ + pathTemplate: decoSiteMapUrl, + handler: { + value: { + __resolveType: "website/handlers/sitemap.ts", + }, + }, + }]] + : [includeSiteMap, []]; + + return [ + ...routes, + ...routesFromPaths, + ]; + } catch (e) { + console.log("Error parsing publicUrl from Shopify"); + console.error(e); + return []; + } +}; + +export interface Props { + extraPathsToProxy?: string[]; + /** + * @title Other site maps to include + */ + includeSiteMap?: string[]; + /** + * @title If deco site map should be exposed at /deco-sitemap.xml + */ + generateDecoSiteMap?: boolean; +} + +/** + * @title Shopify Proxy Routes + */ +function loader( + { extraPathsToProxy = [], includeSiteMap = [], generateDecoSiteMap = true }: + Props, + _req: Request, + ctx: AppContext, +): Route[] { + return buildProxyRoutes({ + generateDecoSiteMap, + includeSiteMap, + publicUrl: ctx.publicUrl, + extraPaths: extraPathsToProxy, + }); +} + +export default loader; diff --git a/shopify/manifest.gen.ts b/shopify/manifest.gen.ts index 6aaf6cd5e..bcb7051fe 100644 --- a/shopify/manifest.gen.ts +++ b/shopify/manifest.gen.ts @@ -5,17 +5,19 @@ import * as $$$0 from "./loaders/ProductList.ts"; import * as $$$1 from "./loaders/ProductDetailsPage.ts"; import * as $$$2 from "./loaders/ProductListingPage.ts"; -import * as $$$3 from "./loaders/cart.ts"; +import * as $$$3 from "./loaders/proxy.ts"; +import * as $$$4 from "./loaders/cart.ts"; import * as $$$$$$$$$0 from "./actions/cart/updateCoupons.ts"; import * as $$$$$$$$$1 from "./actions/cart/updateItems.ts"; import * as $$$$$$$$$2 from "./actions/cart/addItems.ts"; const manifest = { "loaders": { - "shopify/loaders/cart.ts": $$$3, + "shopify/loaders/cart.ts": $$$4, "shopify/loaders/ProductDetailsPage.ts": $$$1, "shopify/loaders/ProductList.ts": $$$0, "shopify/loaders/ProductListingPage.ts": $$$2, + "shopify/loaders/proxy.ts": $$$3, }, "actions": { "shopify/actions/cart/addItems.ts": $$$$$$$$$2, diff --git a/shopify/mod.ts b/shopify/mod.ts index 8cb0b96c1..9e4bb40dd 100644 --- a/shopify/mod.ts +++ b/shopify/mod.ts @@ -11,6 +11,11 @@ export interface Props { */ storeName: string; + /** + * @description Store public URL. + */ + publicUrl: string; + /** * @ttile Access Token * @description Shopify storefront access token. diff --git a/shopify/utils/types.ts b/shopify/utils/types.ts index f00f8f444..89c1b5c6f 100644 --- a/shopify/utils/types.ts +++ b/shopify/utils/types.ts @@ -140,28 +140,30 @@ export interface Image { altText: string; } +export interface Item{ + id: string; + quantity: number; + merchandise: { + id: string; + title: string; + product: { + title: string; + }; + image: Image; + price: Money; + }; + cost: { + totalAmount: Money; + subtotalAmount: Money; + amountPerQuantity: Money; + compareAtAmountPerQuantity: Money; + }; +} + export interface CartData { id: string; lines?: { - nodes: { - id: string; - quantity: number; - merchandise: { - id: string; - title: string; - product: { - title: string; - }; - image: Image; - price: Money; - }; - cost: { - totalAmount: Money; - subtotalAmount: Money; - amountPerQuantity: Money; - compareAtAmountPerQuantity: Money; - }; - }[]; + nodes: Item[]; }; checkoutUrl?: string; cost?: { From c2616ece8f156a3512d5d6608d6506992bb7eeb5 Mon Sep 17 00:00:00 2001 From: guitavano Date: Thu, 31 Aug 2023 14:34:24 -0300 Subject: [PATCH 2/3] update live --- shopify/mod.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shopify/mod.ts b/shopify/mod.ts index 9e4bb40dd..1bee2de72 100644 --- a/shopify/mod.ts +++ b/shopify/mod.ts @@ -11,10 +11,10 @@ export interface Props { */ storeName: string; - /** + /** * @description Store public URL. */ - publicUrl: string; + publicUrl: string; /** * @ttile Access Token From 21b7e91e24284cf7baf6f9fa076b5d6f25d3e704 Mon Sep 17 00:00:00 2001 From: gimenes Date: Fri, 1 Sep 2023 10:23:28 -0300 Subject: [PATCH 3/3] re-use storeName --- shopify/loaders/proxy.ts | 19 +++++++------------ shopify/mod.ts | 5 ----- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/shopify/loaders/proxy.ts b/shopify/loaders/proxy.ts index ebe22011e..ed56aff07 100644 --- a/shopify/loaders/proxy.ts +++ b/shopify/loaders/proxy.ts @@ -14,22 +14,17 @@ const PATHS_TO_PROXY = [ const decoSiteMapUrl = "/sitemap/deco.xml"; const buildProxyRoutes = ( - { publicUrl, extraPaths, includeSiteMap, generateDecoSiteMap }: { - publicUrl?: string; + { storeName, extraPaths, includeSiteMap, generateDecoSiteMap }: { + storeName?: string; extraPaths: string[]; includeSiteMap?: string[]; generateDecoSiteMap?: boolean; }, ) => { - - if (!publicUrl) { - return []; - } + const publicUrl = new URL(`https://${storeName}.myshopify.com`); try { - const hostname = (new URL( - publicUrl?.startsWith("http") ? publicUrl : `https://${publicUrl}`, - )).hostname; + const hostname = publicUrl.hostname; // Rejects TLD mystore.com, keep this if Shopify doesn't support if (!hostname || hostname.split(".").length <= 2) { @@ -55,7 +50,7 @@ const buildProxyRoutes = ( routeFromPath, ); - const [include, routes] = generateDecoSiteMap + const [_include, routes] = generateDecoSiteMap ? [[...(includeSiteMap ?? []), decoSiteMapUrl], [{ pathTemplate: decoSiteMapUrl, handler: { @@ -65,7 +60,7 @@ const buildProxyRoutes = ( }, }]] : [includeSiteMap, []]; - + return [ ...routes, ...routesFromPaths, @@ -101,7 +96,7 @@ function loader( return buildProxyRoutes({ generateDecoSiteMap, includeSiteMap, - publicUrl: ctx.publicUrl, + storeName: ctx.storeName, extraPaths: extraPathsToProxy, }); } diff --git a/shopify/mod.ts b/shopify/mod.ts index 1bee2de72..8cb0b96c1 100644 --- a/shopify/mod.ts +++ b/shopify/mod.ts @@ -11,11 +11,6 @@ export interface Props { */ storeName: string; - /** - * @description Store public URL. - */ - publicUrl: string; - /** * @ttile Access Token * @description Shopify storefront access token.