Skip to content

Commit

Permalink
feat(core): Add currency selector to header (#1912)
Browse files Browse the repository at this point in the history
* Add currency selector

Co-Authored-By: Chancellor Clark <[email protected]>

* Abstract cart cookie handling to a common lib

* Change currency of cart

* Only show transactional currencies in switcher (for now)

* Use default currency when there is no preference specified in cookie

* Add cart ID + version to key to invalidate cart on currency change

---------

Co-authored-by: Chancellor Clark <[email protected]>
  • Loading branch information
bookernath and chanceaclark authored Jan 22, 2025
1 parent ee42e45 commit da2a462
Show file tree
Hide file tree
Showing 25 changed files with 341 additions and 49 deletions.
5 changes: 5 additions & 0 deletions .changeset/gold-ducks-tie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@bigcommerce/catalyst-core": minor
---

Add currency selector to header
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { PaginationFragment } from '~/client/fragments/pagination';
import { graphql, VariablesOf } from '~/client/graphql';
import { revalidate } from '~/client/revalidate-target';
import { ProductCardFragment } from '~/components/product-card/fragment';
import { getPreferredCurrencyCode } from '~/lib/currency';

const GetProductSearchResultsQuery = graphql(
`
Expand All @@ -18,6 +19,7 @@ const GetProductSearchResultsQuery = graphql(
$before: String
$filters: SearchProductsFiltersInput!
$sort: SearchProductsSortInput
$currencyCode: currencyCode
) {
site {
search {
Expand Down Expand Up @@ -168,12 +170,13 @@ interface ProductSearch {
const getProductSearchResults = cache(
async ({ limit = 9, after, before, sort, filters }: ProductSearch) => {
const customerAccessToken = await getSessionCustomerAccessToken();
const currencyCode = await getPreferredCurrencyCode();
const filterArgs = { filters, sort };
const paginationArgs = before ? { last: limit, before } : { first: limit, after };

const response = await client.fetch({
document: GetProductSearchResultsQuery,
variables: { ...filterArgs, ...paginationArgs },
variables: { ...filterArgs, ...paginationArgs, currencyCode },
customerAccessToken,
fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate: 300 } },
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
import { BigCommerceGQLError } from '@bigcommerce/catalyst-client';
import { SubmissionResult } from '@conform-to/react';
import { parseWithZod } from '@conform-to/zod';
import { cookies } from 'next/headers';
import { getLocale, getTranslations } from 'next-intl/server';
import { z } from 'zod';

import { getSessionCustomerAccessToken } from '~/auth';
import { client } from '~/client';
import { graphql } from '~/client/graphql';
import { redirect } from '~/i18n/routing';
import { getCartId } from '~/lib/cart';

const CheckoutRedirectMutation = graphql(`
mutation CheckoutRedirectMutation($cartId: String!) {
Expand All @@ -30,13 +30,12 @@ export const redirectToCheckout = async (
): Promise<SubmissionResult | null> => {
const locale = await getLocale();
const t = await getTranslations('Cart.Errors');
const cookieStore = await cookies();

const customerAccessToken = await getSessionCustomerAccessToken();

const submission = parseWithZod(formData, { schema: z.object({}) });

const cartId = cookieStore.get('cartId')?.value;
const cartId = await getCartId();

if (!cartId) {
return submission.reply({ formErrors: [t('cartNotFound')] });
Expand Down
7 changes: 3 additions & 4 deletions core/app/[locale]/(default)/cart/_actions/remove-item.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
'use server';

import { unstable_expireTag } from 'next/cache';
import { cookies } from 'next/headers';
import { getTranslations } from 'next-intl/server';

import { getSessionCustomerAccessToken } from '~/auth';
import { client } from '~/client';
import { graphql, VariablesOf } from '~/client/graphql';
import { TAGS } from '~/client/tags';
import { clearCartId, getCartId } from '~/lib/cart';

const DeleteCartLineItemMutation = graphql(`
mutation DeleteCartLineItemMutation($input: DeleteCartLineItemInput!) {
Expand All @@ -31,8 +31,7 @@ export async function removeItem({

const customerAccessToken = await getSessionCustomerAccessToken();

const cookieStore = await cookies();
const cartId = cookieStore.get('cartId')?.value;
const cartId = await getCartId();

if (!cartId) {
throw new Error(t('cartNotFound'));
Expand Down Expand Up @@ -60,7 +59,7 @@ export async function removeItem({
// so we need to remove the cartId cookie
// TODO: We need to figure out if it actually failed.
if (!cart) {
cookieStore.delete('cartId');
await clearCartId();
}

unstable_expireTag(TAGS.cart);
Expand Down
5 changes: 2 additions & 3 deletions core/app/[locale]/(default)/cart/_actions/update-quantity.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
'use server';

import { unstable_expirePath } from 'next/cache';
import { cookies } from 'next/headers';
import { getTranslations } from 'next-intl/server';

import { getSessionCustomerAccessToken } from '~/auth';
import { client } from '~/client';
import { graphql, VariablesOf } from '~/client/graphql';
import { getCartId } from '~/lib/cart';

import { removeItem } from './remove-item';

Expand Down Expand Up @@ -44,8 +44,7 @@ export const updateQuantity = async ({

const customerAccessToken = await getSessionCustomerAccessToken();

const cookieStore = await cookies();
const cartId = cookieStore.get('cartId')?.value;
const cartId = await getCartId();

if (!cartId) {
throw new Error(t('cartNotFound'));
Expand Down
1 change: 1 addition & 0 deletions core/app/[locale]/(default)/cart/page-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ const CartPageQuery = graphql(
site {
cart(entityId: $cartId) {
entityId
version
currencyCode
lineItems {
physicalItems {
Expand Down
5 changes: 3 additions & 2 deletions core/app/[locale]/(default)/cart/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Metadata } from 'next';
import { cookies } from 'next/headers';
import { getFormatter, getTranslations } from 'next-intl/server';

import { Cart as CartComponent, CartEmptyState } from '@/vibes/soul/sections/cart';
import { getCartId } from '~/lib/cart';

import { redirectToCheckout } from './_actions/redirect-to-checkout';
import { updateLineItem } from './_actions/update-line-item';
Expand All @@ -20,7 +20,7 @@ export async function generateMetadata(): Promise<Metadata> {
export default async function Cart() {
const t = await getTranslations('Cart');
const format = await getFormatter();
const cartId = (await cookies()).get('cartId')?.value;
const cartId = await getCartId();

if (!cartId) {
return (
Expand Down Expand Up @@ -98,6 +98,7 @@ export default async function Cart() {
cta: { label: t('Empty.cta'), href: '/shop-all' },
}}
incrementLineItemLabel={t('increment')}
key={`${cart.entityId}-${cart.version}`}
lineItemAction={updateLineItem}
lineItems={formattedLineItems}
summary={{
Expand Down
14 changes: 3 additions & 11 deletions core/app/[locale]/(default)/compare/_actions/add-to-cart.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use server';

import { unstable_expireTag } from 'next/cache';
import { cookies } from 'next/headers';

import {
addCartLineItem,
Expand All @@ -10,12 +9,12 @@ import {
import { assertCreateCartErrors, createCart } from '~/client/mutations/create-cart';
import { getCart } from '~/client/queries/get-cart';
import { TAGS } from '~/client/tags';
import { getCartId, setCartId } from '~/lib/cart';

export const addToCart = async (data: FormData) => {
const productEntityId = Number(data.get('product_id'));

const cookieStore = await cookies();
const cartId = cookieStore.get('cartId')?.value;
const cartId = await getCartId();

let cart;

Expand Down Expand Up @@ -55,14 +54,7 @@ export const addToCart = async (data: FormData) => {
return { status: 'error', error: 'Failed to add product to cart.' };
}

cookieStore.set({
name: 'cartId',
value: cart.entityId,
httpOnly: true,
sameSite: 'lax',
secure: true,
path: '/',
});
await setCartId(cart.entityId);

unstable_expireTag(TAGS.cart);

Expand Down
5 changes: 4 additions & 1 deletion core/app/[locale]/(default)/compare/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Link } from '~/components/link';
import { SearchForm } from '~/components/search-form';
import { Button } from '~/components/ui/button';
import { Rating } from '~/components/ui/rating';
import { getPreferredCurrencyCode } from '~/lib/currency';
import { cn } from '~/lib/utils';

import { AddToCart } from './_components/add-to-cart';
Expand All @@ -39,7 +40,7 @@ const CompareParamsSchema = z.object({

const ComparePageQuery = graphql(
`
query ComparePageQuery($entityIds: [Int!], $first: Int) {
query ComparePageQuery($entityIds: [Int!], $first: Int, $currencyCode: currencyCode) {
site {
products(entityIds: $entityIds, first: $first) {
edges {
Expand Down Expand Up @@ -99,6 +100,7 @@ export default async function Compare(props: Props) {
const t = await getTranslations('Compare');
const format = await getFormatter();
const customerAccessToken = await getSessionCustomerAccessToken();
const currencyCode = await getPreferredCurrencyCode();

const parsed = CompareParamsSchema.parse(searchParams);
const productIds = parsed.ids?.filter((id) => !Number.isNaN(id));
Expand All @@ -108,6 +110,7 @@ export default async function Compare(props: Props) {
variables: {
entityIds: productIds ?? [],
first: productIds?.length ? MAX_COMPARE_LIMIT : 0,
currencyCode,
},
customerAccessToken,
fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate } },
Expand Down
6 changes: 4 additions & 2 deletions core/app/[locale]/(default)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ import { FeaturedProductsCarouselFragment } from '~/components/featured-products
import { FeaturedProductsListFragment } from '~/components/featured-products-list/fragment';
import { Subscribe } from '~/components/subscribe';
import { productCardTransformer } from '~/data-transformers/product-card-transformer';
import { getPreferredCurrencyCode } from '~/lib/currency';

import { Slideshow } from './_components/slideshow';

const HomePageQuery = graphql(
`
query HomePageQuery {
query HomePageQuery($currencyCode: currencyCode) {
site {
featuredProducts(first: 12) {
edges {
Expand All @@ -41,10 +42,11 @@ const HomePageQuery = graphql(

const getPageData = cache(async () => {
const customerAccessToken = await getSessionCustomerAccessToken();

const currencyCode = await getPreferredCurrencyCode();
const { data } = await client.fetch({
document: HomePageQuery,
customerAccessToken,
variables: { currencyCode },
fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate } },
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { BigCommerceGQLError } from '@bigcommerce/catalyst-client';
import { SubmissionResult } from '@conform-to/react';
import { parseWithZod } from '@conform-to/zod';
import { unstable_expireTag } from 'next/cache';
import { cookies } from 'next/headers';
import { getTranslations } from 'next-intl/server';
import { ReactNode } from 'react';

Expand All @@ -15,6 +14,7 @@ import { createCart } from '~/client/mutations/create-cart';
import { getCart } from '~/client/queries/get-cart';
import { TAGS } from '~/client/tags';
import { Link } from '~/components/link';
import { getCartId, setCartId } from '~/lib/cart';

type CartSelectedOptionsInput = ReturnType<typeof graphql.scalar<'CartSelectedOptionsInput'>>;

Expand Down Expand Up @@ -43,8 +43,7 @@ export const addToCart = async (
const productEntityId = Number(submission.value.id);
const quantity = Number(submission.value.quantity);

const cookieStore = await cookies();
const cartId = cookieStore.get('cartId')?.value;
const cartId = await getCartId();

let cart;

Expand Down Expand Up @@ -217,14 +216,7 @@ export const addToCart = async (
};
}

cookieStore.set({
name: 'cartId',
value: cart.entityId,
httpOnly: true,
sameSite: 'lax',
secure: true,
path: '/',
});
await setCartId(cart.entityId);

unstable_expireTag(TAGS.cart);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const ProductSchemaFragment = graphql(`
defaultImage {
url: urlTemplate(lossy: true)
}
prices {
prices(currencyCode: $currencyCode) {
price {
value
currencyCode
Expand Down
5 changes: 4 additions & 1 deletion core/app/[locale]/(default)/product/[slug]/page-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { PricingFragment } from '~/client/fragments/pricing';
import { graphql, VariablesOf } from '~/client/graphql';
import { revalidate } from '~/client/revalidate-target';
import { FeaturedProductsCarouselFragment } from '~/components/featured-products-carousel/fragment';
import { getPreferredCurrencyCode } from '~/lib/currency';

import { ProductSchemaFragment } from './_components/product-schema/fragment';
import { ProductViewedFragment } from './_components/product-viewed/fragment';
Expand Down Expand Up @@ -211,6 +212,7 @@ const ProductPageQuery = graphql(
$entityId: Int!
$optionValueIds: [OptionValueId!]
$useDefaultOptionSelections: Boolean
$currencyCode: currencyCode
) {
site {
product(
Expand Down Expand Up @@ -254,10 +256,11 @@ type Variables = VariablesOf<typeof ProductPageQuery>;

export const getProductData = cache(async (variables: Variables) => {
const customerAccessToken = await getSessionCustomerAccessToken();
const currencyCode = await getPreferredCurrencyCode();

const { data } = await client.fetch({
document: ProductPageQuery,
variables,
variables: { ...variables, currencyCode },
customerAccessToken,
fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate } },
});
Expand Down
3 changes: 3 additions & 0 deletions core/app/[locale]/(default)/product/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ProductDetail } from '@/vibes/soul/sections/product-detail';
import { pricesTransformer } from '~/data-transformers/prices-transformer';
import { productCardTransformer } from '~/data-transformers/product-card-transformer';
import { productOptionsTransformer } from '~/data-transformers/product-options-transformer';
import { getPreferredCurrencyCode } from '~/lib/currency';

import { addToCart } from './_actions/add-to-cart';
import { ProductSchema } from './_components/product-schema';
Expand Down Expand Up @@ -204,6 +205,7 @@ export async function generateMetadata(props: Props): Promise<Metadata> {
export default async function Product(props: Props) {
const searchParams = await props.searchParams;
const params = await props.params;
const currencyCode = await getPreferredCurrencyCode();

const { locale, slug } = params;

Expand All @@ -219,6 +221,7 @@ export default async function Product(props: Props) {
entityId: productId,
optionValueIds,
useDefaultOptionSelections: true,
currencyCode,
});

return (
Expand Down
5 changes: 4 additions & 1 deletion core/app/[locale]/not-found.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ import { Footer } from '~/components/footer/footer';
import { Header } from '~/components/header';
import { ProductCardFragment } from '~/components/product-card/fragment';
import { productCardTransformer } from '~/data-transformers/product-card-transformer';
import { getPreferredCurrencyCode } from '~/lib/currency';

const NotFoundQuery = graphql(
`
query NotFoundQuery {
query NotFoundQuery($currencyCode: currencyCode) {
site {
featuredProducts(first: 10) {
edges {
Expand All @@ -31,8 +32,10 @@ const NotFoundQuery = graphql(

async function getFeaturedProducts(): Promise<CarouselProduct[]> {
const format = await getFormatter();
const currencyCode = await getPreferredCurrencyCode();
const { data } = await client.fetch({
document: NotFoundQuery,
variables: { currencyCode },
fetchOptions: { next: { revalidate } },
});

Expand Down
Loading

0 comments on commit da2a462

Please sign in to comment.