From 70ee95a8d7070dde8a76ed9b5dda156cb9610901 Mon Sep 17 00:00:00 2001 From: bc-arezoo Date: Fri, 19 Jun 2026 16:32:48 +1000 Subject: [PATCH] refactor(login): B2B-4930 Extract currentCustomerJWT fetch into a shared helper Centralise the customer JWT fetch/persist logic that was duplicated across loginInfo and (incoming in #927) the Login page. Adds a single validity chokepoint (isCustomerJwtValid) so callers don't repeat the check. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/utils/currentCustomerJwt.test.ts | 36 +++++++++++++++++++ .../src/utils/currentCustomerJwt.ts | 20 +++++++++++ apps/storefront/src/utils/loginInfo.ts | 14 +++----- 3 files changed, 61 insertions(+), 9 deletions(-) create mode 100644 apps/storefront/src/utils/currentCustomerJwt.test.ts create mode 100644 apps/storefront/src/utils/currentCustomerJwt.ts diff --git a/apps/storefront/src/utils/currentCustomerJwt.test.ts b/apps/storefront/src/utils/currentCustomerJwt.test.ts new file mode 100644 index 000000000..d72c59b95 --- /dev/null +++ b/apps/storefront/src/utils/currentCustomerJwt.test.ts @@ -0,0 +1,36 @@ +import * as bcService from '@/shared/service/bc'; + +import b2bLogger from './b3Logger'; +import { fetchCurrentCustomerJwt, isCustomerJwtValid } from './currentCustomerJwt'; + +describe('currentCustomerJwt helpers', () => { + beforeEach(() => { + vi.spyOn(b2bLogger, 'error').mockImplementation(() => undefined); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('fetchCurrentCustomerJwt', () => { + it('returns the fetched JWT', async () => { + vi.spyOn(bcService, 'getCurrentCustomerJWT').mockResolvedValue('jwt-token'); + + await expect(fetchCurrentCustomerJwt()).resolves.toBe('jwt-token'); + }); + + it('returns an empty string and logs when the fetch fails', async () => { + vi.spyOn(bcService, 'getCurrentCustomerJWT').mockRejectedValue(new Error('network')); + + await expect(fetchCurrentCustomerJwt()).resolves.toBe(''); + expect(b2bLogger.error).toHaveBeenCalled(); + }); + }); + + describe('isCustomerJwtValid', () => { + it('treats a present token as valid and an empty token as invalid', () => { + expect(isCustomerJwtValid('jwt-token')).toBe(true); + expect(isCustomerJwtValid('')).toBe(false); + }); + }); +}); diff --git a/apps/storefront/src/utils/currentCustomerJwt.ts b/apps/storefront/src/utils/currentCustomerJwt.ts new file mode 100644 index 000000000..fd1d0168a --- /dev/null +++ b/apps/storefront/src/utils/currentCustomerJwt.ts @@ -0,0 +1,20 @@ +import { getCurrentCustomerJWT } from '@/shared/service/bc'; +import { getAppClientId } from '@/shared/service/request/base'; + +import b2bLogger from './b3Logger'; + +// Fetch the BC customer JWT from the storefront. Returns '' on failure (logged). +// Store-free on purpose: callers persist with the dispatch available in their own +// context (useAppDispatch in components, the store singleton in non-React utils). +export const fetchCurrentCustomerJwt = async (): Promise => { + const jwt = await getCurrentCustomerJWT(getAppClientId()).catch((error) => { + b2bLogger.error(error); + return ''; + }); + + return jwt ?? ''; +}; + +// Single chokepoint for JWT validity so callers don't repeat the check. +// TODO(B2B-4930): decode and verify the token (e.g. exp) instead of a presence check. +export const isCustomerJwtValid = (jwt: string): boolean => Boolean(jwt); diff --git a/apps/storefront/src/utils/loginInfo.ts b/apps/storefront/src/utils/loginInfo.ts index de6d480d6..4479dfd7e 100644 --- a/apps/storefront/src/utils/loginInfo.ts +++ b/apps/storefront/src/utils/loginInfo.ts @@ -9,8 +9,7 @@ import { getUserCompany, getUserMasqueradingCompany, } from '@/shared/service/b2b'; -import { getCurrentCustomerJWT, getCustomerInfo } from '@/shared/service/bc'; -import { getAppClientId } from '@/shared/service/request/base'; +import { getCustomerInfo } from '@/shared/service/bc'; import { clearMasqueradeCompany, MasqueradeCompany, @@ -36,6 +35,7 @@ import { CompanyStatus, CustomerRole, CustomerRoleName, LoginTypes, UserTypes } import b2bLogger from './b3Logger'; import { B3LStorage, B3SStorage } from './b3Storage'; import { channelId, storeHash } from './basicConfig'; +import { fetchCurrentCustomerJwt } from './currentCustomerJwt'; import { getAccountHierarchyIsEnabled } from './storefrontConfig'; const getLoginTokenInfo = () => { @@ -187,11 +187,7 @@ const getCompanyUserInfo = async (useBcLoginAndAuthorisation = false) => { const loginWithCurrentCustomerJWT = async () => { const prevCurrentCustomerJWT = store.getState().company.tokens.currentCustomerJWT; - const currentCustomerJWT = await getCurrentCustomerJWT(getAppClientId()).catch((error) => { - // eslint-disable-next-line no-console - console.error(error); - return undefined; - }); + const currentCustomerJWT = await fetchCurrentCustomerJwt(); if (!currentCustomerJWT || prevCurrentCustomerJWT === currentCustomerJWT) return undefined; @@ -346,8 +342,8 @@ export const getCurrentCustomerInfo = async ( if (useBcLoginAndAuthorisation) { let { currentCustomerJWT } = store.getState().company.tokens; if (!currentCustomerJWT) { - currentCustomerJWT = - (await getCurrentCustomerJWT(getAppClientId()).catch(() => '')) ?? ''; + currentCustomerJWT = await fetchCurrentCustomerJwt(); + if (currentCustomerJWT) store.dispatch(setCurrentCustomerJWT(currentCustomerJWT)); } if (currentCustomerJWT) { const authorizationData = await b2bAuthorization({