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 { StorefrontFeedbackCookieService } from './StorefrontFeedbackCookieService';
import { StorefrontOrderAuthCookieService } from './StorefrontOrderAuthCookieService'; import { StorefrontOrderAuthCookieService } from './StorefrontOrderAuthCookieService';
import { StorefrontThemeCookieService } from './StorefrontThemeCookieService'; import { StorefrontThemeCookieService } from './StorefrontThemeCookieService';
import { StorefrontCheckoutSessionCookieService } from './StorefrontCheckoutSessionCookieService';
import type { StorefrontPageMetaInput } from '../types/StorefrontPageMetaInput'; import type { StorefrontPageMetaInput } from '../types/StorefrontPageMetaInput';
import type { StorefrontShopViewServiceOverrides } from '../types/StorefrontShopViewServiceTestTypes'; import type { StorefrontShopViewServiceOverrides } from '../types/StorefrontShopViewServiceTestTypes';
import { PaymentMethod } from '../../payment/types/PaymentMethod'; import { PaymentMethod } from '../../payment/types/PaymentMethod';
@@ -22,6 +23,7 @@ describe('StorefrontShopViewService', () => {
cart = [], cart = [],
feedback, feedback,
authorizedOrderIds = [], authorizedOrderIds = [],
checkoutSessionId,
theme = 'system', theme = 'system',
branding = { branding = {
logoUrl: null, logoUrl: null,
@@ -62,6 +64,9 @@ describe('StorefrontShopViewService', () => {
const themeCookieService = { const themeCookieService = {
getTheme: jest.fn().mockReturnValue(theme) getTheme: jest.fn().mockReturnValue(theme)
} as unknown as StorefrontThemeCookieService; } as unknown as StorefrontThemeCookieService;
const checkoutSessionCookieService = {
getSessionId: jest.fn().mockReturnValue(checkoutSessionId)
} as unknown as StorefrontCheckoutSessionCookieService;
const service = new StorefrontShopViewService( const service = new StorefrontShopViewService(
exchangeRateService, exchangeRateService,
@@ -70,7 +75,8 @@ describe('StorefrontShopViewService', () => {
cartCookieService, cartCookieService,
feedbackCookieService, feedbackCookieService,
orderAuthCookieService, orderAuthCookieService,
themeCookieService themeCookieService,
checkoutSessionCookieService
); );
const req = { const req = {
@@ -100,6 +106,22 @@ describe('StorefrontShopViewService', () => {
productJsonLd: null productJsonLd: null
}); });
expect(locals.themePreference).toBe('system'); 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 () => { 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 type { StorefrontProductJsonLdInput } from '../types/StorefrontProductJsonLdInput';
import { resolveShopNavActive } from '../utils/resolveShopNavActive'; import { resolveShopNavActive } from '../utils/resolveShopNavActive';
import { StorefrontCartCookieService } from './StorefrontCartCookieService'; import { StorefrontCartCookieService } from './StorefrontCartCookieService';
import { StorefrontCheckoutSessionCookieService } from './StorefrontCheckoutSessionCookieService';
import { StorefrontFeedbackCookieService } from './StorefrontFeedbackCookieService'; import { StorefrontFeedbackCookieService } from './StorefrontFeedbackCookieService';
import { StorefrontOrderAuthCookieService } from './StorefrontOrderAuthCookieService'; import { StorefrontOrderAuthCookieService } from './StorefrontOrderAuthCookieService';
import { StorefrontThemeCookieService } from './StorefrontThemeCookieService'; import { StorefrontThemeCookieService } from './StorefrontThemeCookieService';
@@ -32,7 +33,8 @@ export class StorefrontShopViewService {
private readonly cartCookieService: StorefrontCartCookieService, private readonly cartCookieService: StorefrontCartCookieService,
private readonly feedbackCookieService: StorefrontFeedbackCookieService, private readonly feedbackCookieService: StorefrontFeedbackCookieService,
private readonly orderAuthCookieService: StorefrontOrderAuthCookieService, 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> { 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 themePreference = this.themeCookieService.getTheme(req, res);
const cartTotalQty = getTotalCartQtyFromCart(cart); const cartTotalQty = getTotalCartQtyFromCart(cart);
const hasActiveCheckout = Boolean(this.checkoutSessionCookieService.getSessionId(req, res));
const { shopName, shopFiatCurrency, enabledPaymentMethods } = this.configService.get( const { shopName, shopFiatCurrency, enabledPaymentMethods } = this.configService.get(
'shopSettings' 'shopSettings'
@@ -62,6 +65,7 @@ export class StorefrontShopViewService {
title: page.title, title: page.title,
shopName, shopName,
cartTotalQty, cartTotalQty,
hasActiveCheckout,
feedback, feedback,
authorizedOrders, authorizedOrders,
shopNavActive, 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; title: string;
shopName: string; shopName: string;
cartTotalQty: number; cartTotalQty: number;
hasActiveCheckout: boolean;
feedback: StorefrontFeedback | undefined; feedback: StorefrontFeedback | undefined;
authorizedOrders: AuthorizedOrderNavItem[]; authorizedOrders: AuthorizedOrderNavItem[];
shopNavActive: ShopNavActive; shopNavActive: ShopNavActive;
@@ -6,6 +6,7 @@ export type StorefrontShopViewServiceOverrides = {
cart?: { variantId: string; qty: number }[]; cart?: { variantId: string; qty: number }[];
feedback?: { type: 'success'; text: string }; feedback?: { type: 'success'; text: string };
authorizedOrderIds?: string[]; authorizedOrderIds?: string[];
checkoutSessionId?: string;
theme?: StorefrontThemePreference; theme?: StorefrontThemePreference;
branding?: { branding?: {
logoUrl: string | null; 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/cart')).toEqual({ activeShopNav: 'cart', activeOrderId: null });
expect(resolveShopNavActive('/shop/checkout')).toEqual({ activeShopNav: 'checkout', activeOrderId: null });
expect(resolveShopNavActive('/shop/check-order')).toEqual({ expect(resolveShopNavActive('/shop/check-order')).toEqual({
activeShopNav: 'check-order', activeShopNav: 'check-order',
activeOrderId: null activeOrderId: null
@@ -29,6 +30,6 @@ describe('resolveShopNavActive', () => {
}); });
it('does not highlight nav items on unrelated pages', () => { 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 }; return { activeShopNav: 'cart', activeOrderId: null };
} }
if (first === 'shop' && second === 'checkout') {
return { activeShopNav: 'checkout', activeOrderId: null };
}
if (first === 'shop' && second === 'check-order') { if (first === 'shop' && second === 'check-order') {
return { activeShopNav: 'check-order', activeOrderId: null }; return { activeShopNav: 'check-order', activeOrderId: null };
} }
@@ -17,7 +17,11 @@
<nav class='sf-nav' aria-label='Shop navigation'> <nav class='sf-nav' aria-label='Shop navigation'>
{{> nav-link active=shopNavActive.activeShopNav key='shop' href='/' label='Shop'}} {{> 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}} {{> 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'}} {{> nav-link active=shopNavActive.activeShopNav key='check-order' href='/shop/check-order' label='Check order'}}
{{#each authorizedOrders}} {{#each authorizedOrders}}