From f1715f7e7f98a8c77d6ef82b647a18aae7e58d64 Mon Sep 17 00:00:00 2001 From: nobswebdev Date: Fri, 11 Sep 2026 20:22:22 +0200 Subject: [PATCH] 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. --- .../StorefrontShopViewService.spec.ts | 24 ++++++++++++++++++- .../services/StorefrontShopViewService.ts | 6 ++++- .../storefrontCore/types/ShopNavKey.ts | 2 +- .../storefrontCore/types/ShopRenderLocals.ts | 1 + .../StorefrontShopViewServiceTestTypes.ts | 1 + .../utils/resolveShopNavActive.spec.ts | 5 ++-- .../utils/resolveShopNavActive.ts | 4 ++++ .../views/partials/shop-nav.hbs | 6 ++++- 8 files changed, 43 insertions(+), 6 deletions(-) diff --git a/backend/src/modules/storefrontCore/services/StorefrontShopViewService.spec.ts b/backend/src/modules/storefrontCore/services/StorefrontShopViewService.spec.ts index 27a6846..ca6dda6 100644 --- a/backend/src/modules/storefrontCore/services/StorefrontShopViewService.spec.ts +++ b/backend/src/modules/storefrontCore/services/StorefrontShopViewService.spec.ts @@ -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 () => { diff --git a/backend/src/modules/storefrontCore/services/StorefrontShopViewService.ts b/backend/src/modules/storefrontCore/services/StorefrontShopViewService.ts index 08f5d62..f236966 100644 --- a/backend/src/modules/storefrontCore/services/StorefrontShopViewService.ts +++ b/backend/src/modules/storefrontCore/services/StorefrontShopViewService.ts @@ -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 { @@ -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, diff --git a/backend/src/modules/storefrontCore/types/ShopNavKey.ts b/backend/src/modules/storefrontCore/types/ShopNavKey.ts index 8a86d42..ce92918 100644 --- a/backend/src/modules/storefrontCore/types/ShopNavKey.ts +++ b/backend/src/modules/storefrontCore/types/ShopNavKey.ts @@ -1 +1 @@ -export type ShopNavKey = 'shop' | 'cart' | 'check-order'; +export type ShopNavKey = 'shop' | 'cart' | 'checkout' | 'check-order'; diff --git a/backend/src/modules/storefrontCore/types/ShopRenderLocals.ts b/backend/src/modules/storefrontCore/types/ShopRenderLocals.ts index 6b3e1dc..d04e7bb 100644 --- a/backend/src/modules/storefrontCore/types/ShopRenderLocals.ts +++ b/backend/src/modules/storefrontCore/types/ShopRenderLocals.ts @@ -10,6 +10,7 @@ export type ShopRenderLocals = { title: string; shopName: string; cartTotalQty: number; + hasActiveCheckout: boolean; feedback: StorefrontFeedback | undefined; authorizedOrders: AuthorizedOrderNavItem[]; shopNavActive: ShopNavActive; diff --git a/backend/src/modules/storefrontCore/types/StorefrontShopViewServiceTestTypes.ts b/backend/src/modules/storefrontCore/types/StorefrontShopViewServiceTestTypes.ts index 3780bdf..4b68795 100644 --- a/backend/src/modules/storefrontCore/types/StorefrontShopViewServiceTestTypes.ts +++ b/backend/src/modules/storefrontCore/types/StorefrontShopViewServiceTestTypes.ts @@ -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; diff --git a/backend/src/modules/storefrontCore/utils/resolveShopNavActive.spec.ts b/backend/src/modules/storefrontCore/utils/resolveShopNavActive.spec.ts index f496403..7182ed6 100644 --- a/backend/src/modules/storefrontCore/utils/resolveShopNavActive.spec.ts +++ b/backend/src/modules/storefrontCore/utils/resolveShopNavActive.spec.ts @@ -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 }); }); }); diff --git a/backend/src/modules/storefrontCore/utils/resolveShopNavActive.ts b/backend/src/modules/storefrontCore/utils/resolveShopNavActive.ts index bf6bd69..c24859c 100644 --- a/backend/src/modules/storefrontCore/utils/resolveShopNavActive.ts +++ b/backend/src/modules/storefrontCore/utils/resolveShopNavActive.ts @@ -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 }; } diff --git a/backend/src/modules/storefrontCore/views/partials/shop-nav.hbs b/backend/src/modules/storefrontCore/views/partials/shop-nav.hbs index b5cedcc..7e67148 100644 --- a/backend/src/modules/storefrontCore/views/partials/shop-nav.hbs +++ b/backend/src/modules/storefrontCore/views/partials/shop-nav.hbs @@ -17,7 +17,11 @@