Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions core/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Dependencies
node_modules/

# Build outputs
.next/
.wrangler/
.open-next/
out/
dist/
build/

# Generated files
.turbo/
messages/*.d.json.ts
next-env.d.ts
*-graphql.d.ts

# Test outputs
playwright-report/
test-results/
.tests/

# Cache
.eslintcache
7 changes: 0 additions & 7 deletions core/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,6 @@ const config = {
},
},
],
ignorePatterns: [
'client/generated/**/*.ts',
'playwright-report/**',
'test-results/**',
'.tests/**',
'**/google_analytics4.js',
],
};

module.exports = config;

This file was deleted.

5 changes: 3 additions & 2 deletions core/app/[locale]/(default)/cart/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { getPreferredCurrencyCode } from '~/lib/currency';
import { Slot } from '~/lib/makeswift/slot';
import { exists } from '~/lib/utils';

import { redirectToCheckout } from './_actions/redirect-to-checkout';
import { updateCouponCode } from './_actions/update-coupon-code';
import { updateGiftCertificate } from './_actions/update-gift-certificate';
import { updateLineItem } from './_actions/update-line-item';
Expand All @@ -22,6 +21,8 @@ interface Props {
params: Promise<{ locale: string }>;
}

const CHECKOUT_URL = process.env.TRAILING_SLASH !== 'false' ? '/checkout/' : '/checkout';

export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { locale } = await params;

Expand Down Expand Up @@ -253,7 +254,7 @@ export default async function Cart({ params }: Props) {
},
].filter(exists),
}}
checkoutAction={redirectToCheckout}
checkoutAction={CHECKOUT_URL}
checkoutLabel={t('proceedToCheckout')}
couponCode={{
action: updateCouponCode,
Expand Down
7 changes: 7 additions & 0 deletions core/app/[locale]/(default)/checkout/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { BigCommerceAuthError } from '@bigcommerce/catalyst-client';
import { unstable_rethrow as rethrow } from 'next/navigation';
import { NextRequest, NextResponse } from 'next/server';
import { getTranslations } from 'next-intl/server';

import { getSessionCustomerAccessToken } from '~/auth';
import { getChannelIdFromLocale } from '~/channels.config';
Expand All @@ -10,6 +11,7 @@ import { redirect } from '~/i18n/routing';
import { getVisitIdCookie, getVisitorIdCookie } from '~/lib/analytics/bigcommerce';
import { getCartId } from '~/lib/cart';
import { getConsentCookie } from '~/lib/consent-manager/cookies/server';
import { serverToast } from '~/lib/server-toast';

const CheckoutRedirectMutation = graphql(`
mutation CheckoutRedirectMutation(
Expand Down Expand Up @@ -55,8 +57,11 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ loca
const cartId = req.nextUrl.searchParams.get('cartId') ?? (await getCartId());
const customerAccessToken = await getSessionCustomerAccessToken();
const channelId = getChannelIdFromLocale(locale);
const t = await getTranslations('Cart.Errors');

if (!cartId) {
await serverToast.error(t('cartNotFound'));

return redirect({ href: '/cart', locale });
}

Expand Down Expand Up @@ -86,6 +91,8 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ loca
data.cart.createCartRedirectUrls.errors.length > 0 ||
!data.cart.createCartRedirectUrls.redirectUrls
) {
await serverToast.error(t('somethingWentWrong'));

return redirect({ href: '/cart', locale });
}

Expand Down
2 changes: 1 addition & 1 deletion core/lib/server-toast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ async function setToastCookie(data: ServerToastData) {
sameSite: 'strict',
path: '/',
partitioned: true,
maxAge: 0,
maxAge: 1,
});
}

Expand Down
8 changes: 4 additions & 4 deletions core/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@
"couponCode": "Coupon code",
"removeCouponCode": "Remove coupon code",
"invalidCouponCode": "Please enter a valid coupon code",
"cartNotFound": "No cartId cookie found."
"cartNotFound": "An error occurred when retrieving your cart"
},
"Shipping": {
"shipping": "Shipping",
Expand All @@ -380,7 +380,7 @@
"shippingOptions": "Shipping options",
"updateShipping": "Update shipping",
"addShipping": "Add shipping",
"cartNotFound": "No cartId cookie found.",
"cartNotFound": "An error occurred when retrieving your cart",
"noShippingOptions": "There are no shipping options available for your address"
}
},
Expand All @@ -392,15 +392,15 @@
"to": "To",
"message": "Message",
"invalidGiftCertificate": "Please enter a valid gift certificate code",
"cartNotFound": "No cartId cookie found."
"cartNotFound": "An error occurred when retrieving your cart"
},
"Empty": {
"title": "Your cart is empty.",
"subtitle": "Add some products to get started.",
"cta": "Continue shopping"
},
"Errors": {
"cartNotFound": "No cartId cookie found.",
"cartNotFound": "An error occurred when retrieving your cart",
"lineItemNotFound": "Line item not found.",
"failedToUpdateQuantity": "Failed to update quantity.",
"somethingWentWrong": "Something went wrong. Please try again later."
Expand Down
5 changes: 3 additions & 2 deletions core/package.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
{
"name": "@bigcommerce/catalyst-makeswift",
"description": "BigCommerce Catalyst is a Next.js starter kit for building headless BigCommerce storefronts.",
"version": "1.3.0",
"version": "1.3.1",
"private": true,
"scripts": {
"dev": "npm run generate && next dev",
"generate": "dotenv -e .env.local -- node ./scripts/generate.cjs",
"build": "npm run generate && next build",
"build:analyze": "ANALYZE=true npm run build",
"start": "next start",
"lint": "next lint",
"prelint": "next typegen",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"typecheck": "tsc --noEmit"
},
"dependencies": {
Expand Down
19 changes: 16 additions & 3 deletions core/vibes/soul/sections/cart/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ export interface CartProps<LineItem extends CartLineItem> {
summaryTitle?: string;
emptyState?: CartEmptyState;
lineItemAction: Action<CartState<LineItem>, FormData>;
checkoutAction: Action<SubmissionResult | null, FormData>;
checkoutAction: Action<SubmissionResult | null, FormData> | string;
checkoutLabel?: string;
deleteLineItemLabel?: string;
decrementLineItemLabel?: string;
Expand Down Expand Up @@ -628,10 +628,23 @@ function CheckoutButton({
isCartUpdatePending,
...props
}: {
action: Action<SubmissionResult | null, FormData>;
action: Action<SubmissionResult | null, FormData> | string;
isCartUpdatePending: boolean;
} & ComponentPropsWithoutRef<typeof Button>) {
const [lastResult, formAction] = useActionState(action, null);
const [lastResult, formAction] = useActionState(
async (state: SubmissionResult | null, formData: FormData) => {
if (typeof action === 'string') {
await new Promise<void>(() => {
window.location.assign(action);
});

return null;
}

return action(state, formData);
},
Comment on lines +643 to +645

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: useActionState creates an unresolvable Promise when action is a string, causing the form to hang in a pending state.
Severity: HIGH | Confidence: 1.00

🔍 Detailed Analysis

The useActionState hook in core/vibes/soul/sections/cart/client.tsx creates a Promise that never settles when the action is a string. The new Promise<void>(() => { window.location.assign(action); }) executor function is missing the resolve() callback. This causes the async function to never return, leaving the form action in a perpetual "pending" state. Consequently, the UI's loading indicator remains active indefinitely during page navigation.

💡 Suggested Fix

When action is a string, directly call window.location.assign(action) and return null without awaiting a Promise.

🤖 Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: core/vibes/soul/sections/cart/client.tsx#L643-L645

Potential issue: The `useActionState` hook in `core/vibes/soul/sections/cart/client.tsx`
creates a Promise that never settles when the `action` is a string. The `new
Promise<void>(() => { window.location.assign(action); })` executor function is missing
the `resolve()` callback. This causes the async function to never return, leaving the
form action in a perpetual "pending" state. Consequently, the UI's loading indicator
remains active indefinitely during page navigation.

Did we get this right? 👍 / 👎 to inform future reviews.

Reference_id: 2624952

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intended and not a bug. This happens when the page is being navigated away from, so that the checkout button can have a pending state.

null,
);

const [form] = useForm({ lastResult });

Expand Down