diff --git a/backend/src/consts/storefrontThemePreferences.ts b/backend/src/consts/storefrontThemePreferences.ts new file mode 100644 index 0000000..cd08960 --- /dev/null +++ b/backend/src/consts/storefrontThemePreferences.ts @@ -0,0 +1 @@ +export const STOREFRONT_THEME_PREFERENCES = ['light', 'dark', 'system'] as const; diff --git a/backend/src/modules/storefrontCore/dto/SetThemePreferenceDto.ts b/backend/src/modules/storefrontCore/dto/SetThemePreferenceDto.ts index f9c3b72..a78a629 100644 --- a/backend/src/modules/storefrontCore/dto/SetThemePreferenceDto.ts +++ b/backend/src/modules/storefrontCore/dto/SetThemePreferenceDto.ts @@ -1,8 +1,9 @@ import { IsIn, IsNotEmpty } from 'class-validator'; +import { STOREFRONT_THEME_PREFERENCES } from '../../../consts/storefrontThemePreferences'; import type { StorefrontThemePreference } from '../types/StorefrontThemePreference'; export class SetThemePreferenceDto { @IsNotEmpty() - @IsIn(['light', 'dark']) + @IsIn(STOREFRONT_THEME_PREFERENCES) theme: StorefrontThemePreference; } diff --git a/backend/src/modules/storefrontCore/services/StorefrontShopViewService.spec.ts b/backend/src/modules/storefrontCore/services/StorefrontShopViewService.spec.ts index 52ecc67..27a6846 100644 --- a/backend/src/modules/storefrontCore/services/StorefrontShopViewService.spec.ts +++ b/backend/src/modules/storefrontCore/services/StorefrontShopViewService.spec.ts @@ -8,39 +8,21 @@ import { StorefrontFeedbackCookieService } from './StorefrontFeedbackCookieServi import { StorefrontOrderAuthCookieService } from './StorefrontOrderAuthCookieService'; import { StorefrontThemeCookieService } from './StorefrontThemeCookieService'; import type { StorefrontPageMetaInput } from '../types/StorefrontPageMetaInput'; +import type { StorefrontShopViewServiceOverrides } from '../types/StorefrontShopViewServiceTestTypes'; import { PaymentMethod } from '../../payment/types/PaymentMethod'; describe('StorefrontShopViewService', () => { - type ServiceOverrides = { - cart?: { variantId: string; qty: number }[]; - feedback?: { type: 'success'; text: string }; - authorizedOrderIds?: string[]; - theme?: 'light' | 'dark'; - branding?: { - logoUrl: string | null; - faviconUrl: string | null; - simplexLink: string | null; - shippingNote: string | null; - }; - shopSettings?: { shopName: string; shopFiatCurrency: string; enabledPaymentMethods?: PaymentMethod[] }; - fiatPerXmr?: number; - fiatPerBtc?: number | null; - req?: Partial> & { - host?: string; - }; - }; - const defaultPage: StorefrontPageMetaInput = { title: 'Cart', metaDescription: 'Review your cart.' }; - const createService = (overrides: ServiceOverrides = {}) => { + const createService = (overrides: StorefrontShopViewServiceOverrides = {}) => { const { cart = [], feedback, authorizedOrderIds = [], - theme, + theme = 'system', branding = { logoUrl: null, faviconUrl: null, @@ -117,6 +99,7 @@ describe('StorefrontShopViewService', () => { ogImageUrl: null, productJsonLd: null }); + expect(locals.themePreference).toBe('system'); }); it('exposes shop settings, cart qty, feedback, branding, theme, and request context', async () => { diff --git a/backend/src/modules/storefrontCore/services/StorefrontThemeCookieService.spec.ts b/backend/src/modules/storefrontCore/services/StorefrontThemeCookieService.spec.ts index c96f26d..305fa58 100644 --- a/backend/src/modules/storefrontCore/services/StorefrontThemeCookieService.spec.ts +++ b/backend/src/modules/storefrontCore/services/StorefrontThemeCookieService.spec.ts @@ -14,10 +14,16 @@ describe('StorefrontThemeCookieService', () => { service = new StorefrontThemeCookieService(signedCookies as unknown as StorefrontSignedCookieService); }); - it('returns undefined for invalid theme values', () => { - signedCookies.getSignedCookie.mockReturnValue({ theme: 'system' }); + it('returns system when no cookie is set', () => { + signedCookies.getSignedCookie.mockReturnValue(undefined); - expect(service.getTheme({} as never, {} as never)).toBeUndefined(); + expect(service.getTheme({} as never, {} as never)).toBe('system'); + }); + + it('returns system for invalid theme values', () => { + signedCookies.getSignedCookie.mockReturnValue({ theme: 'sepia' }); + + expect(service.getTheme({} as never, {} as never)).toBe('system'); }); it('returns a supported theme preference', () => { @@ -25,4 +31,10 @@ describe('StorefrontThemeCookieService', () => { expect(service.getTheme({} as never, {} as never)).toBe('dark'); }); + + it('returns system when stored in the cookie', () => { + signedCookies.getSignedCookie.mockReturnValue({ theme: 'system' }); + + expect(service.getTheme({} as never, {} as never)).toBe('system'); + }); }); diff --git a/backend/src/modules/storefrontCore/services/StorefrontThemeCookieService.ts b/backend/src/modules/storefrontCore/services/StorefrontThemeCookieService.ts index f2de8f6..6708dae 100644 --- a/backend/src/modules/storefrontCore/services/StorefrontThemeCookieService.ts +++ b/backend/src/modules/storefrontCore/services/StorefrontThemeCookieService.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; import type { Request, Response } from 'express'; +import { STOREFRONT_THEME_PREFERENCES } from '../../../consts/storefrontThemePreferences'; import type { StorefrontThemePreference } from '../types/StorefrontThemePreference'; import { StorefrontSignedCookieService } from './StorefrontSignedCookieService'; @@ -7,15 +8,15 @@ import { StorefrontSignedCookieService } from './StorefrontSignedCookieService'; export class StorefrontThemeCookieService { constructor(private readonly signedCookies: StorefrontSignedCookieService) {} - getTheme(req: Request, res: Response): StorefrontThemePreference | undefined { - const payload = this.signedCookies.getSignedCookie<{ theme: string }>(req, res, 'theme'); + getTheme(req: Request, res: Response): StorefrontThemePreference { + const payload = this.signedCookies.getSignedCookie<{ theme: StorefrontThemePreference }>(req, res, 'theme'); const theme = payload?.theme; - if (theme === 'light' || theme === 'dark') { + if (theme && STOREFRONT_THEME_PREFERENCES.includes(theme)) { return theme; } - return undefined; + return 'system'; } setTheme(req: Request, res: Response, theme: StorefrontThemePreference): void { diff --git a/backend/src/modules/storefrontCore/types/ShopRenderLocals.ts b/backend/src/modules/storefrontCore/types/ShopRenderLocals.ts index 49742f9..6b3e1dc 100644 --- a/backend/src/modules/storefrontCore/types/ShopRenderLocals.ts +++ b/backend/src/modules/storefrontCore/types/ShopRenderLocals.ts @@ -19,6 +19,6 @@ export type ShopRenderLocals = { faviconUrl: string | null; simplexLink: string | null; shippingNote: string | null; - themePreference: StorefrontThemePreference | undefined; + themePreference: StorefrontThemePreference; pageMeta: StorefrontPageMeta; }; diff --git a/backend/src/modules/storefrontCore/types/StorefrontShopViewServiceTestTypes.ts b/backend/src/modules/storefrontCore/types/StorefrontShopViewServiceTestTypes.ts new file mode 100644 index 0000000..3780bdf --- /dev/null +++ b/backend/src/modules/storefrontCore/types/StorefrontShopViewServiceTestTypes.ts @@ -0,0 +1,22 @@ +import type { Request } from 'express'; +import type { PaymentMethod } from '../../payment/types/PaymentMethod'; +import type { StorefrontThemePreference } from './StorefrontThemePreference'; + +export type StorefrontShopViewServiceOverrides = { + cart?: { variantId: string; qty: number }[]; + feedback?: { type: 'success'; text: string }; + authorizedOrderIds?: string[]; + theme?: StorefrontThemePreference; + branding?: { + logoUrl: string | null; + faviconUrl: string | null; + simplexLink: string | null; + shippingNote: string | null; + }; + shopSettings?: { shopName: string; shopFiatCurrency: string; enabledPaymentMethods?: PaymentMethod[] }; + fiatPerXmr?: number; + fiatPerBtc?: number | null; + req?: Partial> & { + host?: string; + }; +}; diff --git a/backend/src/modules/storefrontCore/types/StorefrontThemePreference.ts b/backend/src/modules/storefrontCore/types/StorefrontThemePreference.ts index 06b14c2..557d744 100644 --- a/backend/src/modules/storefrontCore/types/StorefrontThemePreference.ts +++ b/backend/src/modules/storefrontCore/types/StorefrontThemePreference.ts @@ -1 +1,3 @@ -export type StorefrontThemePreference = 'light' | 'dark'; +import { STOREFRONT_THEME_PREFERENCES } from '../../../consts/storefrontThemePreferences'; + +export type StorefrontThemePreference = (typeof STOREFRONT_THEME_PREFERENCES)[number]; diff --git a/backend/src/modules/storefrontCore/utils/registerStorefrontPartials.ts b/backend/src/modules/storefrontCore/utils/registerStorefrontPartials.ts index 2c40008..78d1fbf 100644 --- a/backend/src/modules/storefrontCore/utils/registerStorefrontPartials.ts +++ b/backend/src/modules/storefrontCore/utils/registerStorefrontPartials.ts @@ -29,6 +29,7 @@ const STOREFRONT_PARTIALS = [ { name: 'shop-footer', file: 'shop-footer.hbs' }, { name: 'storefront-image', file: 'storefront-image.hbs' }, { name: 'theme-switcher', file: 'theme-switcher.hbs' }, + { name: 'html-theme-attributes', file: 'html-theme-attributes.hbs' }, { name: 'page-back-link', file: 'page-back-link.hbs' }, { name: 'product-card', file: 'product-card.hbs' }, { name: 'order-data-retention-notice', file: 'order-data-retention-notice.hbs' }, diff --git a/backend/src/modules/storefrontCore/views/layouts/shop-minimal.hbs b/backend/src/modules/storefrontCore/views/layouts/shop-minimal.hbs index 5bd9662..f44f3ec 100644 --- a/backend/src/modules/storefrontCore/views/layouts/shop-minimal.hbs +++ b/backend/src/modules/storefrontCore/views/layouts/shop-minimal.hbs @@ -1,8 +1,7 @@ html-theme-attributes}} > {{> shop-head}} diff --git a/backend/src/modules/storefrontCore/views/layouts/shop.hbs b/backend/src/modules/storefrontCore/views/layouts/shop.hbs index 0bbd707..fd20b11 100644 --- a/backend/src/modules/storefrontCore/views/layouts/shop.hbs +++ b/backend/src/modules/storefrontCore/views/layouts/shop.hbs @@ -1,8 +1,7 @@ html-theme-attributes}} > {{> shop-head}} diff --git a/backend/src/modules/storefrontCore/views/partials/html-theme-attributes.hbs b/backend/src/modules/storefrontCore/views/partials/html-theme-attributes.hbs new file mode 100644 index 0000000..3c92932 --- /dev/null +++ b/backend/src/modules/storefrontCore/views/partials/html-theme-attributes.hbs @@ -0,0 +1,2 @@ +{{#ifeq themePreference 'light'}}data-theme='light'{{/ifeq}} +{{#ifeq themePreference 'dark'}}data-theme='dark'{{/ifeq}} diff --git a/backend/src/modules/storefrontCore/views/partials/theme-switcher.hbs b/backend/src/modules/storefrontCore/views/partials/theme-switcher.hbs index fde9ba2..7139940 100644 --- a/backend/src/modules/storefrontCore/views/partials/theme-switcher.hbs +++ b/backend/src/modules/storefrontCore/views/partials/theme-switcher.hbs @@ -15,5 +15,12 @@ class='sf-btn sf-btn--sm{{#ifeq themePreference 'dark'}} sf-btn--primary{{/ifeq}}' {{#ifeq themePreference 'dark'}}aria-current='true'{{/ifeq}} >Dark +