state/config/config.state.tstypescript
/**
* Config Store - Zustand store for SDK configuration management
*/
import { create } from 'zustand';
import { createLogger } from '@/core/logger';
import type {
ConfigState,
PageType,
PaymentConfig,
GoogleMapsConfig,
AddressConfig,
DiscountDefinition,
} from '@/types/global';
const logger = createLogger('ConfigStore');
/**
* The canonical form of `tag` if it is a usable BCP 47 locale, otherwise `null`.
*
* Guards the one config value that would otherwise throw on *every price on the page*:
* `new Intl.NumberFormat('de_DE')` raises a `RangeError`, and an underscore instead of a
* hyphen is the obvious way to mistype a locale. Rejecting it here means a bad tag costs a
* warning and the browser's own locale, not a blank storefront.
*/
function canonicalLocale(tag: unknown): string | null {
if (typeof tag !== 'string' || tag.trim() === '') return null;
try {
return Intl.getCanonicalLocales(tag.trim())[0] ?? null;
} catch {
return null;
}
}
interface ConfigActions {
loadFromMeta: () => void;
loadFromWindow: () => void;
updateConfig: (config: Partial<ConfigState>) => void;
setSpreedlyEnvironmentKey: (key: string) => void;
reset: () => void;
getCurrency: () => string;
}
const initialState: ConfigState = {
apiKey: '',
campaignId: '',
debug: false,
debugger: false,
pageType: 'product',
// spreedlyEnvironmentKey: undefined, - omitted to avoid exactOptionalPropertyTypes issue
paymentConfig: {},
googleMapsConfig: {},
addressConfig: {},
// Additional configuration with enterprise defaults
autoInit: true,
rateLimit: 4,
cacheTtl: 300,
retryAttempts: 3,
timeout: 10000,
testMode: false,
// API and performance settings
maxRetries: 3,
requestTimeout: 30000,
enableAnalytics: true,
enableDebugMode: false,
// Environment and deployment settings
environment: 'production',
// version: undefined, - omitted
// buildTimestamp: undefined, - omitted
// Discount system
discounts: {},
// Attribution
// utmTransfer: undefined, - omitted
// Tracking configuration
tracking: 'auto', // 'auto', 'manual', 'disabled'
// Location and currency detection
detectedCountry: '',
detectedCurrency: '',
detectedIp: '', // User's IP address from location detection
selectedCurrency: '',
locationData: null as any, // Cache the entire location response
currencyBehavior: 'auto' as 'auto' | 'manual', // Default to auto-switch currency on country change
currencyFallbackOccurred: false, // Track if currency fallback happened
// locale: undefined, - omitted deliberately: unset means "follow the browser", which is
// not the same as pinning 'en-US'. See CurrencyFormatter.getUserLocale().
clearCartOnInit: false,
// Error monitoring removed - add externally via HTML/scripts
};
export const configStore = create<ConfigState & ConfigActions>((set, get) => ({
...(initialState as ConfigState),
loadFromMeta: () => {
if (typeof document === 'undefined') return;
const updates: Partial<ConfigState> = {};
// Load API key
const apiKeyMeta = document.querySelector('meta[name="next-api-key"]');
if (apiKeyMeta) {
updates.apiKey = apiKeyMeta.getAttribute('content') ?? '';
}
// Load campaign ID
const campaignIdMeta = document.querySelector(
'meta[name="next-campaign-id"]'
);
if (campaignIdMeta) {
updates.campaignId = campaignIdMeta.getAttribute('content') ?? '';
}
// Load debug flag
const debugMeta = document.querySelector('meta[name="next-debug"]');
if (debugMeta) {
updates.debug = debugMeta.getAttribute('content') === 'true';
}
// Load clear cart on init flag
const clearCartMeta = document.querySelector(
'meta[name="next-clear-cart"]'
);
if (clearCartMeta) {
updates.clearCartOnInit =
clearCartMeta.getAttribute('content') === 'true';
}
// Load page type
const pageTypeMeta = document.querySelector('meta[name="next-page-type"]');
if (pageTypeMeta) {
updates.pageType = pageTypeMeta.getAttribute('content') as PageType;
}
// Load Spreedly environment key (fallback - campaign data takes precedence)
const spreedlyKeyMeta =
document.querySelector('meta[name="next-spreedly-key"]') ||
document.querySelector('meta[name="next-payment-env-key"]');
if (spreedlyKeyMeta) {
const spreedlyKey = spreedlyKeyMeta.getAttribute('content');
if (spreedlyKey) {
updates.spreedlyEnvironmentKey = spreedlyKey;
}
}
if (Object.keys(updates).length > 0) {
set(updates);
}
},
loadFromWindow: () => {
if (typeof window === 'undefined') return;
const windowConfig = (window as any).nextConfig;
if (!windowConfig || typeof windowConfig !== 'object') return;
const updates: Partial<ConfigState> = {};
if (typeof windowConfig.apiKey === 'string') {
updates.apiKey = windowConfig.apiKey;
}
if (typeof windowConfig.campaignId === 'string') {
updates.campaignId = windowConfig.campaignId;
}
if (typeof windowConfig.debug === 'boolean') {
updates.debug = windowConfig.debug;
}
if (typeof windowConfig.debugger === 'boolean') {
updates.debugger = windowConfig.debugger;
}
if (typeof windowConfig.storeName === 'string') {
updates.storeName = windowConfig.storeName;
}
if (typeof windowConfig.pageType === 'string') {
updates.pageType = windowConfig.pageType as PageType;
}
if (typeof windowConfig.spreedlyEnvironmentKey === 'string') {
updates.spreedlyEnvironmentKey = windowConfig.spreedlyEnvironmentKey;
}
if (windowConfig.payment && typeof windowConfig.payment === 'object') {
updates.paymentConfig = windowConfig.payment as PaymentConfig;
}
// Support both payment and paymentConfig for backwards compatibility
if (
windowConfig.paymentConfig &&
typeof windowConfig.paymentConfig === 'object'
) {
updates.paymentConfig = windowConfig.paymentConfig as PaymentConfig;
}
// Load card input configuration from window.nextConfig
// Supports multiple naming conventions for flexibility and backward compatibility:
// - cardInputConfig (preferred, generic naming)
// - spreedly (legacy naming, still supported)
// - spreedlyConfig (legacy naming, still supported)
// Priority: cardInputConfig > spreedly > spreedlyConfig
if (
windowConfig.cardInputConfig &&
typeof windowConfig.cardInputConfig === 'object'
) {
if (!updates.paymentConfig) {
updates.paymentConfig = {};
}
updates.paymentConfig.cardInputConfig = windowConfig.cardInputConfig;
} else if (
windowConfig.spreedly &&
typeof windowConfig.spreedly === 'object'
) {
if (!updates.paymentConfig) {
updates.paymentConfig = {};
}
updates.paymentConfig.cardInputConfig = windowConfig.spreedly;
} else if (
windowConfig.spreedlyConfig &&
typeof windowConfig.spreedlyConfig === 'object'
) {
if (!updates.paymentConfig) {
updates.paymentConfig = {};
}
updates.paymentConfig.cardInputConfig = windowConfig.spreedlyConfig;
}
if (
windowConfig.googleMaps &&
typeof windowConfig.googleMaps === 'object'
) {
updates.googleMapsConfig = windowConfig.googleMaps as GoogleMapsConfig;
}
// Load address config from window config
if (
windowConfig.addressConfig &&
typeof windowConfig.addressConfig === 'object'
) {
updates.addressConfig = windowConfig.addressConfig as AddressConfig;
}
// Load currency behavior from window config
if (
windowConfig.currencyBehavior &&
(windowConfig.currencyBehavior === 'auto' ||
windowConfig.currencyBehavior === 'manual')
) {
updates.currencyBehavior = windowConfig.currencyBehavior;
}
// Load the price-formatting locale. Left unset on a bad tag so the browser locale
// still applies — see canonicalLocale().
if (windowConfig.locale !== undefined) {
const locale = canonicalLocale(windowConfig.locale);
if (locale) {
updates.locale = locale;
} else {
logger.warn(
'[Config] Ignoring invalid locale, using the browser locale instead:',
windowConfig.locale
);
}
}
// Load discount definitions from window config
if (windowConfig.discounts && typeof windowConfig.discounts === 'object') {
updates.discounts = windowConfig.discounts as Record<
string,
DiscountDefinition
>;
}
// Load tracking mode
if (typeof windowConfig.tracking === 'string') {
updates.tracking = windowConfig.tracking as
| 'auto'
| 'manual'
| 'disabled';
}
// Load analytics configuration
if (windowConfig.analytics && typeof windowConfig.analytics === 'object') {
updates.analytics = windowConfig.analytics;
}
// Monitoring configuration removed - add error tracking externally via HTML/scripts
// Load UTM transfer configuration
if (
windowConfig.utmTransfer &&
typeof windowConfig.utmTransfer === 'object'
) {
updates.utmTransfer = windowConfig.utmTransfer;
}
if (Object.keys(updates).length > 0) {
set(updates);
}
},
updateConfig: (config: Partial<ConfigState>) => {
set(state => ({ ...state, ...config }));
},
setSpreedlyEnvironmentKey: (key: string) => {
set({ spreedlyEnvironmentKey: key });
},
reset: () => {
set(initialState);
},
getCurrency: () => {
const state = get();
return state.selectedCurrency || state.detectedCurrency || 'USD';
},
}));
/**
* The config store — the SDK's resolved {@link ConfigState} (API credentials,
* page type, payment/address setup, detected location and currency). Mostly
* read-only at runtime; set from the loader script at init.
*
* @example
* ```ts
* const currency = useConfigStore.getState().getCurrency();
* ```
*
* @category Core
*/
export const useConfigStore = configStore;