Switch shop nav from Cart to Checkout during an active checkout session.

Show item quantity on both nav states and highlight checkout while the user is on the payment page.
This commit is contained in:
2026-09-11 20:22:22 +02:00
parent ffc9a0bbe5
commit f1715f7e7f
8 changed files with 43 additions and 6 deletions
@@ -7,6 +7,7 @@ import { StorefrontCartCookieService } from './StorefrontCartCookieService';
import { StorefrontFeedbackCookieService } from './StorefrontFeedbackCookieService';
import { StorefrontOrderAuthCookieService } from './StorefrontOrderAuthCookieService';
import { StorefrontThemeCookieService } from './StorefrontThemeCookieService';
import { StorefrontCheckoutSessionCookieService } from './StorefrontCheckoutSessionCookieService';
import type { StorefrontPageMetaInput } from '../types/StorefrontPageMetaInput';
import type { StorefrontShopViewServiceOverrides } from '../types/StorefrontShopViewServiceTestTypes';
import { PaymentMethod } from '../../payment/types/PaymentMethod';
@@ -22,6 +23,7 @@ describe('StorefrontShopViewService', () => {
cart = [],
feedback,
authorizedOrderIds = [],
checkoutSessionId,
theme = 'system',
branding = {
logoUrl: null,
@@ -62,6 +64,9 @@ describe('StorefrontShopViewService', () => {
const themeCookieService = {
getTheme: jest.fn().mockReturnValue(theme)
} as unknown as StorefrontThemeCookieService;
const checkoutSessionCookieService = {
getSessionId: jest.fn().mockReturnValue(checkoutSessionId)
} as unknown as StorefrontCheckoutSessionCookieService;
const service = new StorefrontShopViewService(
exchangeRateService,
@@ -70,7 +75,8 @@ describe('StorefrontShopViewService', () => {
cartCookieService,
feedbackCookieService,
orderAuthCookieService,
themeCookieService
themeCookieService,
checkoutSessionCookieService
);
const req = {
@@ -100,6 +106,22 @@ describe('StorefrontShopViewService', () => {
productJsonLd: null
});
expect(locals.themePreference).toBe('system');
expect(locals.hasActiveCheckout).toBe(false);
});
it('exposes active checkout state from the checkout session cookie', async () => {
const { service, req, res } = createService({
checkoutSessionId: 'session-1',
req: { path: '/shop/checkout', originalUrl: '/shop/checkout' }
});
const locals = await service.buildShopRenderLocals(req, res, {
title: 'Checkout',
metaDescription: 'Complete your purchase at {shopName}.'
});
expect(locals.hasActiveCheckout).toBe(true);
expect(locals.shopNavActive).toEqual({ activeShopNav: 'checkout', activeOrderId: null });
});
it('exposes shop settings, cart qty, feedback, branding, theme, and request context', async () => {
@@ -19,6 +19,7 @@ import type { StorefrontPageMetaInput } from '../types/StorefrontPageMetaInput';
import type { StorefrontProductJsonLdInput } from '../types/StorefrontProductJsonLdInput';
import { resolveShopNavActive } from '../utils/resolveShopNavActive';
import { StorefrontCartCookieService } from './StorefrontCartCookieService';
import { StorefrontCheckoutSessionCookieService } from './StorefrontCheckoutSessionCookieService';
import { StorefrontFeedbackCookieService } from './StorefrontFeedbackCookieService';
import { StorefrontOrderAuthCookieService } from './StorefrontOrderAuthCookieService';
import { StorefrontThemeCookieService } from './StorefrontThemeCookieService';
@@ -32,7 +33,8 @@ export class StorefrontShopViewService {
private readonly cartCookieService: StorefrontCartCookieService,
private readonly feedbackCookieService: StorefrontFeedbackCookieService,
private readonly orderAuthCookieService: StorefrontOrderAuthCookieService,
private readonly themeCookieService: StorefrontThemeCookieService
private readonly themeCookieService: StorefrontThemeCookieService,
private readonly checkoutSessionCookieService: StorefrontCheckoutSessionCookieService
) {}
async buildShopRenderLocals(req: Request, res: Response, page: StorefrontPageMetaInput): Promise<ShopRenderLocals> {
@@ -41,6 +43,7 @@ export class StorefrontShopViewService {
const themePreference = this.themeCookieService.getTheme(req, res);
const cartTotalQty = getTotalCartQtyFromCart(cart);
const hasActiveCheckout = Boolean(this.checkoutSessionCookieService.getSessionId(req, res));
const { shopName, shopFiatCurrency, enabledPaymentMethods } = this.configService.get(
'shopSettings'
@@ -62,6 +65,7 @@ export class StorefrontShopViewService {
title: page.title,
shopName,
cartTotalQty,
hasActiveCheckout,
feedback,
authorizedOrders,
shopNavActive,
@@ -1 +1 @@
export type ShopNavKey = 'shop' | 'cart' | 'check-order';
export type ShopNavKey = 'shop' | 'cart' | 'checkout' | 'check-order';
@@ -10,6 +10,7 @@ export type ShopRenderLocals = {
title: string;
shopName: string;
cartTotalQty: number;
hasActiveCheckout: boolean;
feedback: StorefrontFeedback | undefined;
authorizedOrders: AuthorizedOrderNavItem[];
shopNavActive: ShopNavActive;
@@ -6,6 +6,7 @@ export type StorefrontShopViewServiceOverrides = {
cart?: { variantId: string; qty: number }[];
feedback?: { type: 'success'; text: string };
authorizedOrderIds?: string[];
checkoutSessionId?: string;
theme?: StorefrontThemePreference;
branding?: {
logoUrl: string | null;
@@ -13,8 +13,9 @@ describe('resolveShopNavActive', () => {
});
});
it('highlights cart and check-order pages', () => {
it('highlights cart, checkout, and check-order pages', () => {
expect(resolveShopNavActive('/shop/cart')).toEqual({ activeShopNav: 'cart', activeOrderId: null });
expect(resolveShopNavActive('/shop/checkout')).toEqual({ activeShopNav: 'checkout', activeOrderId: null });
expect(resolveShopNavActive('/shop/check-order')).toEqual({
activeShopNav: 'check-order',
activeOrderId: null
@@ -29,6 +30,6 @@ describe('resolveShopNavActive', () => {
});
it('does not highlight nav items on unrelated pages', () => {
expect(resolveShopNavActive('/shop/checkout')).toEqual({ activeShopNav: null, activeOrderId: null });
expect(resolveShopNavActive('/shop/theme')).toEqual({ activeShopNav: null, activeOrderId: null });
});
});
@@ -11,6 +11,10 @@ export const resolveShopNavActive = (path: string): ShopNavActive => {
return { activeShopNav: 'cart', activeOrderId: null };
}
if (first === 'shop' && second === 'checkout') {
return { activeShopNav: 'checkout', activeOrderId: null };
}
if (first === 'shop' && second === 'check-order') {
return { activeShopNav: 'check-order', activeOrderId: null };
}
@@ -17,7 +17,11 @@
<nav class='sf-nav' aria-label='Shop navigation'>
{{> nav-link active=shopNavActive.activeShopNav key='shop' href='/' label='Shop'}}
{{#if hasActiveCheckout}}
{{> nav-link active=shopNavActive.activeShopNav key='checkout' href='/shop/checkout' label='Checkout' count=cartTotalQty}}
{{else}}
{{> nav-link active=shopNavActive.activeShopNav key='cart' href='/shop/cart' label='Cart' count=cartTotalQty}}
{{/if}}
{{> nav-link active=shopNavActive.activeShopNav key='check-order' href='/shop/check-order' label='Check order'}}
{{#each authorizedOrders}}