} - a promise to the i18next t function\n */\nexport const bootLanguages = (\n translations = window.touchwebTranslations,\n attributeTranslations = window.attributeTranslations\n) => {\n const translationResources = Object.entries(translations).map(\n ([key, value]) => ({\n key,\n value,\n namespace: 'touchWeb'\n })\n );\n const attributeResources = Object.entries(\n attributeTranslations\n ).map(([key, value]) => ({ key, value, namespace: 'attributes' }));\n\n const resources = translationResources\n .concat(attributeResources)\n .reduce((prev, { key, value, namespace }) => {\n const s = prev[key] || {};\n s[namespace] = value;\n return {\n ...prev,\n [key]: s\n };\n }, {});\n\n i18nInitialized = i18n\n .use(initReactI18next)\n .use(LanguageDetector)\n .init(initOptions(resources), err => {\n if (err) {\n logger('error when loading translations', err);\n }\n });\n\n return i18nInitialized;\n};\n\nexport const initOptions = resources => ({\n detection: {\n order: ['htmlTag'],\n htmlTag: document.html\n },\n defaultNS: [NS_TOUCHWEB],\n ns: [NS_ATTRIBUTES],\n fallbackLng: DEFAULT_LANGUAGE,\n resources,\n debug: false,\n keySeparator: false,\n nsSeparator: ':::',\n interpolation: {\n escape: str => str.replace(/{{/g, '').replace(/}}/g, ''),\n format: (value, format) => {\n if (format === 'formatNumber') {\n return formatNumberWithSeparators(value);\n }\n\n return value;\n }\n },\n react: {\n useSuspense: false // loading from file currently breaks if this is true\n },\n ...missingKeyLoggingConfig\n});\n\nconst missingKeyLoggingConfig = {\n saveMissing: true,\n saveMissingTo: 'current',\n missingKeyHandler: (language, namespace, key) => {\n logger(\n `TranslationMissing (javascript) - Unable to find key ${key} (namespace ${namespace}) in language ${language}!`\n );\n },\n missingInterpolationHandler: (text, value) => {\n logger(\n `InterpolationFailure (javascript) - Unable to interpolate the text \"${text}\" fully. The following placeholder values were missing: ${JSON.stringify(\n value\n )}`\n );\n return undefined;\n }\n};\n\n/**\n * This function helps you make sure that i18next init has completed before continuing\n */\nexport const whenInitialized = () => {\n if (!i18nInitialized) {\n throw new Error(\n \"Can't call boot-languages whenInitialized before init!\"\n );\n }\n return i18nInitialized.then(t => ({\n t,\n toLocalizedUrl: url => toLocalizedUrl(url, i18n.language)\n }));\n};\n\n/**\n * Only required for Storybook, do not use.\n */\nexport const changeLanguage = language => i18n.changeLanguage(language);\n\nexport const getCurrentLanguage = () => i18n.language;\n","import * as actions from './actions';\nimport reducer from './reducer';\nimport {\n selectIsLoggedIn,\n selectGeolocation,\n selectIsOutsideSweden\n} from './selectors';\n\nexport {\n actions,\n reducer,\n selectIsLoggedIn,\n selectGeolocation,\n selectIsOutsideSweden\n};\n","import axios from 'axios';\nimport { handleError } from 'tradera-utils/api';\n\nexport const saveLanguageSetting = (code, toLocalizedUrl) =>\n axios\n .post(toLocalizedUrl('/my/profile/save-language-setting'), {\n languageCodeIso2: code\n })\n .catch(handleError());\n\n// Export object for Sinon stubbing\nexport default {\n saveLanguageSetting\n};\n","import Analytics from 'packages/analytics';\nimport logger from 'packages/logger';\nimport locationUtils from 'tradera-utils/location';\nimport api from './api';\nimport { selectIsLoggedIn } from 'tradera-state/member';\nimport { setPreferred } from './reducer';\n\n/**\n * Sets new preferred language\n * @param {string} code - example: 'sv' or 'en'\n */\nexport const setPreferredLanguage = (code, toLocalizedUrl) => async (\n dispatch,\n getState\n) => {\n Analytics.trackEvent({\n category: 'Settings',\n action: 'Language',\n label: code\n });\n\n try {\n if (selectIsLoggedIn(getState())) {\n await api.saveLanguageSetting(code, toLocalizedUrl);\n }\n dispatch(setPreferred(code));\n locationUtils.reloadLocalized(code);\n } catch (error) {\n logger(error);\n }\n};\n","export const capitalize = text => `${text[0].toUpperCase()}${text.slice(1)}`;\n\nexport const removeTags = text => text.replace(/(<([^>]+)>)/gi, '');\n","import plainDayjs from 'dayjs';\nimport isBetween from 'dayjs/plugin/isBetween';\nimport isSameOrAfter from 'dayjs/plugin/isSameOrAfter';\nimport utc from 'dayjs/plugin/utc';\nimport timezone from 'dayjs/plugin/timezone';\nimport localizedFormat from 'dayjs/plugin/localizedFormat';\nimport 'dayjs/locale/sv';\nimport 'dayjs/locale/da';\nimport 'dayjs/locale/de';\n\nplainDayjs.extend(isBetween);\nplainDayjs.extend(isSameOrAfter);\nplainDayjs.extend(utc);\nplainDayjs.extend(timezone);\nplainDayjs.extend(localizedFormat);\n\nexport const dayjs = plainDayjs;\n\nexport const localizedDayjs = locale => (...args) =>\n dayjs(...args).locale(locale);\n\nexport const swedishTimeZoneDate = (date, localizedDayjs) =>\n localizedDayjs(date).tz('Europe/Stockholm');\n\nexport const formatUtcDateAsSwedishTimeZoneDate = (date, lozalizedDayjs) =>\n swedishTimeZoneDate(date, lozalizedDayjs).format('D MMM HH:mm');\n\n/**\n * Format a number of seconds to an object of time units\n *\n * @param {number} seconds\n * @param {object} [options]\n * @param {string} [options.startUnit] - Time part to start counting from, empty for all parts\n * @param {string} [options.endUnit] - Time part to stop counting on, empty for all parts\n * @returns {{string: integer}}\n */\nexport const getTimeSpanParts = (seconds, options = {}) => {\n const optionsWithDefaults = {\n startUnit: '',\n endUnit: '',\n ...options\n };\n const parts = {};\n let remaining = seconds;\n let started = !optionsWithDefaults.startUnit;\n for (const [part, length] of Object.entries(TIME_SPAN_UNIT_LENGTHS)) {\n if (!started && part === optionsWithDefaults.startUnit) started = true;\n if (started) {\n if (remaining >= length) {\n parts[part] = Math.floor(remaining / length);\n }\n remaining %= length;\n if (remaining === 0 || optionsWithDefaults.endUnit === part) break;\n }\n }\n return parts;\n};\n\nexport const TIME_SPAN_UNITS = {\n DAYS: 'days',\n HOURS: 'hours',\n MINUTES: 'minutes',\n SECONDS: 'seconds'\n};\n\nconst TIME_SPAN_UNIT_LENGTHS = {\n [TIME_SPAN_UNITS.DAYS]: 24 * 60 * 60,\n [TIME_SPAN_UNITS.HOURS]: 60 * 60,\n [TIME_SPAN_UNITS.MINUTES]: 60,\n [TIME_SPAN_UNITS.SECONDS]: 1\n};\n","import cookie from 'cookie';\n\nexport const getCookieFromString = (cookieString, key) =>\n cookie.parse(cookieString)[key] || null;\n\n/**\n * Helper function for cookie and GDPR utilities do NOT use this directly. Use the cookie\n * utility instead as that is GDPR compliant.\n * @param {*} key\n * @returns\n */\nexport const getCookieFromBrowser = key =>\n getCookieFromString(window.document.cookie, key);\n\n/**\n * Helper function for cookie and GDPR utilities do NOT use this directly. Use the cookie\n * utility instead as that is GDPR compliant.\n * @param {*} key\n * @returns\n */\nexport const setCookieToBrowser = s => {\n window.document.cookie = s;\n};\n","// extracted by mini-css-extract-plugin\nexport default {\"item-toast\":\"item-toast--QaLFk\",\"itemToast\":\"item-toast--QaLFk\",\"item-toast-image\":\"item-toast-image--i_l4w\",\"itemToastImage\":\"item-toast-image--i_l4w\"};","import React from 'react';\nimport { formatNumberWithSeparators } from 'tradera-utils/format';\nimport ALink from 'tradera-components/alink/alink';\nimport { useTranslator, useUrlLocalizer } from 'tradera-lang/translate';\nimport styles from './item-toast.module.scss';\n\nexport const ItemToast = ({\n imageUrl,\n itemUrl,\n shortDescription,\n type,\n eventData\n}) => {\n const { toLocalizedUrl } = useUrlLocalizer();\n\n return (\n \n \n

\n
\n
\n \n );\n};\n\nconst ToastMessage = ({ type, shortDescription, eventData }) => {\n const { t } = useTranslator();\n let message;\n switch (type) {\n case 'ItemOutbidded':\n message = t('siteWideNotifications_itemOutbidded', {\n newLeadingBidAmount: formatNumberWithSeparators(\n eventData.newLeadingBidAmount\n ),\n shortDescription\n });\n break;\n case 'ItemFirstBid':\n message = t('siteWideNotifications_itemFirstBid', {\n shortDescription\n });\n break;\n case 'ItemSold':\n message = t('siteWideNotifications_itemSold', {\n price: formatNumberWithSeparators(eventData.price),\n shortDescription\n });\n break;\n case 'ItemWon':\n message = t('siteWideNotifications_itemWon', {\n price: formatNumberWithSeparators(eventData.price),\n shortDescription\n });\n break;\n case 'ItemPaid':\n message = t('siteWideNotifications_itemPaid', {\n shortDescription\n });\n break;\n case 'ItemWishListReminder':\n message = t('siteWideNotifications_itemWishListReminder', {\n shortDescription,\n timeLeftMinutes: eventData.timeLeftMinutes\n });\n }\n return {message}
;\n};\n","import React from 'react';\nimport { useTranslator } from 'tradera-lang/translate';\n\nconst MessageToast = ({ message, header }) => {\n const { t } = useTranslator();\n\n return (\n \n {header &&
{t(header)}
}\n {message && (\n {t(message)}\n )}\n \n );\n};\n\nexport default MessageToast;\n","import React from 'react';\nimport { toast } from 'react-toastify';\n\nimport { ItemToast } from 'tradera-components/toasts/item-toast';\nimport MessageToast from 'tradera-components/toasts/message-toast';\nimport { webApiClient } from '../utils/http';\nimport { createQueue } from './toast-queue';\n\nconst queuedToasts = createQueue();\n\nexport const ToastTestButton = () => (\n \n);\n\nconst openNextToast = async () => {\n if (queuedToasts.isInProgress()) {\n return;\n }\n const nextItem = queuedToasts.next();\n if (nextItem) {\n try {\n const queueLength = queuedToasts.getLength();\n const toastOptions = {\n autoClose: Math.max(2, 5 - queueLength) * 1000,\n onClose: () => {\n queuedToasts.notifyDone();\n openNextToast();\n }\n };\n if (nextItem.eventData.itemId) {\n const response = await webApiClient.get(\n `/browse/item-basic-info/${nextItem.eventData.itemId}`\n );\n const {\n imageUrl,\n itemUrl,\n shortDescription\n } = response.data.item;\n toast(\n React.createElement(ItemToast, {\n imageUrl,\n itemUrl,\n shortDescription,\n type: nextItem.type,\n eventData: nextItem.eventData\n }),\n toastOptions\n );\n } else if (nextItem.eventData.message) {\n const { message, header } = nextItem.eventData;\n toast(\n React.createElement(MessageToast, {\n message,\n header\n }),\n { ...toastOptions, type: nextItem.eventData.type }\n );\n }\n } catch (e) {\n queuedToasts.notifyDone();\n openNextToast();\n throw e;\n }\n }\n};\n\nconst handleNotification = async (type, eventData, ttlInMinutes = 10) => {\n if (document.visibilityState !== 'visible') {\n // We don't want to show messages if page is not visible. If we do they will queue up!\n return;\n }\n\n queuedToasts.add({ type, eventData }, ttlInMinutes);\n openNextToast();\n};\n\nexport const showItemOutbiddedToast = (itemId, newLeadingBidAmount) =>\n handleNotification('ItemOutbidded', {\n itemId,\n newLeadingBidAmount\n });\n\nexport const showItemFirstBidToast = itemId =>\n handleNotification('ItemFirstBid', { itemId });\n\nexport const showItemSoldToast = (itemId, price) =>\n handleNotification('ItemSold', { itemId, price });\n\nexport const showItemWonToast = (itemId, price) =>\n handleNotification('ItemWon', { itemId, price });\n\nexport const showItemPaidToast = itemId =>\n handleNotification('ItemPaid', { itemId });\n\nexport const showItemWishListReminderToast = (itemId, timeLeftMinutes) =>\n handleNotification(\n 'ItemWishListReminder',\n { itemId, timeLeftMinutes },\n timeLeftMinutes > 10 ? 10 : 2\n );\n\nexport const showSuccessToast = (message, header) => {\n handleNotification('Message', {\n message,\n header,\n type: toast.TYPE.SUCCESS\n });\n};\n\nexport const showErrorToast = (message, header) => {\n handleNotification('Message', {\n message,\n header,\n type: toast.TYPE.ERROR\n });\n};\n\nexport const showInfoToast = (message, header) => {\n handleNotification('Message', {\n message,\n header,\n type: toast.TYPE.INFO\n });\n};\n","/**\n * A queue with a that handled max age (ttl) for messages.\n * @returns\n */\nexport const createQueue = () => {\n const items = [];\n const dayInMinutes = 60 * 24;\n const add = (item, ttlInMinutes = dayInMinutes) => {\n const timeStamp = Date.now();\n items.push({ item, ttlInMinutes, timeStamp });\n };\n const isAlive = ({ ttlInMinutes, timeStamp }) => {\n return timeStamp + ttlInMinutes * 60 * 1000 >= Date.now();\n };\n let inProgress = null;\n return {\n add,\n next: () => {\n let itemData = items.shift();\n while (itemData && !isAlive(itemData)) {\n itemData = items.shift();\n }\n inProgress = itemData?.item || null;\n return inProgress;\n },\n notifyDone: () => {\n inProgress = null;\n },\n isInProgress: () => inProgress !== null,\n getLength: () => items.length\n };\n};\n","const bannedCountries = new Set([\n 'by',\n 'cu',\n 'fx',\n 'ir',\n 'kp',\n 'ru',\n 'sy',\n 'ua'\n]);\n\nexport const removeBannedCountries = countryCodeIso2 =>\n !bannedCountries.has(countryCodeIso2.toLowerCase());\n\nexport const getAvailableCountries = t => {\n return Object.keys(countries)\n .filter(removeBannedCountries)\n .map(code => ({\n code,\n name: t(mapCountryCodeToCountryKey(code))\n }))\n .sort((a, b) => (a.name > b.name ? 1 : -1));\n};\n\n// We map this way to ensure that each key in lokalise is present in code.\n// This allows us to clean up unnecessary codes in the future.\nexport const mapCountryCodeToCountryKey = countryCode => {\n const lokaliseKey = countries[countryCode.toLowerCase()];\n if (!lokaliseKey)\n throw `CountryCode ${countryCode} has no mapping to lokalise key`;\n return lokaliseKey;\n};\n\nexport const countries = {\n ad: 'country_ad',\n ae: 'country_ae',\n af: 'country_af',\n ag: 'country_ag',\n ai: 'country_ai',\n al: 'country_al',\n am: 'country_am',\n an: 'country_an',\n ao: 'country_ao',\n aq: 'country_aq',\n ar: 'country_ar',\n as: 'country_as',\n at: 'country_at',\n au: 'country_au',\n aw: 'country_aw',\n az: 'country_az',\n ba: 'country_ba',\n bb: 'country_bb',\n bd: 'country_bd',\n be: 'country_be',\n bf: 'country_bf',\n bg: 'country_bg',\n bh: 'country_bh',\n bi: 'country_bi',\n bj: 'country_bj',\n bm: 'country_bm',\n bn: 'country_bn',\n bo: 'country_bo',\n br: 'country_br',\n bs: 'country_bs',\n bt: 'country_bt',\n bv: 'country_bv',\n bw: 'country_bw',\n by: 'country_by',\n bz: 'country_bz',\n ca: 'country_ca',\n cc: 'country_cc',\n cd: 'country_cd',\n cf: 'country_cf',\n cg: 'country_cg',\n ch: 'country_ch',\n ci: 'country_ci',\n ck: 'country_ck',\n cl: 'country_cl',\n cm: 'country_cm',\n cn: 'country_cn',\n co: 'country_co',\n cr: 'country_cr',\n cu: 'country_cu',\n cv: 'country_cv',\n cx: 'country_cx',\n cy: 'country_cy',\n cz: 'country_cz',\n de: 'country_de',\n dj: 'country_dj',\n dk: 'country_dk',\n dm: 'country_dm',\n do: 'country_do',\n dz: 'country_dz',\n ec: 'country_ec',\n ee: 'country_ee',\n eg: 'country_eg',\n eh: 'country_eh',\n er: 'country_er',\n es: 'country_es',\n et: 'country_et',\n fi: 'country_fi',\n fj: 'country_fj',\n fk: 'country_fk',\n fm: 'country_fm',\n fo: 'country_fo',\n fr: 'country_fr',\n fx: 'country_fx',\n ga: 'country_ga',\n gb: 'country_gb',\n gd: 'country_gd',\n ge: 'country_ge',\n gf: 'country_gf',\n gh: 'country_gh',\n gi: 'country_gi',\n gl: 'country_gl',\n gm: 'country_gm',\n gn: 'country_gn',\n gp: 'country_gp',\n gq: 'country_gq',\n gr: 'country_gr',\n gs: 'country_gs',\n gt: 'country_gt',\n gu: 'country_gu',\n gw: 'country_gw',\n gy: 'country_gy',\n hk: 'country_hk',\n hm: 'country_hm',\n hn: 'country_hn',\n hr: 'country_hr',\n ht: 'country_ht',\n hu: 'country_hu',\n id: 'country_id',\n ie: 'country_ie',\n il: 'country_il',\n in: 'country_in',\n io: 'country_io',\n iq: 'country_iq',\n ir: 'country_ir',\n is: 'country_is',\n it: 'country_it',\n jm: 'country_jm',\n jo: 'country_jo',\n jp: 'country_jp',\n ke: 'country_ke',\n kg: 'country_kg',\n kh: 'country_kh',\n ki: 'country_ki',\n km: 'country_km',\n kn: 'country_kn',\n kp: 'country_kp',\n kr: 'country_kr',\n kw: 'country_kw',\n ky: 'country_ky',\n kz: 'country_kz',\n la: 'country_la',\n lb: 'country_lb',\n lc: 'country_lc',\n li: 'country_li',\n lk: 'country_lk',\n lr: 'country_lr',\n ls: 'country_ls',\n lt: 'country_lt',\n lu: 'country_lu',\n lv: 'country_lv',\n ly: 'country_ly',\n ma: 'country_ma',\n mc: 'country_mc',\n md: 'country_md',\n mg: 'country_mg',\n mh: 'country_mh',\n mk: 'country_mk',\n ml: 'country_ml',\n mm: 'country_mm',\n mn: 'country_mn',\n mo: 'country_mo',\n mp: 'country_mp',\n mq: 'country_mq',\n mr: 'country_mr',\n ms: 'country_ms',\n mt: 'country_mt',\n mu: 'country_mu',\n mv: 'country_mv',\n mw: 'country_mw',\n mx: 'country_mx',\n my: 'country_my',\n mz: 'country_mz',\n na: 'country_na',\n nc: 'country_nc',\n ne: 'country_ne',\n nf: 'country_nf',\n ng: 'country_ng',\n ni: 'country_ni',\n nl: 'country_nl',\n no: 'country_no',\n np: 'country_np',\n nr: 'country_nr',\n nu: 'country_nu',\n nz: 'country_nz',\n om: 'country_om',\n pa: 'country_pa',\n pe: 'country_pe',\n pf: 'country_pf',\n pg: 'country_pg',\n ph: 'country_ph',\n pk: 'country_pk',\n pl: 'country_pl',\n pm: 'country_pm',\n pn: 'country_pn',\n pr: 'country_pr',\n ps: 'country_ps',\n pt: 'country_pt',\n pw: 'country_pw',\n py: 'country_py',\n qa: 'country_qa',\n re: 'country_re',\n ro: 'country_ro',\n ru: 'country_ru',\n rs: 'country_rs',\n rw: 'country_rw',\n sa: 'country_sa',\n sb: 'country_sb',\n sc: 'country_sc',\n sd: 'country_sd',\n se: 'country_se',\n sg: 'country_sg',\n sh: 'country_sh',\n si: 'country_si',\n sj: 'country_sj',\n sk: 'country_sk',\n sl: 'country_sl',\n sm: 'country_sm',\n sn: 'country_sn',\n so: 'country_so',\n sr: 'country_sr',\n st: 'country_st',\n sv: 'country_sv',\n sy: 'country_sy',\n sz: 'country_sz',\n tc: 'country_tc',\n td: 'country_td',\n tf: 'country_tf',\n tg: 'country_tg',\n th: 'country_th',\n tj: 'country_tj',\n tk: 'country_tk',\n tl: 'country_tl',\n tm: 'country_tm',\n tn: 'country_tn',\n to: 'country_to',\n tr: 'country_tr',\n tt: 'country_tt',\n tv: 'country_tv',\n tw: 'country_tw',\n tz: 'country_tz',\n ua: 'country_ua',\n ug: 'country_ug',\n um: 'country_um',\n us: 'country_us',\n uy: 'country_uy',\n uz: 'country_uz',\n va: 'country_va',\n vc: 'country_vc',\n ve: 'country_ve',\n vg: 'country_vg',\n vi: 'country_vi',\n vn: 'country_vn',\n vu: 'country_vu',\n wf: 'country_wf',\n ws: 'country_ws',\n ye: 'country_ye',\n yt: 'country_yt',\n yu: 'country_yu',\n za: 'country_za',\n zm: 'country_zm',\n zw: 'country_zw'\n};\n","import ENDPOINTS from 'tradera-constants/endpoints';\nimport {\n axiosWithTokenRefresh,\n checkResponseVersion,\n finalizeResponse,\n handleError,\n utilizeCancelToken,\n axiosConfigs\n} from 'tradera-utils/api';\nimport initData from 'init-data';\nimport { toLocalizedUrl } from 'tradera-utils/url';\nimport { getLanguage } from 'tradera-apps/syi/script/app_react/utils/language';\n\n// Prevents URL:s that begins with // when that was not intended.\nconst getSafeUrl = (baseUrl, url) => {\n if (baseUrl.endsWith('/') && url.startsWith('/')) {\n return baseUrl + url.substring(1);\n }\n return baseUrl + url;\n};\n\nconst httpClient = (baseUrl, shouldLocalizeUrl) => {\n const axiosWrapper = (url, httpClientConfig, axiosCaller) => {\n let { cancelTokenId, ...axiosConfig } = httpClientConfig;\n const version = initData.version;\n if (cancelTokenId) {\n const { cancel, cancelToken } = utilizeCancelToken(cancelTokenId);\n axiosConfig.cancelToken = cancelToken;\n if (cancel) {\n cancel();\n }\n }\n const safeUrl = getSafeUrl(baseUrl, url);\n const localizedUrl = shouldLocalizeUrl\n ? toLocalizedUrl(safeUrl, getLanguage())\n : safeUrl;\n return axiosCaller(axiosWithTokenRefresh(), localizedUrl, axiosConfig)\n .then(\n version ? checkResponseVersion(version) : response => response\n )\n .then(finalizeResponse())\n .catch(handleError());\n };\n\n return {\n get: (url, httpClientConfig = axiosConfigs.authenticated) => {\n return axiosWrapper(url, httpClientConfig, (axios, url, config) =>\n axios.get(url, config)\n );\n },\n post: (url, payload, httpClientConfig = axiosConfigs.authenticated) => {\n return axiosWrapper(url, httpClientConfig, (axios, url, config) =>\n axios.post(url, payload, config)\n );\n },\n put: (url, payload, httpClientConfig = axiosConfigs.authenticated) => {\n return axiosWrapper(url, httpClientConfig, (axios, url, config) =>\n axios.put(url, payload, config)\n );\n }\n };\n};\n\nconst trpcClient = endpoint => {\n const client = httpClient(`/api/${endpoint}`, false);\n const unpackResponse = ({ data }) => data;\n return {\n command: (commandName, payload, httpClientConfig) =>\n client\n .post(`/commands/${commandName}`, payload, httpClientConfig)\n .then(unpackResponse),\n query: (queryName, httpClientConfig) =>\n client\n .get(`/queries/${queryName}`, httpClientConfig)\n .then(unpackResponse)\n };\n};\n\nexport const defaultClient = httpClient('');\nexport const touchWebClient = httpClient('/', true);\nexport const webApiClient = httpClient(ENDPOINTS.WEB_API);\nexport const cmsApiClient = httpClient(ENDPOINTS.CMS_API);\nexport const marketingApiClient = httpClient(ENDPOINTS.MARKETING_PUBLIC_API);\nexport const memberIdentificationClient = trpcClient('member-identification');\nexport const translationOnDemandClient = trpcClient('translation-on-demand');\nexport const shippingRecommendationsClient = trpcClient(\n 'shipping-recommendations'\n);\n","import { nullifyUndefinedProperties } from 'tradera-utils/object';\nimport { capitalize } from 'tradera-utils/string';\nimport {\n SUPPORTED_LANGUAGES,\n DEFAULT_LANGUAGE\n} from 'tradera-lang/constants.mjs';\n\nconst getInitialPreferredLanguage = (\n languageCodeIso2,\n availableLanguages,\n defaultLanguage\n) => {\n const availableIsoCodes = availableLanguages.map(\n lang => lang.languageCodeIso2\n );\n if (\n !!languageCodeIso2 &&\n availableIsoCodes.indexOf(languageCodeIso2) !== -1\n ) {\n return availableLanguages.find(\n l => l.languageCodeIso2 === languageCodeIso2\n );\n }\n\n return defaultLanguage;\n};\n\nconst getLanguageDisplayName = languageCodeIso2 =>\n capitalize(\n // Only works on server side, lacks good browser support\n new Intl.DisplayNames([languageCodeIso2], { type: 'language' }).of(\n languageCodeIso2\n )\n );\n\nexport const getInitialServerLanguage = ({\n preferredLanguage = DEFAULT_LANGUAGE,\n memberCountryCodeIso2 = null,\n automaticTranslationPreference = false\n}) => {\n const defaultLanguage = {\n languageCodeIso2: preferredLanguage,\n displayName: getLanguageDisplayName(preferredLanguage)\n };\n const availableLanguages = SUPPORTED_LANGUAGES.map(languageCodeIso2 => ({\n languageCodeIso2,\n displayName: getLanguageDisplayName(languageCodeIso2)\n }));\n\n return {\n preferred: getInitialPreferredLanguage(\n preferredLanguage,\n availableLanguages,\n defaultLanguage\n ),\n available: availableLanguages,\n memberCountryCodeIso2,\n automaticTranslationPreference\n };\n};\n\nexport default initData => {\n const result = nullifyUndefinedProperties({\n preferred: getInitialPreferredLanguage(\n initData.languageCodeIso2,\n initData.availableLanguages || [],\n initData.defaultLanguage\n ),\n memberCountryCodeIso2: initData.memberCountryCodeIso2,\n available: initData.availableLanguages,\n automaticTranslationPreference: initData.automaticTranslationPreference\n });\n return result;\n};\n","import { createSlice } from '@reduxjs/toolkit';\nimport getInitialState, { getInitialServerLanguage } from './initial-state';\n\nconst languageSlice = createSlice({\n name: 'language',\n initialState: {},\n reducers: {\n initialize: (_state, { payload }) => {\n return getInitialState(payload);\n },\n initServerLanguage: (_state, { payload }) => {\n return getInitialServerLanguage(payload);\n },\n setPreferred: (state, action) => {\n state.preferred =\n state.available.find(\n language => language.languageCodeIso2 === action.payload\n ) || {};\n },\n setAutomaticTranslationPreference: (state, { payload }) => {\n state.automaticTranslationPreference =\n payload.automaticTranslationPreference;\n }\n }\n});\n\nexport const {\n initialize,\n initServerLanguage,\n setPreferred,\n setAutomaticTranslationPreference\n} = languageSlice.actions;\nexport default languageSlice.reducer;\n","import {\n selectGeolocation,\n selectIsOutsideSweden\n} from 'tradera-state/member/selectors';\nimport { selectPreferredLanguage } from 'tradera-state/language/selectors';\nimport { selectIsLoggedIn } from 'tradera-state/member';\nimport { getLanguageFromCountryCode } from 'tradera-utils/languagecode';\nimport { isSwedenCountryCodeOrUndefined } from 'tradera-utils/countrycode';\n\nexport const selectShowRegionSelection = state =>\n !selectIsLoggedIn(state) && selectIsOutsideSweden(state);\nexport const selectAvailableShippingCountries = state =>\n state.shippingRegion.availableShippingCountries;\nexport const selectShippingCountry = state =>\n state.shippingRegion.shippingCountry;\nexport const selectIsLoadingAvailableShippingCountries = state =>\n state.shippingRegion.isLoadingAvailableShippingCountries;\nexport const selectHasLoadedAvailableShippingCountries = state =>\n state.shippingRegion.hasLoadedAvailableShippingCountries;\nexport const selectShippingRegionCountryCodeIso2 = state =>\n state.shippingRegion.shippingCountry?.countryCodeIso2;\nexport const selectIsSwedishVisitor = state =>\n isSwedenCountryCodeOrUndefined(selectShippingRegionCountryCodeIso2(state));\n\nconst gpsMatchesLanguage = state => {\n const geoLocation = selectGeolocation(state);\n if (!geoLocation) {\n return true;\n }\n const country = geoLocation?.isoCode?.toLowerCase();\n const expectedLang = getLanguageFromCountryCode(country);\n const language =\n selectPreferredLanguage(state)?.languageCodeIso2?.toLowerCase() || 'sv';\n return expectedLang === language;\n};\n\nexport const selectShowLocalizationRegionPicker = state =>\n !selectIsLoggedIn(state) &&\n state.shippingRegion.localizationRegionPickerEnabled &&\n !state.shippingRegion.regionPickerDismissed &&\n !gpsMatchesLanguage(state);\n","export default {\n TRADERA: 'https://www.tradera.com'\n};\n","import qs from 'qs';\nimport HOSTS from 'tradera-constants/hosts';\nimport initData from 'init-data';\nimport logger from 'packages/logger';\nimport { URL_LANGUAGES } from 'tradera-lang/constants.mjs';\n\n// List of SPA paths for the regex match, without initial slash\nconst SPA_REGEX_PATHS = [\n 'search',\n 'campaign',\n 'charity',\n 'kategorier',\n 'profile\\\\/items',\n 'brand\\\\/.+',\n // Category\n '[a-z]+[\\\\w\\\\-]+[\\\\-|_]\\\\d+'\n];\nconst SPA_PATH_REGEX = new RegExp(\n `^(\\\\/en|\\\\/da|\\\\/de)?/(${SPA_REGEX_PATHS.join('|')})`\n);\n\n/**\n * Returns language parameter from url\n * @param {*} url\n */\nconst getLanguagePrefixFromUrl = url => {\n const matches = url.match(/\\/([a-z][a-z])((\\/.*$)|$)/);\n\n if (matches === null) {\n return null;\n }\n\n const firstMatch = matches.find((match, index) => index > 0 && match);\n\n if (URL_LANGUAGES.indexOf(firstMatch) !== -1) {\n return firstMatch;\n }\n\n return null;\n};\n\n/**\n * Format query params parsed from query string\n *\n * @param {object} queryParams\n * @returns {object}\n */\nconst formatQueryParams = queryParams => {\n const { categoryId, ...rest } = queryParams;\n const formattedQueryParams = rest;\n if (categoryId !== undefined) {\n formattedQueryParams.categoryId = parseInt(categoryId, 10);\n }\n return formattedQueryParams;\n};\n\n/**\n * Check if url is absolute\n *\n * @param {string} url\n * @returns {boolean}\n */\nconst isAbsoluteUrl = url => new RegExp('^(?:[a-z]+:)?//', 'i').test(url);\n\n/**\n * Tests if a given url or path is an SPA link or not\n *\n * @param {string} urlOrPath\n * @returns {boolean}\n */\nfunction isSpaLink(urlOrPath) {\n let pathname;\n if (/(^http)s?:\\/\\//i.test(urlOrPath)) {\n try {\n ({ pathname } = new URL(urlOrPath));\n } catch {\n return false;\n }\n } else {\n pathname = urlOrPath;\n }\n return SPA_PATH_REGEX.test(pathname);\n}\n\n/**\n * Takes an object and returns a new object without attribute properties\n * @param {object} queryParams\n * @returns {object}\n */\nconst stripAttributesFromQueryParameters = queryParams => {\n // Using object spread to exclude listed variables from 'rest'\n const { attributes, ...rest } = queryParams;\n return rest;\n};\n\n/**\n * Takes an object and returns a new object without pagination properties\n * @param {object} queryParams\n * @returns {object}\n */\nconst stripCategoryIdFromQueryParameters = queryParams => {\n // Using object spread to exclude listed variables from 'rest'\n const { categoryId, ...rest } = queryParams;\n return rest;\n};\n\n/**\n * Removes tradera base from a url, leaving path and query\n *\n * @param {string} url\n * @returns {string}\n */\nconst stripHost = url =>\n url.startsWith('http') ? url.replace(HOSTS.TRADERA, '') : url;\n\n/**\n * Takes an object and returns a new object without itm properties\n * Itm tags should no longer be used, so this method will then be obsolete\n * @param {object} queryParams\n * @returns {object}\n */\nconst stripItmFromQueryParameters = queryParams => {\n // Using object spread to exclude listed variables from 'rest'\n const { itm_source, itm_medium, ...rest } = queryParams;\n return rest;\n};\n\n/**\n * Takes an object and returns a new object without pagination properties\n * @param {object} queryParams\n * @returns {object}\n */\nconst stripPaginationFromQueryParameters = queryParams => {\n // Using object spread to exclude listed variables from 'rest'\n const { spage, paging, page, fe, ...rest } = queryParams;\n return rest;\n};\n\n/**\n * Takes an object and returns a new object without savedQueryName properties\n * @param {object} queryParams\n * @returns {object}\n */\nconst stripSavedQueryNameFromQueryParameters = queryParams => {\n // Using object spread to exclude listed variables from 'rest'\n const { savedQueryName, ...rest } = queryParams;\n return rest;\n};\n\n/**\n * Takes an url and adds tradera base url if it is not already an absolute url\n *\n * @param {string} relativeUrl\n * @returns {string}\n */\nconst toAbsoluteUrl = relativeUrl =>\n relativeUrl.startsWith('http')\n ? relativeUrl\n : `${HOSTS.TRADERA}${\n relativeUrl.startsWith('/') ? relativeUrl : '/' + relativeUrl\n }`;\n\n/**\n * From react router location object to absolute url string\n * @param {{pathname: string, search: string}} url\n * @returns {string}\n */\nconst toAbsoluteUrlFromObject = url =>\n toAbsoluteUrl(\n `${url.pathname ? url.pathname : ''}${url.search ? url.search : ''}`\n );\n\n/**\n * From URL to URL with language prefix\n * @param {*} url\n * @param {*} languageCodeIso2\n */\nconst toLocalizedUrl = (url, languageCodeIso2) => {\n if (url === null || url === undefined || url === '' || url === false) {\n return url;\n }\n\n const ignoredUrls = [initData.webApiUrl, '/api/webapi/'];\n const isIgnoredUrl = ignoredUrls.find(domain => url.indexOf(domain) !== -1);\n if (isIgnoredUrl) {\n return url;\n }\n\n if (!languageCodeIso2) {\n logger(`Missing language code for URL ${url}`);\n return url;\n }\n\n const isAbsolute = isAbsoluteUrl(url);\n\n const urlObject = isAbsolute\n ? new URL(url)\n : new URL(url, 'http://dummy.com');\n const origin = isAbsolute ? urlObject.origin : '';\n const localisedPathname = setLanguagePrefix(\n urlObject.pathname,\n languageCodeIso2\n );\n\n return `${origin}${localisedPathname}${urlObject.search}`;\n};\n\n/**\n * Creates query parameters object from search string\n * @param {string} search\n */\nconst toQueryParameters = search =>\n qs.parse(search, { ignoreQueryPrefix: true });\n\n/**\n * Removes the Tradera host from a url, making it relative\n * @param {string} url\n * @returns {string}\n */\nconst toRelativeTraderaUrl = url => {\n if (url.startsWith(HOSTS.TRADERA)) {\n return url.replace(HOSTS.TRADERA, '');\n } else {\n return url;\n }\n};\n\n/**\n * Creates search string from all properties in object\n * @param {object} queryParams\n * @param {{[addQueryPrefix]: bool, [encode]: bool, [skipNulls]: bool }}\n * @returns {string}\n */\nconst toSearchString = (\n queryParams,\n {\n addQueryPrefix = true,\n encode = true,\n skipNulls = true,\n arrayFormat = 'repeat' //qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'repeat' }) ==> 'a=b&a=c'\n } = {}\n) =>\n qs.stringify(queryParams, {\n addQueryPrefix: addQueryPrefix,\n encode: encode,\n skipNulls: skipNulls,\n arrayFormat: arrayFormat\n });\n\nconst setLanguagePrefix = (path, languageCodeIso2) => {\n const languagePrefix = getLanguagePrefixFromUrl(path);\n const defaultLanguage = 'sv';\n\n if (languagePrefix) {\n if (languageCodeIso2 === defaultLanguage) {\n return path.replace(`/${languagePrefix}`, '');\n } else {\n return path.replace(languagePrefix, languageCodeIso2);\n }\n } else {\n if (languageCodeIso2 === defaultLanguage) {\n return path;\n } else {\n return `/${languageCodeIso2}${path}`;\n }\n }\n};\n\nconst getBackToHereRedirectUrl = () => {\n // Add random query parameter to force reload when redirecting to this url\n // and only the hash differs\n const queryParams = {\n ...qs.parse(location.search, { ignoreQueryPrefix: true }),\n forceRedirect: Math.round(Math.random() * 100000)\n };\n return (\n location.protocol +\n '//' +\n location.host +\n location.pathname +\n qs.stringify(queryParams, { addQueryPrefix: true })\n );\n};\n\nexport {\n getLanguagePrefixFromUrl,\n formatQueryParams,\n getBackToHereRedirectUrl,\n isAbsoluteUrl,\n isSpaLink,\n stripAttributesFromQueryParameters,\n stripCategoryIdFromQueryParameters,\n stripHost,\n stripItmFromQueryParameters,\n stripPaginationFromQueryParameters,\n stripSavedQueryNameFromQueryParameters,\n toAbsoluteUrl,\n toAbsoluteUrlFromObject,\n toLocalizedUrl,\n toQueryParameters,\n toRelativeTraderaUrl,\n toSearchString\n};\n","import { swedishTimeZoneDate } from 'tradera-utils/date';\n\nfunction formatDuration(durationInSeconds, minuteBreakpoint = 60) {\n if (Number.isInteger(durationInSeconds)) {\n if (durationInSeconds > 24 * 60 * 60) return ''; // Show format from server\n\n if (durationInSeconds > 60 * 60)\n return `${Math.floor(\n durationInSeconds / 60 / 60\n )} tim ${Math.floor((durationInSeconds / 60) % 60)} min`;\n\n if (durationInSeconds > minuteBreakpoint)\n return `${Math.floor(durationInSeconds / 60)} min`;\n\n return `${durationInSeconds} s`;\n } else {\n return '';\n }\n}\n\nfunction formatEndDate(endDateInput, t, nowDateInput, dayjs) {\n if (!endDateInput || !nowDateInput) return '';\n\n const endDate = swedishTimeZoneDate(endDateInput, dayjs);\n const now = swedishTimeZoneDate(nowDateInput, dayjs);\n if (endDate.isSameOrAfter(now.add(1, 'week'), 'day')) {\n return endDate.format('D MMM HH:mm');\n }\n if (endDate.isSameOrAfter(now.add(2, 'day'), 'day')) {\n return endDate.format('ddd HH:mm');\n }\n if (endDate.isSameOrAfter(now.add(1, 'day'))) {\n return `${t('vip_tomorrow')} ${endDate.format('HH:mm')}`;\n }\n if (endDate.isSameOrAfter(now.add(1, 'minute'))) {\n const hours = endDate.diff(now, 'hour');\n const minutes = endDate.diff(now, 'minute') % 60;\n const timeParts = [];\n if (hours > 0) {\n timeParts.push(t('vip_hours', { count: hours }));\n }\n if (minutes > 0) {\n timeParts.push(t('vip_minutes', { count: minutes }));\n }\n return timeParts.join(' ');\n }\n if (endDate.isAfter(now)) {\n return t('vip_less_than_1_min_left');\n }\n return t('vip_ended');\n}\n\nfunction formatNumberWithSeparators(number, separator = '\\u2006') {\n return number\n .toString()\n .replace(/(\\d)(?=(\\d{3})+(?!\\d))/g, '$1' + separator);\n}\n\n// https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Number/toLocaleString\nfunction toLocaleStringSupportsLocales() {\n try {\n Number(0).toLocaleString('i');\n } catch (e) {\n return e.name === 'RangeError';\n }\n return false;\n}\n\n// https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Number/toLocaleString\nfunction toLocaleStringSupportsOptions() {\n return !!(\n typeof Intl === 'object' &&\n Intl &&\n typeof Intl.NumberFormat === 'function'\n );\n}\n\nconst isLocaleFormattingSupportedInBrowser =\n toLocaleStringSupportsLocales() && toLocaleStringSupportsOptions();\n\nfunction formatSellerDsrAverage(value, locale = 'sv-SE') {\n return value?.toLocaleString(locale, {\n minimumFractionDigits: 1,\n maximumFractionDigits: 1\n });\n}\n\nfunction formatPrice(price, locale = 'sv-SE', overrideOptions = {}) {\n const defaultOptions = {\n style: 'currency',\n currency: 'SEK',\n currencyDisplay:\n locale.startsWith('sv') &&\n (overrideOptions.currency || 'SEK') === 'SEK'\n ? 'symbol'\n : 'code',\n useGrouping: false,\n minimumFractionDigits: 0,\n maximumFractionDigits: 2,\n ...overrideOptions\n };\n const priceAsNumber = typeof price !== 'number' ? parseInt(price) : price;\n\n if (!isLocaleFormattingSupportedInBrowser) {\n const suffix =\n defaultOptions.currencyDisplay === 'code' &&\n defaultOptions.currency === 'SEK'\n ? 'kr'\n : defaultOptions.currency;\n return `${priceAsNumber} ${suffix}`;\n }\n\n const formattedPrice = priceAsNumber.toLocaleString(locale, defaultOptions);\n return formatNumberWithSeparators(formattedPrice);\n}\n\nfunction formatDateWithDayOfWeek(dateString, locale) {\n if (!isLocaleFormattingSupportedInBrowser) {\n return dateString;\n }\n\n var options = {\n weekday: 'long',\n year: undefined,\n month: 'long',\n day: 'numeric'\n };\n return new Intl.DateTimeFormat(locale, options).format(\n Date.parse(dateString)\n );\n}\n\nexport {\n formatDuration,\n formatEndDate,\n formatNumberWithSeparators,\n formatPrice,\n formatSellerDsrAverage,\n formatDateWithDayOfWeek\n};\n","export const parseAppVersion = version => {\n const [major = 0, minor = 0, patch = 0] = String(version)\n .split('.')\n .map(Number)\n .filter(number => !Number.isNaN(number));\n return { major, minor, patch };\n};\n\nexport const isSupportedMinimumVersion = (\n appVersionString,\n minimumVersionString\n) => {\n const appVersion = parseAppVersion(appVersionString);\n const minimumVersion = parseAppVersion(minimumVersionString);\n if (appVersion.major > minimumVersion.major) {\n return true;\n }\n if (appVersion.major === minimumVersion.major) {\n if (appVersion.minor > minimumVersion.minor) {\n return true;\n }\n if (appVersion.minor === minimumVersion.minor) {\n if (appVersion.patch >= minimumVersion.patch) {\n return true;\n }\n }\n }\n\n return false;\n};\n","import { isSupportedMinimumVersion } from 'tradera-utils/versions';\n\nexport const getNativeAppSupport = ({\n isNativeAppContext,\n isHybridAppContext,\n hybridAppDevice,\n hybridAppVersion,\n appOsVersion,\n isIOS,\n isAndroid\n}) => {\n const isHybridAppContextForAndroid = Boolean(\n isHybridAppContext &&\n (hybridAppDevice?.toLowerCase().includes('android') || isAndroid)\n );\n const isHybridAppContextForIos = Boolean(\n isHybridAppContext &&\n (hybridAppDevice?.toLowerCase().includes('iphone') ||\n hybridAppDevice?.toLowerCase().includes('ipad') ||\n isIOS)\n );\n const isIos13 =\n isNativeAppContext &&\n isHybridAppContextForIos &&\n isSupportedMinimumVersion(appOsVersion, '13');\n return {\n isHybridAppContextForAndroid,\n isHybridAppContextForIos,\n isNativeAppWithApplyPaySupport:\n isIos13 && isSupportedMinimumVersion(hybridAppVersion, '3.51'),\n isNativeAppWithSwishCallbackSupport:\n isIos13 && isSupportedMinimumVersion(hybridAppVersion, '3.57')\n };\n};\n","import {\n isAndroid,\n isMobile,\n isMobileSafari,\n isIOS,\n getSelectorsByUserAgent\n} from 'react-device-detect';\nimport { nullifyUndefinedProperties } from 'tradera-utils/object';\nimport { isServer } from 'tradera-utils/nextjs';\nimport { getNativeAppSupport } from './native-app-support';\n\nconst IPHONE_USER_AGENT =\n 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1';\n\nexport const getInitialServerState = ({\n environmentHash,\n featureToggles = {},\n splitTestGroups = {},\n variables = {},\n version,\n userAgent = IPHONE_USER_AGENT,\n appOsVersion = '',\n hybridAppDevice = '',\n hybridAppVersion = '',\n isNativeAppContext = false,\n isHybridAppContext = false\n} = {}) => {\n const {\n isMobile,\n isMobileSafari,\n isIOS,\n isAndroid\n } = getSelectorsByUserAgent(userAgent);\n\n return {\n ...getNativeAppSupport({\n isNativeAppContext,\n isHybridAppContext,\n hybridAppDevice,\n hybridAppVersion,\n appOsVersion,\n isIOS,\n isAndroid\n }),\n isNativeAppContext,\n hybridAppVersion,\n environment: process.env.NODE_ENV,\n isHybridAppContext,\n isIOS,\n isMobileDevice: isMobile,\n isMobileSafari,\n isSpaNavigationEnabled: true,\n featureSwitches: featureToggles,\n splitTestGroups,\n variables,\n environmentHash,\n version\n // TODO: Fetch values for below\n // splitTests,\n };\n};\n\nexport const initEnvironment = () => {\n return nullifyUndefinedProperties({});\n};\n\nexport default ({\n appOsVersion,\n environment,\n environmentHash,\n featureSwitches,\n geoPublicApiBaseUrl,\n hybridAppDevice,\n hybridAppVersion,\n isHybridAppContext,\n isNativeAppContext,\n isSinglePageApp,\n splitTests,\n splitTestGroups,\n version,\n webLiveUrl\n}) => {\n return nullifyUndefinedProperties({\n ...getNativeAppSupport({\n isNativeAppContext,\n isHybridAppContext,\n hybridAppDevice,\n hybridAppVersion,\n appOsVersion,\n isIOS,\n isAndroid\n }),\n environment,\n featureSwitches,\n environmentHash,\n isHybridAppContext,\n isIOS,\n isNativeAppContext,\n isMobileDevice: isMobile,\n isMobileSafari,\n isSpaNavigationEnabled:\n !isServer &&\n isSinglePageApp &&\n !isHybridAppContext &&\n !window.frameElement\n ? true\n : false, // No SPA navigation in native apps or iFrame\n splitTests,\n splitTestGroups,\n variables: {\n // Names must be the same as NextWeb's .env variables\n PUBLIC_GEO_PUBLIC_API_BASE_URL: geoPublicApiBaseUrl,\n PUBLIC_WEB_LIVE_URL: webLiveUrl\n },\n version\n });\n};\n","import cookie from 'cookie';\nimport { NATIVE_APP_ENVIRONMENT } from 'tradera-constants/cookies';\n\nconst NATIVE_APP_COOKIE_AND_HEADER_CONFIG = {\n osVersion: {\n header: 'x-tradera-app-os-version',\n initDataName: 'nativeAppOsVersion'\n },\n appDevice: {\n header: 'x-tradera-app-device',\n initDataName: 'nativeAppDevice'\n },\n appVersion: {\n header: 'x-tradera-app-version',\n initDataName: 'nativeAppVersion'\n },\n appContext: { header: 'x-tradera-app', initDataName: 'nativeAppContext' }\n};\n\nconst createCookie = (name, value) => {\n const options = {\n httpOnly: false,\n path: '/'\n };\n return cookie.serialize(name, String(value), options);\n};\n\nconst isString = s => typeof s === 'string';\n\nexport const createNativeAppCookieValue = nativeAppInfo => {\n const valueObject = Object.fromEntries(\n Object.entries(nativeAppInfo).filter(\n ([key, value]) =>\n key in NATIVE_APP_COOKIE_AND_HEADER_CONFIG && isString(value)\n )\n );\n return JSON.stringify(valueObject);\n};\n\nexport const createNativeAppCookie = nativeAppInfo => {\n return createCookie(\n NATIVE_APP_ENVIRONMENT,\n createNativeAppCookieValue(nativeAppInfo)\n );\n};\n\n/**\n * Gets info about the native app if in native app context.\n * The function also updates a cookie with the same information\n * to be used for later calls to this function if the headers are\n * not present.\n * \n * @param {*} req\n * @param {*} res\n * @returns An object with these properties: {\n osVersion,\n appDevice,\n appVersion,\n appContext\n }\n */\nexport const getAndUpdateNativeAppInfo = (req, res) => {\n const nativeAppInfoFromHeaders = extractNativeAppInfoFromHeaders(\n req.headers\n );\n\n const hasAppHeaders =\n Object.values(nativeAppInfoFromHeaders).filter(Boolean).length > 0;\n if (hasAppHeaders) {\n const cookie = createNativeAppCookie(nativeAppInfoFromHeaders);\n res.setHeader('Set-Cookie', cookie);\n return nativeAppInfoFromHeaders;\n }\n\n const nativeAppInfoFromCookie = extractNativeAppInfoFromCookie(\n req.cookies[NATIVE_APP_ENVIRONMENT]\n );\n return nativeAppInfoFromCookie;\n};\n\nexport const extractNativeAppInfoFromCookie = nativeAppCookieString =>\n nativeAppCookieString\n ? Object.fromEntries(\n Object.entries(\n JSON.parse(decodeURIComponent(nativeAppCookieString))\n ).filter(([key]) => key in NATIVE_APP_COOKIE_AND_HEADER_CONFIG)\n )\n : {};\nexport const extractNativeAppInfoFromHeaders = headers => {\n return Object.entries(NATIVE_APP_COOKIE_AND_HEADER_CONFIG).reduce(\n (obj, [key, config]) => {\n obj[key] = headers[config.header] || '';\n return obj;\n },\n {}\n );\n};\n\nexport const extractNativeAppInfoFromInitData = initData => {\n return Object.entries(NATIVE_APP_COOKIE_AND_HEADER_CONFIG).reduce(\n (obj, [key, config]) => {\n obj[key] = initData[config.initDataName] || '';\n return obj;\n },\n {}\n );\n};\n","import axios from 'axios';\nimport { selectPreferredLanguageCode } from 'tradera-state/language/selectors';\nimport { toLocalizedUrl } from 'tradera-utils/url';\nimport {\n initialize,\n setEnvironmentHash,\n setIsSpaNavigationEnabled\n} from './reducer';\nimport { isNextJs } from 'tradera-utils/nextjs';\nimport { selectMemberId } from 'tradera-state/member/selectors';\nimport getInitialState from './initial-state';\nimport {\n createNativeAppCookieValue,\n extractNativeAppInfoFromCookie,\n extractNativeAppInfoFromInitData\n} from './native-app-info-helper';\nimport cookie from 'tradera-utils/cookie';\nimport { NATIVE_APP_ENVIRONMENT } from 'tradera-constants/cookies';\n\nconst getVersionUrl = getState => {\n if (isNextJs) {\n const queryParameters = new URLSearchParams({ next: 1 });\n const memberId = selectMemberId(getState());\n if (memberId) {\n queryParameters.set('memberId', memberId);\n }\n return `/api/version?${queryParameters}`;\n }\n\n const preferredLanguageCode = selectPreferredLanguageCode(getState());\n\n return toLocalizedUrl('/ping', preferredLanguageCode);\n};\n\nexport const updateEnvironmentHash = () => async (dispatch, getState) => {\n const url = getVersionUrl(getState);\n const response = await axios.get(url);\n const environmentHash = response.headers['x-tradera-environment'];\n const current = getState().environment.environmentHash;\n if (environmentHash !== current) {\n dispatch(setEnvironmentHash(environmentHash));\n dispatch(setIsSpaNavigationEnabled(false));\n }\n};\n\nconst getAndUpdateNativeAppInfo = initData => {\n const nativeAppInfoFromInitData = extractNativeAppInfoFromInitData(\n initData\n );\n const hasAnyValidProperties =\n Object.values(nativeAppInfoFromInitData).filter(Boolean).length > 0;\n if (hasAnyValidProperties) {\n const nativeAppCookieValue = createNativeAppCookieValue(\n nativeAppInfoFromInitData\n );\n cookie.createCookie(NATIVE_APP_ENVIRONMENT, nativeAppCookieValue);\n return nativeAppInfoFromInitData;\n }\n\n const cookieValue = cookie.readCookie(NATIVE_APP_ENVIRONMENT);\n const nativeAppInfo = extractNativeAppInfoFromCookie(cookieValue);\n return nativeAppInfo;\n};\n\nexport const initEnvironment = initData => dispatch => {\n const {\n osVersion,\n appDevice,\n appVersion,\n appContext\n } = getAndUpdateNativeAppInfo(initData);\n const {\n environment,\n environmentHash,\n featureSwitches,\n geoPublicApiBaseUrl,\n isSinglePageApp,\n splitTests,\n splitTestGroups,\n version,\n webLiveUrl\n } = initData;\n const initialState = getInitialState({\n appOsVersion: osVersion,\n environment,\n environmentHash,\n featureSwitches,\n geoPublicApiBaseUrl,\n hybridAppDevice: appDevice,\n hybridAppVersion: appVersion,\n isNativeAppContext: appContext === 'Native',\n isHybridAppContext: Boolean(appContext),\n isSinglePageApp,\n splitTests,\n splitTestGroups,\n version,\n webLiveUrl\n });\n dispatch(initialize(initialState));\n};\n","/**\n * @returns initData as JSON;\n * Default\n */\nconst initData = function() {\n if (typeof window === 'undefined') {\n return {};\n } else if (!window.initData) {\n const initDataEL = document.getElementById('init-data');\n if (initDataEL) {\n const data = initDataEL.getAttribute('data-init-data');\n const parsed = JSON.parse(data);\n return parsed;\n } else {\n return null;\n }\n } else {\n return window.initData;\n }\n};\n\nexport const getInitData = initData;\nexport default new initData();\n","import { toLocalizedUrl } from 'tradera-utils/url';\nimport { isServer } from 'tradera-utils/nextjs';\n\nexport const toLocalizedRiotUrl = url => toLocalizedUrl(url, getLanguage());\n\nlet language;\nexport const getLanguage = () => {\n if (isServer) {\n return;\n }\n if (language === '' || language === null || language === undefined) {\n language = document.querySelector('html').getAttribute('lang');\n }\n\n return language;\n};\n","import { createSlice } from '@reduxjs/toolkit';\nexport const initialState = {\n featureSwitches: {},\n splitTestGroups: {},\n variables: {}\n};\n\nconst slice = createSlice({\n name: 'environment',\n initialState,\n reducers: {\n initialize: (_state, { payload }) => {\n return { ...payload };\n },\n setEnvironmentHash: (state, { payload }) => {\n state.environmentHash = payload;\n },\n setIsSpaNavigationEnabled: (state, { payload }) => {\n state.isSpaNavigationEnabled = payload;\n }\n }\n});\n\nexport const {\n initialize,\n setEnvironmentHash,\n setIsSpaNavigationEnabled\n} = slice.actions;\nexport const reducer = slice.reducer;\n","import { createSlice } from '@reduxjs/toolkit';\nimport cookieUtil from 'tradera-utils/cookie';\nimport { REGION_PICKER_DISMISSED } from 'tradera-constants/cookies';\n\nconst shippingRegionSlice = createSlice({\n name: 'shippingRegion',\n initialState: {},\n reducers: {\n initialize: (_state, { payload }) => {\n return { ...payload };\n },\n setShippingCountry: (state, action) => {\n const { countryCodeIso2 } = action.payload;\n state.shippingCountry = {\n countryCodeIso2: toUpperCase(countryCodeIso2)\n };\n },\n setDefaultShippingCountry: state => {\n // Remove name\n state.shippingCountry = { countryCodeIso2: 'SE' };\n },\n setAvailableShippingCountries: (state, action) => {\n state.availableShippingCountries = action.payload.shippingCountries;\n state.hasLoadedAvailableShippingCountries = true;\n state.isLoadingAvailableShippingCountries = false;\n },\n beginLoadingAvailableShippingOptions: state => {\n state.isLoadingAvailableShippingCountries = true;\n },\n dismissLocalizationRegionPicker: {\n reducer: state => {\n state.localizationRegionPickerEnabled = false;\n },\n prepare: () => {\n cookieUtil.createCookie(REGION_PICKER_DISMISSED, true);\n return {};\n }\n }\n }\n});\n\nconst toUpperCase = (countryCodeIso2 = '') => countryCodeIso2.toUpperCase();\n\nexport const {\n initialize,\n setShippingCountry,\n setAvailableShippingCountries,\n beginLoadingAvailableShippingOptions,\n dismissLocalizationRegionPicker,\n setDefaultShippingCountry\n} = shippingRegionSlice.actions;\nexport default shippingRegionSlice.reducer;\n","import axios from 'axios';\n\nimport { logger } from 'packages/index';\nimport ENDPOINTS from 'tradera-constants/endpoints';\n\n/**\n * Standardized api error codes\n */\nconst API_ERRORS = {\n IGNORE_ME: 'IGNORE_ME',\n ABORTED: 'ABORTED',\n CANCELLED: 'CANCELLED',\n NETWORK: 'NETWORK',\n TIMEOUT: 'TIMEOUT',\n VERSION_MISMATCH: 'VERSION_MISMATCH',\n LOGGED_OUT: 'LOGGED_OUT'\n};\n\nconst defaultJsonRequestHeaders = {\n // Force json response\n Accept: 'application/json',\n // For WebAPI and RequiresAuthorization to handle ajax request\n 'X-Requested-With': 'XMLHttpRequest'\n};\n\n/**\n * Standard Axios configs\n */\nconst axiosConfigs = {\n // Requests to endpoints with authentication\n authenticated: {\n // Send auth cookies\n withCredentials: true,\n headers: defaultJsonRequestHeaders\n },\n notAuthenticated: {\n // Dont send auth cookies\n withCredentials: false,\n headers: defaultJsonRequestHeaders\n }\n};\n\nconst isMissingAuthToken = responseString => {\n try {\n const responseObject = JSON.parse(responseString);\n const isMissing =\n responseObject.responseStatus.errorCode === 'MissingAuthToken';\n return isMissing;\n } catch {\n return false;\n }\n};\n\nconst isUnauthorized = statusCode => statusCode === 401;\n\nconst addAntiCacheParam = url => {\n const antiCacheRegExp = /([?&]_=)[^&]*/;\n return antiCacheRegExp.test(url)\n ? url.replace(antiCacheRegExp, '$1' + new Date().getTime())\n : url + (/\\?/.test(url) ? '&' : '?') + '_=' + new Date().getTime();\n};\n\nconst submitOriginalRequest = async error =>\n axios({\n ...error.config,\n url: addAntiCacheParam(error.config.url)\n });\n\nconst handleMissingAuthToken = async error => {\n const refreshAccessToken = async () =>\n axios.get(ENDPOINTS.TOUCHWEB_URL + 'login/state');\n const handleErrorAfterRefresh = async errorAfterRefreshToken => {\n if (isUnauthorized(errorAfterRefreshToken.request?.status)) {\n error.message = API_ERRORS.LOGGED_OUT;\n }\n return Promise.reject(error);\n };\n return refreshAccessToken()\n .then(() => submitOriginalRequest(error))\n .catch(handleErrorAfterRefresh);\n};\n\nconst handleUnauthorized = async error => {\n const checkLoginState = async () =>\n axios.get(ENDPOINTS.TOUCHWEB_URL + 'login/state');\n return checkLoginState().then(response => {\n const isLoggedIn = response.data?.isLoggedIn;\n if (isLoggedIn) {\n return submitOriginalRequest(error);\n }\n error.message = API_ERRORS.LOGGED_OUT;\n // eslint-disable-next-line promise/no-return-wrap\n return Promise.reject(error);\n });\n};\n\nconst errorResponseInterceptor = error => {\n if (isMissingAuthToken(error.request?.response)) {\n return handleMissingAuthToken(error);\n } else if (isUnauthorized(error.request?.status)) {\n return handleUnauthorized(error);\n }\n return Promise.reject(error);\n};\n\n/**\n * Create Axios instance or decorate existing instance with interceptor for expired token responses\n *\n * @param {AxiosInstance} [axiosInstance]\n * @returns {AxiosInstance}\n */\nfunction axiosWithTokenRefresh(axiosInstance) {\n let instance;\n if (axiosInstance === undefined) {\n instance = axios.create();\n } else {\n instance = axiosInstance;\n }\n instance.interceptors.response.use(\n response => response,\n errorResponseInterceptor\n );\n return instance;\n}\n\nconst cancelTokens = {};\n\n/**\n * Create new or return existing cancellation function\n *\n * Usage:\n * const { cancel, cancelToken } = utilizeCancelToken('tokenId')\n * if (cancel) cancel();\n * axios.get('http://api.com/...', { cancelToken });\n *\n * @param {string} tokenId - A string ID shared between requests that cancel together\n * @returns {{ cancel: function, cancelToken: function }}\n */\nfunction utilizeCancelToken(tokenId) {\n let cancel;\n if (tokenId in cancelTokens) {\n cancel = cancelTokens[tokenId].cancel;\n }\n cancelTokens[tokenId] = axios.CancelToken.source();\n return {\n cancel,\n cancelToken: cancelTokens[tokenId].token\n };\n}\n\n/**\n * Check response version and throw error if different from current\n *\n * @param {string} version\n * @return {function(*): object|void}\n */\nfunction checkResponseVersion(version = '12.0.0') {\n return response => {\n if (\n response.headers &&\n response.headers['X-Tradera-Version'] &&\n response.headers['X-Tradera-Version'] !== version\n ) {\n throw new Error(API_ERRORS.VERSION_MISMATCH);\n } else {\n return response;\n }\n };\n}\n\n/**\n * Checks redirect response for logged out user\n * This is an old solution. New solutions return a 401 response instead (see handleError).\n * Requires Axios config to include { withCredentials: true }\n *\n * @returns {Function}\n */\nfunction checkResponseLoggedOut() {\n return response => {\n if (\n response.data &&\n typeof response.data === 'string' &&\n response.data.includes('Logga in')\n ) {\n throw new Error(API_ERRORS.LOGGED_OUT);\n } else {\n return response;\n }\n };\n}\n\n/**\n * Final standardized formatting of an api response\n *\n * @returns { function(*): { data: *, status: integer }}\n */\nfunction finalizeResponse() {\n return response => {\n const { data, status } = response;\n return {\n data,\n status\n };\n };\n}\n\n/**\n * Common network request error handling\n *\n * @param {Object.} statusHandlers - { code: message }\n * @returns {function(*=): Promise}\n */\nfunction handleError(statusHandlers = {}) {\n return error => {\n let status = error.response && error.response.status;\n let message = error.message ? error.message : error.toString();\n if (axios.isCancel(error)) {\n error.message = API_ERRORS.CANCELLED;\n } else if (message.includes('timeout') || status === 408) {\n error.message = API_ERRORS.TIMEOUT;\n } else if (message.includes('Network')) {\n error.message = API_ERRORS.NETWORK;\n } else if (message.includes('Request aborted')) {\n error.message = API_ERRORS.ABORTED;\n } else if (status === 401) {\n error.message = API_ERRORS.LOGGED_OUT;\n } else if (status in statusHandlers) {\n error.message = statusHandlers[status];\n }\n throw error;\n };\n}\n\nfunction reloadOnUnauthorized(error) {\n switch (error.message) {\n case API_ERRORS.LOGGED_OUT:\n window.location.reload();\n break;\n default:\n throw error;\n }\n}\n\nfunction logError(error) {\n switch (error.message) {\n case API_ERRORS.IGNORE_ME:\n case API_ERRORS.CANCELLED:\n case API_ERRORS.NETWORK:\n case API_ERRORS.TIMEOUT:\n break;\n default:\n logger(error);\n break;\n }\n}\n\nexport {\n addAntiCacheParam,\n API_ERRORS,\n axiosConfigs,\n axiosWithTokenRefresh,\n checkResponseVersion,\n checkResponseLoggedOut,\n defaultJsonRequestHeaders,\n errorResponseInterceptor,\n finalizeResponse,\n handleError,\n logError,\n reloadOnUnauthorized,\n utilizeCancelToken\n};\n","export default {\n DEFAULT_ACTION: 'member/default',\n INITIALIZE: 'MEMBER/INITIALIZE',\n SET_IS_FETCHING_GEOLOCATION: 'MEMBER/SET_IS_FETCHING_GEOLOCATION',\n SET_GEOLOCATION: 'MEMBER/SET_GEOLOCATION',\n FAILED_LOADING_GEOLOCATION: 'MEMBER/FAILED_LOADING_GEOLOCATION',\n SET_MEMBER_HERO_IMAGE: 'MEMBER/SET_MEMBER_HERO_IMAGE'\n};\n","import { selectFeatureSwitches } from 'tradera-state/environment/selectors';\nimport { selectShippingRegionCountryCodeIso2 } from 'tradera-state/shipping-region/selectors';\nimport { areCountryCodesIso2Equal } from 'tradera-utils/countrycode';\n\nexport const selectIsLoggedIn = state => state.member?.isLoggedIn;\n\nexport const selectGeolocation = state => state.member?.geolocation;\n\nexport const selectMemberId = state => state.member?.memberId;\n\nexport const selectMemberCountryCodeIso2 = state =>\n state.member?.memberCountryCodeIso2;\n\nexport const selectIsOutsideSweden = state =>\n state.member?.geolocation?.isoCode &&\n state.member?.geolocation?.isoCode.toLowerCase() !== 'se';\n\nexport const showDanishFromCountry = state => {\n const fromCountry =\n selectShippingRegionCountryCodeIso2(state) ||\n selectMemberCountryCodeIso2(state);\n return (\n selectFeatureSwitches(state)['shipping-from-country'] &&\n areCountryCodesIso2Equal(fromCountry, 'DK')\n );\n};\n","import axios from 'axios';\nimport {\n utilizeCancelToken,\n finalizeResponse,\n handleError\n} from 'tradera-utils/api';\nimport cookieUtil from 'tradera-utils/cookie';\nimport { FORCE_GEO_DEV } from 'tradera-constants/cookies';\nimport { isServer } from 'tradera-utils/nextjs';\n\nexport const fetchGeolocation = geoPublicBaseUrl => {\n if (isServer) {\n return Promise.reject(\n 'Cannot fetch geo information from server as that would give the wrong result.'\n );\n }\n\n const { cancel, cancelToken } = utilizeCancelToken('fetchGeolocation');\n\n if (cancel) cancel();\n\n const search = (window && window.location && window.location.search) || '';\n return axios\n .get(getGeoPublicApiLocationUrl(geoPublicBaseUrl, search), {\n cancelToken\n })\n .then(finalizeResponse())\n .catch(handleError());\n};\n\nexport const getGeoPublicApiLocationUrl = (geoPublicBaseUrl, search) => {\n const originalUrl = `${geoPublicBaseUrl}/api/country`;\n const forceGeoDevMatch = search.match(/force-geo-(.*?)-dev/);\n\n if (forceGeoDevMatch) {\n cookieUtil.createCookie(FORCE_GEO_DEV, forceGeoDevMatch[1]);\n return `${originalUrl}/${forceGeoDevMatch[1]}`;\n }\n\n const forceGeoDevCookie = cookieUtil.readCookie(FORCE_GEO_DEV);\n if (forceGeoDevCookie) {\n return `${originalUrl}/${forceGeoDevCookie}`;\n }\n\n return originalUrl;\n};\n","import { nullifyUndefinedProperties } from 'tradera-utils/object';\n\nexport default initData => {\n const {\n cleanedMemberAlias,\n facebookId,\n isLoggedIn,\n memberAlias,\n memberDetailedSellerRatingAverage,\n memberCountry,\n memberCountryCodeIso2,\n memberEmail,\n memberEmailMd5,\n memberEmailSha256,\n memberFirstName,\n memberId,\n memberHeroImage,\n memberLastName,\n memberBirthdate,\n paymentSettings = {}\n } = initData;\n return nullifyUndefinedProperties({\n cleanedMemberAlias,\n facebookId,\n geolocation: null,\n isLoggedIn,\n memberAlias,\n memberDetailedSellerRatingAverage,\n memberCountry,\n memberCountryCodeIso2,\n memberEmail,\n memberEmailMd5,\n memberEmailSha256,\n memberFirstName,\n memberId,\n memberHeroImage,\n memberLastName,\n memberBirthdate,\n paymentSettings\n });\n};\n","import actionTypes from './action-types';\nimport { fetchGeolocation } from './api';\nimport { setCurrencyIfNotChosen } from 'tradera-state/multi-currency/actions';\nimport { removeShippingCountryCookieIfSwedish } from 'tradera-state/shipping-region/actions';\nimport { API_ERRORS } from 'tradera-utils/api';\nimport { logger } from 'packages/index';\nimport getInitialState from './initial-state';\n\nexport const initialize = initData => ({\n type: actionTypes.INITIALIZE,\n payload: getInitialState(initData)\n});\n\nexport const initializeServerMember = member => ({\n type: actionTypes.INITIALIZE,\n payload: member\n});\n\nexport const defaultAction = data => ({\n type: actionTypes.DEFAULT_ACTION,\n payload: data\n});\n\nconst setIsFetchingGeolocation = isFetchingGeolocation => ({\n type: actionTypes.SET_IS_FETCHING_GEOLOCATION,\n payload: {\n isFetchingGeolocation\n }\n});\n\nconst setGeolocation = payload => ({\n type: actionTypes.SET_GEOLOCATION,\n payload\n});\n\nconst failedLoadingGeolocation = () => ({\n type: actionTypes.FAILED_LOADING_GEOLOCATION\n});\n\nexport const setMemberHeroImage = payload => ({\n type: actionTypes.SET_MEMBER_HERO_IMAGE,\n payload\n});\n\nexport const getGeolocation = () => (dispatch, getState) => {\n dispatch(setIsFetchingGeolocation(true));\n const { PUBLIC_GEO_PUBLIC_API_BASE_URL } = getState().environment.variables;\n return fetchGeolocation(PUBLIC_GEO_PUBLIC_API_BASE_URL)\n .then(response => {\n const { data } = response;\n dispatch(setGeolocation(data));\n dispatch(setCurrencyIfNotChosen(data.currency));\n dispatch(setIsFetchingGeolocation(false));\n dispatch(removeShippingCountryCookieIfSwedish(data));\n return data;\n })\n .catch(error => {\n switch (error.message) {\n case API_ERRORS.ABORTED:\n case API_ERRORS.CANCELLED:\n case API_ERRORS.NETWORK:\n break;\n default:\n logger(error);\n break;\n }\n dispatch(failedLoadingGeolocation());\n });\n};\n","export const LIST_VIEW_TYPES = {\n BASIC: 'Basic',\n NORMAL: 'Normal',\n PICK_LIST: 'PickList'\n};\n\nexport const PAGE_LIST_TYPES = {\n BUYER_ACTIVE_ITEMS: 'BUYER_ACTIVE_ITEMS',\n BUYER_PURCHASES: 'BUYER_PURCHASES',\n BUYER_ITEMS_LOST: 'BUYER_ITEMS_LOST',\n SELLER_ACTIVE: 'SELLER_ACTIVE',\n SELLER_SOLD: 'SELLER_SOLD',\n SELLER_NOT_SOLD: 'SELLER_NOT_SOLD'\n};\n\nexport const PAGE_BULK_ACTIONS = {\n ACTIVE_CANCEL: 'ActiveItems_Cancel',\n UNSOLD_RESTART: 'UnsoldItems_Restart',\n BUYER_MARK_PAID: 'Buyer_MarkPaid',\n BUYER_SHOW_ACTIVE: 'ShowActive',\n BUYER_HIDE_ACTIVE: 'HideActive',\n PURCHASES_SHOW: 'PurchasesShow'\n};\n","export const isSwedenCountryCodeOrUndefined = countryCodeIso2 =>\n countryCodeIso2 === undefined ||\n countryCodeIso2 === null ||\n countryCodeIso2.toLowerCase() === 'se';\n\nexport const isSwedenCountryNameOrUndefined = countryName =>\n countryName === undefined ||\n countryName === null ||\n countryName.toLowerCase() === 'sweden';\n\nexport const isDenmarkCountryCode = countryCodeIso2 =>\n !isSwedenCountryCodeOrUndefined(countryCodeIso2) &&\n countryCodeIso2.toLowerCase() === 'dk';\n\nexport const areCountryCodesIso2Equal = (first, second) =>\n (first || 'SE').toUpperCase() === (second || 'SE').toUpperCase();\n","import {\n CATEGORIZED_COOKIES,\n COOKIE_DEFAULT_CATEGORY\n} from 'tradera-constants/cookies';\nimport { GdprSettings } from 'tradera-utils/gdpr-settings';\nimport logger from 'packages/logger';\nimport { isServer } from 'tradera-utils/nextjs';\nimport { getCookieFromBrowser, setCookieToBrowser } from './cookie-helpers';\n\nexport class CookieUtil {\n constructor(getCookie, setCookie) {\n if (!getCookie || !setCookie) {\n throw new Error('You must provide getCookie and setCookie');\n }\n this.getCookie = getCookie;\n this.setCookie = setCookie;\n this.gdpr = new GdprSettings(getCookie, setCookie);\n }\n\n segment(name, value) {\n return value ? '; ' + name + '=' + value : '';\n }\n\n convertToExpiresStr(expires) {\n let expiresStr = '';\n\n switch (expires.constructor) {\n case Number:\n expiresStr =\n expires === Infinity\n ? '; expires=Fri, 31 Dec 9999 23:59:59 GMT'\n : '; max-age=' + expires * 24 * 60 * 60;\n break;\n case String:\n expiresStr = '; expires=' + expires;\n break;\n case Date:\n expiresStr = '; expires=' + expires.toUTCString();\n break;\n }\n\n return expiresStr;\n }\n\n createCookie(cookieKey, cookieValue, expires, path, domain, secure) {\n let expiresStr = '';\n\n if (\n !cookieKey ||\n /^(?:expires|max-age|path|domain|secure)$/i.test(cookieKey)\n ) {\n return false;\n }\n\n // check gdpr for cookie category\n let category = CATEGORIZED_COOKIES[cookieKey];\n if (typeof category === 'undefined') {\n logger(`No category set for cookie ${cookieKey}`);\n category = COOKIE_DEFAULT_CATEGORY;\n }\n if (!this.gdpr.isCategoryEnabled(category)) {\n // console.info(\n // `Cookie category not enabled: ${cookieKey}, category: ${category}`\n // );\n return false;\n }\n\n // if expired set prepare date string\n if (expires) {\n expiresStr = this.convertToExpiresStr(expires);\n }\n\n if (isServer) {\n throw new Error('Setting cookie is not supported on the server');\n }\n\n this.setCookie(\n encodeURIComponent(cookieKey) +\n '=' +\n encodeURIComponent(cookieValue) +\n expiresStr +\n this.segment('domain', domain) +\n this.segment('path', path || '/') +\n (location.protocol == 'https:' || secure ? '; secure' : '')\n );\n\n return true;\n }\n\n readCookie(key) {\n return this.getCookie(key);\n }\n\n hasCookie(key) {\n return typeof this.readCookie(key) === 'string';\n }\n\n eraseCookie(key, path, domain) {\n if (!key || !this.hasCookie(key)) {\n return false;\n }\n\n if (isServer) {\n throw new Error('Setting cookie is not supported on the server');\n }\n\n this.setCookie(\n encodeURIComponent(key) +\n '=; expires=Thu, 01 Jan 1970 00:00:01 GMT' +\n this.segment('domain', domain) +\n this.segment('path', path || '/')\n );\n\n return true;\n }\n}\n\nexport default new CookieUtil(getCookieFromBrowser, setCookieToBrowser);\n","import actionTypes from './action-types';\n\nexport default (state = { isLoggedIn: false }, action) => {\n switch (action.type) {\n case actionTypes.INITIALIZE:\n return {\n ...action.payload\n };\n case actionTypes.DEFAULT_ACTION:\n return {\n ...state,\n default: action.payload\n };\n case actionTypes.SET_IS_FETCHING_GEOLOCATION: {\n const { payload } = action;\n return {\n ...state,\n isFetchingGeolocation: payload.isFetchingGeolocation\n };\n }\n case actionTypes.SET_GEOLOCATION: {\n const { payload } = action;\n return {\n ...state,\n geolocation: payload\n };\n }\n case actionTypes.FAILED_LOADING_GEOLOCATION: {\n return {\n ...state,\n geolocation: null,\n isFetchingGeolocation: false\n };\n }\n case actionTypes.SET_MEMBER_HERO_IMAGE: {\n return {\n ...state,\n memberHeroImage: {\n max: action.payload,\n med: action.payload,\n min: action.payload\n }\n };\n }\n default:\n return state;\n }\n};\n","import mapObject from 'underscore/modules/mapObject';\nimport isUndefined from 'underscore/modules/isUndefined';\n\nexport const nullifyUndefinedProperties = obj =>\n mapObject(obj, value => (isUndefined(value) ? null : value));\n","import { Analytics } from 'packages';\nimport cookieUtil from 'tradera-utils/cookie';\nimport { setPreferredCurrency } from './reducer';\nimport { selectIsLoggedIn } from 'tradera-state/member/selectors';\nimport { touchWebClient } from 'tradera-utils/http';\nimport { selectPreferredLanguage } from 'tradera-state/language/selectors';\n\nexport const setPreferredCurrencyThunk = currencyCode => async (\n dispatch,\n getState\n) => {\n Analytics.trackEvent({\n category: 'Settings',\n action: 'Currency',\n label: currencyCode\n });\n\n if (selectIsLoggedIn(getState())) {\n await touchWebClient.post('my/profile/save-currency-setting', {\n currencyCode\n });\n }\n\n createPreferredCurrencyCookie(currencyCode);\n dispatch(setPreferredCurrency({ currencyCode }));\n};\n\nexport const setCurrencyIfNotChosen = currencyCode => (dispatch, getState) => {\n if (\n cookieUtil.readCookie('preferred_currency') ||\n selectIsLoggedIn(getState())\n ) {\n return;\n }\n\n const { languageCodeIso2 } = selectPreferredLanguage(getState());\n\n const map = {\n sv: 'SEK',\n da: 'DKK',\n de: 'EUR'\n };\n\n if (Object.keys(map).includes(languageCodeIso2)) {\n dispatch(setPreferredCurrencyThunk(map[languageCodeIso2]));\n } else {\n dispatch(setPreferredCurrencyThunk(currencyCode));\n }\n};\n\nconst yearInMilliseconds = 1000 * 60 * 60 * 24 * 365;\nconst createPreferredCurrencyCookie = currencyCode => {\n cookieUtil.createCookie(\n 'preferred_currency',\n currencyCode,\n new Date(new Date().getTime() + yearInMilliseconds)\n );\n};\n","import React, { useCallback } from 'react';\nimport PropTypes from 'prop-types';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { Link } from 'react-router-dom';\nimport NextLink from 'next/link';\nimport { updateEnvironmentHash } from 'tradera-state/environment/actions';\nimport { useIsFeatureEnabled } from 'tradera-hooks/use-is-feature-enabled';\nimport { isNextJs } from 'tradera-utils/nextjs';\n\nconst isVipLink = href => href.startsWith('/item/');\n\nconst isLinkToNextWebPage = href =>\n href.endsWith('?next=1') ||\n href.endsWith('/how-to-buy') ||\n href.endsWith('/how-to-sell');\n\nconst ALink = ({\n useLink,\n href: givenHref,\n children,\n onClick: givenOnClick,\n ...otherProps\n}) => {\n const dispatch = useDispatch();\n const isSpaNavigationEnabled = useSelector(\n state => state.environment.isSpaNavigationEnabled\n );\n const isNextWebVipSpaNavigationEnabled = useIsFeatureEnabled(\n 'next-web-vip-spa-navigation'\n );\n const href =\n isNextWebVipSpaNavigationEnabled && isVipLink(givenHref)\n ? `${givenHref}?next=1`\n : givenHref;\n\n const handleSpaNavigationClick = useCallback(\n evt => {\n dispatch(updateEnvironmentHash());\n if (givenOnClick) givenOnClick(evt);\n },\n [dispatch, givenOnClick]\n );\n if (isNextJs && isSpaNavigationEnabled && isLinkToNextWebPage(href)) {\n // NextJS routing\n return (\n \n e.stopPropagation()}>\n {children}\n \n \n );\n } else if (!isNextJs && isSpaNavigationEnabled && useLink) {\n // react-router-dom\n // Strip away origin for local links\n let to = href;\n if (href?.startsWith(location.origin)) {\n const { pathname, search } = new URL(href);\n to = pathname + search;\n }\n return (\n \n {children}\n \n );\n }\n return (\n \n {children}\n \n );\n};\n\nALink.propTypes = {\n useLink: PropTypes.bool,\n href: PropTypes.string.isRequired,\n children: PropTypes.node.isRequired,\n onClick: PropTypes.func\n};\n\nALink.defaultProps = {\n useLink: false,\n onClick: null\n};\n\nexport default ALink;\n","export const isDev = () =>\n process && process.env && process.env.NODE_ENV !== 'production';\n\nexport const warnIfDevBuild = moduleName => {\n if (isDev()) {\n console.log(\n `%c Warning: This is a Dev build of ${moduleName}`,\n 'background:red; color:white; font-size:18px;'\n );\n }\n};\n\n/**\n * helper function that is called after the header has been loaded. useful when you want to have the redux loaded before you start\n * to dispatch actions.\n * @param {func} callback\n */\nexport const onModuleLoaded = callback => {\n const isModuleLoaded = () =>\n window.document.body.getAttribute('data-module-loaded') === 'true';\n\n try {\n if (isModuleLoaded()) {\n callback();\n }\n let observer = new MutationObserver(mutationsList => {\n const element = mutationsList.filter(\n item => item.attributeName === 'data-module-loaded'\n );\n if (element !== null && isModuleLoaded()) {\n callback();\n }\n });\n observer.observe(window.document.body, { attributes: true });\n // eslint-disable-next-line no-empty\n } catch (error) {}\n};\n","import { isDev } from 'static/script/utils/environment';\n\nexport const buildInitialGtmDataLayerFromInitData = initData => {\n // ***** ATTENTION !!! *****\n // If you change anything here you must\n // also change in buildInitialGtmDataLayerFromState\n // so that they match. Otherwise values\n // between Touchweb and NextWeb don't match.\n const initialDataLayer = {\n memberId: initData.memberId || 0,\n userLanguage: initData.languageCodeIso2,\n memberEmail: initData.isLoggedIn ? initData.memberEmail : '',\n memberHashedEmail: initData.isLoggedIn\n ? initData.memberEmailSha256\n : '',\n 'criteo.hashedEmail': initData.isLoggedIn\n ? initData.memberEmailMd5\n : '',\n memberFirstName: initData.isLoggedIn ? initData.memberFirstName : '',\n memberLastName: initData.isLoggedIn ? initData.memberLastName : '',\n memberCountry: initData.isLoggedIn ? initData.memberCountry : '',\n loginState: initData.isLoggedIn ? 'logged in' : 'not logged in',\n isNotInIframe: window.self === window.top,\n 'blueshift.event-api-key': initData.blueshiftEventApiKey,\n isSinglePageApp: initData.isSinglePageApp,\n isNativeAppContext: initData.isNativeAppContext,\n isQuantcastConsentEnabled:\n initData.featureSwitches['quantcast-consent'],\n isDigitalAudienceTrackingEnabled:\n initData.featureSwitches['digital-audience-tracking'],\n quantcastSite: initData.quantcastSite\n };\n // add split tests to datalayer\n if (initData.splitTestGroups) {\n for (let [key, value] of Object.entries(initData.splitTestGroups)) {\n initialDataLayer[`splittest_${key}`] = value;\n }\n }\n return initialDataLayer;\n};\n\nexport const buildInitialGtmDataLayerFromState = state => {\n const quantcastSite = isDev() ? 'beta.tradera.com' : 'www.tradera.com';\n const { environment, language, member } = state;\n const { featureSwitches } = environment;\n const isQuantcastConsentEnabled = featureSwitches['quantcast-consent'];\n const isDigitalAudienceTrackingEnabled =\n featureSwitches['digital-audience-tracking'];\n const splitTestGroups = Object.entries(environment.splitTestGroups).reduce(\n (groups, [key, value]) => {\n return {\n ...groups,\n [`splittest_${key}`]: value\n };\n },\n {}\n );\n // ***** ATTENTION !!! *****\n // If you change anything here you must\n // also change in buildInitialGtmDataLayerFromInitData\n // so that they match. Otherwise values\n // between Touchweb and NextWeb don't match.\n return {\n memberId: member.memberId || 0,\n userLanguage: language.preferred.languageCodeIso2,\n memberEmail: member.memberEmail || '',\n memberHashedEmail: member.memberEmailSha256 || '',\n 'criteo.hashedEmail': member.memberEmailMd5 || '',\n memberFirstName: member.memberFirstName || '',\n memberLastName: member.memberLastName || '',\n memberCountry: member.memberCountry || '',\n loginState: member.isLoggedIn ? 'logged in' : 'not logged in',\n isNotInIframe: window.self === window.top,\n 'blueshift.event-api-key':\n process.env.NEXT_PUBLIC_BLUESHIFT_EVENT_API_KEY,\n isSinglePageApp: true,\n isNativeAppContext: environment.isNativeAppContext,\n isQuantcastConsentEnabled,\n isDigitalAudienceTrackingEnabled,\n quantcastSite,\n ...splitTestGroups\n };\n};\n","export const isPrerender = userAgent =>\n userAgent.indexOf('Prerender (+https://github.com/prerender/prerender)') >\n -1;\n","/**\n OBSOLETE - use google tag manager service instead:\n src\\EbaySweden.TouchWeb\\static\\script\\app\\ui\\google-tagmanager-service.js\n\n\n * Ported from Applications/TouchWeb/src/EbaySweden.TouchWeb/static/script/app/ui/layout/google-tagmanager.js\n * Best way to import this module is `import * as Analytics from 'analytics'`\n */\n\nimport * as Sentry from '@sentry/react';\n\nexport const pushToDataLayer = payload => {\n window.dataLayer = window.dataLayer || [];\n window.dataLayer.push(payload);\n};\nexport const trackPageView = () =>\n window.ga\n ? ga('send', 'pageview', location.pathname)\n : pushToDataLayer({ event: 'trackPageview' });\nexport const isNonInteractive = analyticsData =>\n analyticsData.userTriggered === undefined\n ? true\n : !analyticsData.userTriggered;\n/**\n * trackTiming\n * @param {*} category\n * @param {*} variable\n * @param {*} value\n * @param {*} label\n */\nexport const trackTiming = (category, variable, value, label = '') =>\n window.ga ? ga('send', 'timing', category, variable, value, label) : false;\n\n/**\n * Track Analytics Event\n */\nexport const trackEvent = analyticsData => {\n if (!analyticsData) {\n return;\n }\n pushToDataLayer({\n event: 'trackEvent',\n eventCategory: analyticsData.category || '',\n eventAction: analyticsData.action || '',\n eventLabel: analyticsData.label || '',\n eventValue: analyticsData.value || '0',\n eventNonInteractive: isNonInteractive(analyticsData),\n hitCallback: analyticsData.hitCallback || []\n });\n\n Sentry.addBreadcrumb({\n type: 'default',\n level: Sentry.Severity.Info,\n category: isNonInteractive(analyticsData) ? 'tracking' : 'ui-action',\n message: 'Analytics Event',\n data: analyticsData\n });\n};\n\n// Export object for Sinon stubbing\nexport default {\n pushToDataLayer,\n trackEvent,\n trackPageView,\n trackTiming\n};\n","import { createSlice } from '@reduxjs/toolkit';\n\nconst resolveCurrency = (enabled, preferredCurrencyCookieValue, currencies) => {\n if (!enabled || !preferredCurrencyCookieValue) {\n return null;\n }\n return (\n currencies.find(\n currency => currency.code === preferredCurrencyCookieValue\n ) || null\n );\n};\n\nconst multiCurrencySlice = createSlice({\n name: 'multiCurrency',\n initialState: {},\n reducers: {\n initialize: (_state, { payload }) => {\n const { currencies = [], enabled, preferredCurrency } = payload;\n\n return {\n enabled,\n currencies,\n preferredCurrency: resolveCurrency(\n enabled,\n preferredCurrency,\n currencies\n )\n };\n },\n setPreferredCurrency: (state, { payload }) => {\n const { enabled, currencies } = state;\n\n return {\n ...state,\n preferredCurrency: resolveCurrency(\n enabled,\n payload.currencyCode,\n currencies\n )\n };\n }\n }\n});\n\nexport const { initialize, setPreferredCurrency } = multiCurrencySlice.actions;\nexport default multiCurrencySlice.reducer;\n","/**\n *\n * # track events:\n * import GtmService from 'static/script/app/ui/google-tagmanager-service';\n * GtmService.trackGtmEvent(\"zorro\", { marvel: false, black: true })\n *\n *\n *\n */\n\nimport * as Sentry from '@sentry/react';\nimport initData from 'init-data';\nimport { isServer, isNextJs } from 'tradera-utils/nextjs';\nimport { buildInitialGtmDataLayerFromInitData } from './google-tagmanager-helper';\n\n/**\n * SPA page view tracking becomes enabled after first\n * page view tracking which happens after the first\n * page is loaded from server.\n * This prevents reset of data layer after a SPA route has\n * changed (happens late after full page load) and prevents\n * duplicated trackPageView events.\n */\nlet spaPageViewTrackingEnabled = false;\n\nclass GoogleTagManagerService {\n constructor() {\n this.isScriptLoaded = false;\n }\n\n loadGtmScript() {\n if (!isNextJs) {\n this._newPageFromServer();\n }\n const accountId =\n process?.env.NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ACCOUNT_ID ||\n 'GTM-5TMB2D';\n (function(w, d, s, l, i) {\n w[l] = w[l] || [];\n w[l].push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' });\n var f = d.getElementsByTagName(s)[0],\n j = d.createElement(s),\n dl = l != 'dataLayer' ? '&l=' + l : '';\n j.async = true;\n j.src = '//www.googletagmanager.com/gtm.js?id=' + i + dl;\n f.parentNode.insertBefore(j, f);\n })(window, document, 'script', 'dataLayer', accountId);\n this.isScriptLoaded = true;\n }\n\n push(payload) {\n if (isServer) {\n return;\n }\n window.dataLayer = window.dataLayer || [];\n window.dataLayer.push(payload);\n }\n\n _getDatalayerObject() {\n let output = {};\n if (window.dataLayer) {\n for (let entry of window.dataLayer) {\n output = { ...output, ...entry };\n }\n }\n return output;\n }\n\n /**\n * destroy datalayer from previous loading. this is really only useful in spa pages, it's backwards compatible so it doesnt do any harm on old pages\n */\n _reset() {\n if (!window.dataLayer) {\n return;\n }\n\n let data = this._getDatalayerObject();\n for (let key of Object.keys(data)) {\n data[key] = undefined;\n }\n window.dataLayer.push({ ...data, event: 'reset' });\n }\n\n _hasPreviouslyTrackedPage = () =>\n !!window.dataLayer?.find(item => item.event === 'trackPageview');\n\n newPage(pageType, initialDataLayer) {\n if (this._hasPreviouslyTrackedPage()) {\n this._reset();\n }\n this._pushInitialDataLayer(initialDataLayer);\n // It is important that the \"Container loaded\" triggered by dataLayer.push({event: \"gtm.js\", ...})\n // happens after \"initialDataLayer\" otherwise variables for the GDPR banner may have the wrong\n // values in Tagmanager. For that reason the GTM script is loaded here after initialDatalayer is set.\n if (!this.isScriptLoaded) {\n this.loadGtmScript();\n }\n this.push({ event: 'pageType', 'page.pageType': pageType });\n }\n\n /**\n * Same as newPage but with logic for case in Touchweb where initial\n * datalayer was set on server and already tracked on landing page.\n * This function does nothing until after first call to trackPageView\n * (e.g. after a SPA-navigation away from the landing page).\n * @param {*} pageType\n * @param {*} initialDataLayer\n * @returns\n */\n newSpaPage(pageType, initialDataLayer) {\n if (!spaPageViewTrackingEnabled) {\n return;\n }\n this.newPage(pageType, initialDataLayer);\n }\n\n _newPageFromServer() {\n const initialDataLayer = buildInitialGtmDataLayerFromInitData(initData);\n this._pushInitialDataLayer(initialDataLayer);\n // ------------------------------------------------\n // backwards compatibility\n // get old datalayer info and push it in\n if (window.legacyDataLayer) {\n for (let entry of window.legacyDataLayer) {\n window.dataLayer.push({\n event: 'legacyDataLayer',\n ...entry\n });\n }\n }\n }\n\n _pushInitialDataLayer(initialDataLayer) {\n window.dataLayer = window.dataLayer || [];\n window.dataLayer.push({\n ...initialDataLayer,\n event: 'initialDataLayer'\n });\n }\n\n /**\n * tracks a google analytics event\n * @param {string} category ga category\n * @param {string} action ga action what happens, ie: \"Filter box - open/close\"\n * @param {string} label ga label what the value of the action was, ie \"close\"\n * @param {integer} [value] ga interger value of the action.\n */\n trackAction(category, action, label, value = 0, nonInteractive = false) {\n this.push({\n event: 'trackEvent',\n eventCategory: category || '',\n eventAction: action || '',\n eventLabel: label || '',\n eventValue: value || '0',\n eventNonInteractive: nonInteractive\n });\n\n Sentry.addBreadcrumb({\n type: 'default',\n level: Sentry.Severity.Info,\n category: nonInteractive ? 'tracking' : 'ui-action',\n message: 'Analytics Event',\n data: {\n category,\n action,\n label,\n value\n }\n });\n }\n\n /**\n * google tag manager event. Note: if you want to send google analytics events use trackAction instead\n * @param {string} eventName\n * @param {object} [event] data\n */\n trackGtmEvent(eventName, data = {}) {\n this.push({\n event: eventName,\n ...data\n });\n }\n\n /**\n * Track a pageview. Use this script in SPA's to make sure that analytics gets pageview information.\n * This creates a virtual page view since google analytics by default doesn't track changes to url via\n * push state.\n */\n trackPageView() {\n this.trackGtmEvent('trackPageview');\n spaPageViewTrackingEnabled = true;\n }\n\n /**\n *\n * @param {string} category\n * @param {string} action\n * @param {string} label\n * @param {string} callback\n * @param {} value\n */\n trackLinkClickAndCallback(category, action, label, callback, value = 0) {\n this.push({\n event: 'trackEvent',\n eventCategory: category || '',\n eventAction: action || '',\n eventLabel: label || '',\n eventValue: value || '0',\n eventNonInteractive: false,\n eventCallback: callback // part of google tag manager api to execute code after all tags in datalayer have been executed\n });\n }\n\n trackLinkClickAndGotoUrl(category, action, label, url, value = 0) {\n const callback = () => {\n location.href = url;\n };\n this.trackLinkClickAndCallback(\n category,\n action,\n label,\n callback,\n value\n );\n }\n}\n\nexport default new GoogleTagManagerService();\n"],"sourceRoot":""}