Skip to main content
Version: 3.x

3.2 -> 3.3

This page lists the highlights for upgrading a project from Front-Commerce 3.2 to 3.3.

Update dependencies

Update all your @front-commerce/* dependencies to this version:

pnpm update "@front-commerce/\*@3.3.0"

Automated Migration

We provide a codemod to automatically update your codebase to the latest version of Front-Commerce. This tool will update your code when possible and flag the places where you need to manually update your code (with // TODO Codemod comments).

pnpm run front-commerce migrate --transform 3.3.0

Search for TODO Codemod comments in your codebase. They could be added to for the following manual updates:

  • removing Set-Cookie headers defined in routes, they are now automatically set by Front-Commerce when needed (for a user session commit). See Automatic session cookie headers for details.
  • simplifying the entry.server.tsx file after automatic removal of transformers

Code changes

Remix entrypoints

In this release, we improved features that required to hook into your Remix application files created with the initial skeleton.
You must update these files in your application, as detailed below or copy the ones from the latest skeleton.

root.tsx file
diff --git a/app/root.tsx b/app/root.tsx
index a28dcf87e..68e788d96 100644
--- a/app/root.tsx
+++ b/app/root.tsx
@@ -21,30 +21,15 @@ import {
import { usePageProgress } from "theme/components/helpers/usePageProgress";
import "theme/main.css";
import config from "~/config/website";
-import { AppQueryDocument } from "~/graphql/graphql";
import { LiveReload, useSWEffect } from "@remix-pwa/sw";
import manifest from "~/manifest";

export const loader = async ({ context }: LoaderFunctionArgs) => {
const app = new FrontCommerceApp(context.frontCommerce);

- const response = await app.graphql.query(AppQueryDocument);
-
- if (!response.shop) {
- throw new Response("", { status: 500, statusText: "Shop not found" });
- }
-
return json({
- shop: response.shop,
device: app.config.device,
- process: {
- env: {
- NODE_ENV: process.env.NODE_ENV,
- SERVER: false,
- FRONT_COMMERCE_WEB_DEV_ANALYTICS_WARNING_DISABLE:
- process.env.FRONT_COMMERCE_WEB_DEV_ANALYTICS_WARNING_DISABLE,
- },
- },
+ publicConfig: app.config.public,
});
};
@@ -62,7 +62,7 @@ export const meta: MetaFunction = (args) => {
{ title: config.defaultTitle },
{ name: "robots", content: "Index,Follow" },
{ name: "description", content: config.defaultDescription },
- { name: "baseUrl", content: args.data?.shop?.url },
+ { name: "baseUrl", content: args.data?.publicConfig?.shop?.url },
...manifest?.pwa.metas,
];
if (config.themeColor) {
entry.client.tsx file
diff --git a/app/entry.client.tsx b/app/entry.client.tsx
index e1530e9f8..dcb9118c1 100644
--- a/app/entry.client.tsx
+++ b/app/entry.client.tsx
@@ -21,7 +21,9 @@ const StrictModeOrNoop = appManifest.v2_compat?.useApolloClientQueries
: StrictMode;

const hydrate = async () => {
- const shop = __remixContext.state.loaderData?.root.shop;
+ const rootLoaderData = __remixContext.state.loaderData?.root;
+ const shop = rootLoaderData?.publicConfig?.shop;
+
const messages = await fetch(`/translations/${shop?.locale}`).then(
(res) => res.json() || {}
);
entry.server.tsx file
diff --git a/app/entry.server.tsx b/app/entry.server.tsx
index ffd0db3d2..b39a41f34 100644
--- a/app/entry.server.tsx
+++ b/app/entry.server.tsx
@@ -32,7 +32,8 @@ export default async function handleRequest(
remixContext: EntryContext,
loadContext: AppLoadContext
) {
- const shop = remixContext.staticHandlerContext.loaderData.root.shop;
+ const rootLoaderData = __remixContext.state.loaderData?.root;
+ const shop = rootLoaderData?.publicConfig?.shop;
const messages = await loadTranslationMessages(shop.locale);

return isbot(request.headers.get("user-agent"))

Front-Commerce post-install script

In this release, we added a post-install script in order to patch an issue with the current version of @remix-run/dev we are using. To cope with this, please ensure you applied have the following change in your package.json:

// ...
"scripts": {
"build": "front-commerce build",
"dev": "front-commerce dev --manual -c \"pnpm run dev:server\"",
"dev:debug": "front-commerce dev --manual -c \"pnpm run dev:server --inspect\"",
"dev:server": "tsx watch --ignore ./build/version.txt --ignore ./build/index.js --clear-screen=false -r tsconfig-paths/register server.ts",
"start": "cross-env NODE_ENV=production tsx -r tsconfig-paths/register ./server.ts",
"translate": "front-commerce translate ./app/**/*.{js,jsx,ts,tsx} --locale en",
"front-commerce": "front-commerce",
- "typecheck": "tsc"
+ "typecheck": "tsc",
+ "postinstall": "front-commerce postinstall"
},
// ...

In this release, we added a feature that will automatically set the Set-Cookie header for the session cookie when the user session needs to be commited.

app/routes/my-route.ts
import { FrontCommerceApp } from "@front-commerce/remix";
import { json } from "@front-commerce/remix/node";
import { MyQuery } from "~/graphql/graphql";

const loader = async ({ context, request }) => {
const app = new FrontCommerceApp(context.frontCommerce);

const data = app.query(MyQuery);
- return json(data, {
- headers: {
- // TODO Codemod FC-3.3: please, remove the `Set-Cookie` header manually. Session is now automatically commited by FC.
- "Set-Cookie": await app.user.session.commit(),
- },
- });
+ return json(data);
}

Deprecate process.env.FRONT_COMMERCE_WEB_* usage

Breaking: In this release, we updated the way we handle public configuration.

To cope with these changes, you will need to ensure that you replace any of these usages with the new publicConfig values. We introduced a 3.3.0 codemod to automatically update your codebase, follow the Automated Migration to run the codemod.

Before:

const {
FRONT_COMMERCE_WEB_DEV_ANALYTICS_WARNING_DISABLE,
FRONT_COMMERCE_WEB_DEV_DISABLE_PASSWORD_HINT,
} = process.env;

const example = {
analyticsWarningDisable: FRONT_COMMERCE_WEB_DEV_ANALYTICS_WARNING_DISABLE,
disablePasswordHint: FRONT_COMMERCE_WEB_DEV_DISABLE_PASSWORD_HINT,
myCustomValue: process.env.FRONT_COMMERCE_WEB_MY_CUSTOM_VALUE,
};

After:

import { getPublicConfig } from "@front-commerce/core/react";

// After running your application for the first time,
// you should have all typed values in the public config
const publicConfig = getPublicConfig();

const example = {
analyticsWarningDisable: publicConfig?.analytics?.disableDevWarning ?? false,
disablePasswordHint: publicConfig?.password?.disablePasswordHint ?? false,
myCustomValue: getPublicConfig("FRONT_COMMERCE_WEB_MY_CUSTOM_VALUE"), // This will warn with a deprecation message
};
caution

We highly suggest that you move any of your custom values to a new config provider which extends the public schema.

See Public configurations documentation for more details.

Deprecate getCurrentShopConfig usage

In this version we have deprecated the usages of getCurrentShopConfig in favor config.shop, the function has been moved to @front-commerce/compat/shop/getShopConfig for compatibility reasons, but we suggest you to use config.shop instead.

// from a loader
export const loader = async ({ context }) => {
const app = new FrontCommerceApp(context.frontCommerce);
const currentShopConfig = app.config.shop;
// ...
};

// from an action
export const action = async ({ context }) => {
const app = new FrontCommerceApp(context.frontCommerce);
const currentShopConfig = app.config.shop;
// ...
};

// from a graphql resolver
export default {
Query: {
myQuery: async (parent, args, context, info) => {
const currentShopConfig = context.config.shop;
// ...
},
},
};

Theme chocolatine styles refactor

In this release, we refactored how the design tokens are included in the style sheets. They are now included through the _design-tokens.scss file.
If you have overwritten the theme/main.scss then you should either apply the changes as detailed below or copy the file from the latest skeleton.

  • theme/components/_components.scss has been completely removed
  • theme/main.scss design tokens importation
    theme/main.scss
    @import "theme/normalize";
    @import "theme/variables";
    @import "theme/components/components";
    @import "theme/design-tokens";

Since version 3.3.3

In this release, we fixed some issues related to the requisition lists in the theme.
If your project is using the requisition lists feature, you will need to update those files as detailed below, or copy the ones from the latest skeleton:

theme/modules/RequisitionList/AddCartToRequisitionList/AddCartToRequisitionList.jsx file
diff --git a/theme/modules/RequisitionList/AddCartToRequisitionList/AddCartToRequisitionList.jsx b/theme/modules/RequisitionList/AddCartToRequisitionList/AddCartToRequisitionList.jsx
index 3d7bcfc99..6c039d270 100644
--- a/theme/modules/RequisitionList/AddCartToRequisitionList/AddCartToRequisitionList.jsx
+++ b/theme/modules/RequisitionList/AddCartToRequisitionList/AddCartToRequisitionList.jsx
@@ -2,6 +2,7 @@ import { useMemo } from "react";
import PropTypes from "prop-types";
import AddToRequisitionList from "theme/modules/RequisitionList/AddToRequisitionList";
import { resolveSelectedOptions } from "theme/pages/Product/useSelectedProductWithConfigurableOptions";
+import { resolveSelectedBundleOptions } from "theme/pages/Product/useSelectedProductWithBundleOptions";
import {
productPropTypes,
selectedConfigurableOptionsPropTypes,
@@ -11,7 +12,7 @@ import {
const AddCartToRequisitionList = ({ id = "cart", cart, size }) => {
const cartItems = cart.items;
const items = useMemo(() => {
- return cartItems.map(({ product, options, qty }) => {
+ return cartItems.map(({ product, options, bundleOptions, qty }) => {
const productOptions = product.options?.map((option) => ({
id: option.attribute.id,
label: option.attribute.label,
@@ -23,6 +24,10 @@ const AddCartToRequisitionList = ({ id = "cart", cart, size }) => {
product.options,
options
),
+ selectedBundleOptions: resolveSelectedBundleOptions(
+ product.bundleOptions,
+ bundleOptions
+ ),
quantity: qty,
};
});
theme/modules/RequisitionList/AddProductToRequisitionList/AddProductToRequisitionList.jsx file
diff --git a/theme/modules/RequisitionList/AddProductToRequisitionList/AddProductToRequisitionList.jsx b/theme/modules/RequisitionList/AddProductToRequisitionList/AddProductToRequisitionList.jsx
index 0e5cc0958..d73bde2aa 100644
--- a/theme/modules/RequisitionList/AddProductToRequisitionList/AddProductToRequisitionList.jsx
+++ b/theme/modules/RequisitionList/AddProductToRequisitionList/AddProductToRequisitionList.jsx
@@ -34,6 +34,10 @@ const AddProductToRequisitionList = ({
showOptionsModalIfNotFullyConfigured={
showOptionsModalIfNotFullyConfigured
}
+ redirectOnAddToRequisitionList={
+ // URL to the bundle product
+ product.bundleOptions?.length ? `/product/${product.sku}` : null
+ }
/>
);
};
theme/modules/RequisitionList/AddToRequisitionList/AddToRequisitionList.jsx file
diff --git a/theme/modules/RequisitionList/AddToRequisitionList/AddToRequisitionList.jsx b/theme/modules/RequisitionList/AddToRequisitionList/AddToRequisitionList.jsx
index 13efd3b41..a79e5b025 100644
--- a/theme/modules/RequisitionList/AddToRequisitionList/AddToRequisitionList.jsx
+++ b/theme/modules/RequisitionList/AddToRequisitionList/AddToRequisitionList.jsx
@@ -5,6 +5,7 @@ import Icon from "theme/components/atoms/Icon";
import SelectMenu from "theme/components/molecules/SelectMenu";
import messages from "theme/modules/RequisitionList/AddToRequisitionList/AddToRequisitionListMessages";
import { useIntl } from "react-intl";
+import { useNavigate } from "@remix-run/react";

/** @type {import('./EnhanceAddToRequisitionList').BaseComponent} */
const AddToRequisitionList = ({
@@ -22,11 +23,24 @@ const AddToRequisitionList = ({
addToRequisitionListError,
newRequisitionListModal,
showOptionsModalIfNotFullyConfigured,
+ redirectOnAddToRequisitionList,
productConfigurationModal,
}) => {
const intl = useIntl();
const [isRequisitionListMenuOpen, setIsRequisitionListMenuOpen] =
useState(false);
+ const navigate = useNavigate();
+
+ const shouldRedirect =
+ showOptionsModalIfNotFullyConfigured &&
+ isRequisitionListMenuOpen &&
+ redirectOnAddToRequisitionList;
+
+ useEffect(() => {
+ if (shouldRedirect) {
+ navigate(redirectOnAddToRequisitionList);
+ }
+ }, [shouldRedirect]);

const selectItems = useMemo(() => {
if (!requisitionLists) {
theme/modules/RequisitionList/AddToRequisitionList/withAddMultipleItemsToRequisitionListMutation.jsx file
diff --git a/theme/modules/RequisitionList/AddToRequisitionList/withAddMultipleItemsToRequisitionListMutation.jsx b/theme/modules/RequisitionList/AddToRequisitionList/withAddMultipleItemsToRequisitionListMutation.jsx
index ff92ad7e1..e0ca0580e 100644
--- a/theme/modules/RequisitionList/AddToRequisitionList/withAddMultipleItemsToRequisitionListMutation.jsx
+++ b/theme/modules/RequisitionList/AddToRequisitionList/withAddMultipleItemsToRequisitionListMutation.jsx
@@ -75,20 +75,29 @@ const withAddMultipleItemsToRequisitionListMutation =
}) => ({
sku: product.sku,
quantity,
- selectedConfigurableOptions: Object.entries(
- selectedConfigurableOptions
- ).map(([option_id, option_value]) => ({
- option_id,
- option_value,
- })),
- selectedBundleOptions: Object.entries(
- selectedBundleOptions
- ).map(([option_id, { quantity, values }]) => {
- return {
- option_id,
- option_values: values.map((value) => ({ quantity, value })),
- };
- }),
+ selectedConfigurableOptions:
+ Object.keys(selectedConfigurableOptions || {}).length > 0
+ ? Object.entries(selectedConfigurableOptions).map(
+ ([option_id, option_value]) => ({
+ option_id,
+ option_value,
+ })
+ )
+ : undefined,
+ selectedBundleOptions:
+ Object.keys(selectedBundleOptions || {}).length > 0
+ ? Object.entries(selectedBundleOptions).map(
+ ([option_id, { quantity, values }]) => {
+ return {
+ option_id,
+ option_values: values.map((value) => ({
+ quantity,
+ value,
+ })),
+ };
+ }
+ )
+ : undefined,
})
),
},
theme/modules/RequisitionList/ProductConfigurationModal/ProductConfigurationModalContent.jsx file
diff --git a/theme/modules/RequisitionList/ProductConfigurationModal/ProductConfigurationModalContent.jsx b/theme/modules/RequisitionList/ProductConfigurationModal/ProductConfigurationModalContent.jsx
index 5883fae10..e0449a188 100644
--- a/theme/modules/RequisitionList/ProductConfigurationModal/ProductConfigurationModalContent.jsx
+++ b/theme/modules/RequisitionList/ProductConfigurationModal/ProductConfigurationModalContent.jsx
@@ -6,7 +6,7 @@ import { FormattedMessage } from "react-intl";
import useSelectedProductWithConfigurableOptions from "theme/pages/Product/useSelectedProductWithConfigurableOptions";
import ConfigurableOptions from "theme/modules/Cart/CartItem/CartItemOptionsUpdater/ConfigurableOptions";
import { H2 } from "theme/components/atoms/Typography/Heading";
-import Form from "theme/compat/components/atoms/Form/Form";
+import { Form } from "@remix-run/react";
import FormTitle from "theme/components/molecules/Form/FormTitle";
import useProductBySkuLoader from "theme/hooks/useProductBySkuLoader";
import Stack from "theme/components/atoms/Layout/Stack";
@@ -74,21 +74,22 @@ const ProductConfigurationModalContent = ({
selectedConfigurableOptions
);

- const formRef = useRef();
const [showNotAllOptionsSelected, setShowNotAllOptionsSelected] =
useState(false);

- const onChangeOptions = useCallback(() => {
- const model = formRef.current.getModel();
- Object.keys(model)
- .filter(
- (key) =>
- key.indexOf("custom:") !== 0 && typeof model[key] !== "undefined"
- )
- .forEach((optionId) =>
- setOption(optionId, model[optionId].value || model[optionId])
- );
- }, [setOption]);
+ const onChangeOptions = useCallback(
+ (e) => {
+ const input = e.target;
+ const form = input.form;
+ const data = new FormData(form);
+ for (const pair of data.entries()) {
+ if (pair[0].indexOf("custom:") !== 0) {
+ setOption(pair[0], pair[1]);
+ }
+ }
+ },
+ [setOption]
+ );

const allOptionsSet = useMemo(
() => selectedProduct && areAllOptionsSet(selectedProduct, selectedOptions),
@@ -119,8 +120,7 @@ const ProductConfigurationModalContent = ({

return (
<Form
- setRef={(form) => (formRef.current = form)}
- onValidSubmit={() => onConfiurationsSelected(selectedOptions)}
+ onSubmit={() => onConfiurationsSelected(selectedOptions)}
onChange={onChangeOptions}
>
<Stack>
theme/modules/Cart/CartItem/CartItemOptionsUpdater/CartItemOptionsUpdaterFragment.gql file
diff --git a/theme/modules/Cart/CartItem/CartItemOptionsUpdater/CartItemOptionsUpdaterFragment.gql b/theme/modules/Cart/CartItem/CartItemOptionsUpdater/CartItemOptionsUpdaterFragment.gql
index 19f85b84e..954bbb896 100644
--- a/theme/modules/Cart/CartItem/CartItemOptionsUpdater/CartItemOptionsUpdaterFragment.gql
+++ b/theme/modules/Cart/CartItem/CartItemOptionsUpdater/CartItemOptionsUpdaterFragment.gql
@@ -17,6 +17,14 @@ fragment CartItemCustomOption on Product {
id
}
}
+ bundleOptions {
+ id
+ label
+ values {
+ label
+ value
+ }
+ }
custom_options {
option_id
title