diff --git a/library/packet/countrySheet.js b/library/packet/countrySheet.js index 4d05f8c51..95bff38a4 100644 --- a/library/packet/countrySheet.js +++ b/library/packet/countrySheet.js @@ -92,12 +92,21 @@ const largeOf = (item) => (item && LARGE_SHEETS[String(item.iso3 || '').toUpperC */ // WHAT of a whole country is withheld: its POSTER, and/or its ROWS (its prayer list, packet and cards). // India's whole-country poster is withheld EVERYWHERE BY DEFAULT (it is not released yet). It prints only in an -// explicit PREVIEW context: the Vite DEV server (the pilot's Plugin Studio, where it is reviewed), whose element -// bootstrap sets globalThis.DXM_PREVIEW_UNRELEASED. The read is gated on import.meta.env.DEV too, so every BUILT bundle -// compiles it away: no page, embed or console can set a global and print it from production. Never by a URL -// parameter. Its rows (about a thousand pages) stay withheld everywhere. +// explicit PREVIEW context, which needs ALL of: +// - the Vite DEV server (the pilot), whose element bootstrap sets globalThis.DXM_PREVIEW_UNRELEASED; +// - an explicit OPT-IN on that page: ?preview=india in its URL, or localStorage 'dxm-preview' containing 'india'. +// The pilot runs in dev mode, so dev mode alone must NOT show it (the pilot shows what the release shows). +// The whole read is gated on import.meta.env.DEV first, so every BUILT bundle compiles it away (the opt-in included): +// no page, URL, embed or console can print it from production. Its rows (about a thousand pages) stay withheld +// everywhere. export const WITHHELD = { IND: { poster: true, rows: true, preview: true } } -export const previewUnreleased = () => Boolean(import.meta.env && import.meta.env.DEV && globalThis.DXM_PREVIEW_UNRELEASED === true) +export const PREVIEW_KEY = 'india' +export function previewOptedIn() { + const has = (v) => String(v || '').split(',').map((x) => x.trim()).includes(PREVIEW_KEY) + try { if (new URLSearchParams(globalThis.location?.search || '').getAll('preview').some(has)) return true } catch (_) { /* no URL */ } + try { return has(globalThis.localStorage?.getItem('dxm-preview')) } catch (_) { return false } +} +export const previewUnreleased = () => Boolean(import.meta.env && import.meta.env.DEV && globalThis.DXM_PREVIEW_UNRELEASED === true && previewOptedIn()) const withheldOf = (item) => (item && !item.indiaState && WITHHELD[String(item.iso3 || '').toUpperCase()]) || null // No caller can open it: the preview state is read HERE, never passed in (so a built bundle cannot print it). export function posterWithheld(item) { const w = withheldOf(item); return Boolean(w?.poster) && !(w.preview && previewUnreleased()) } diff --git a/plugin-designs/country-posters/country-posters-mfe.vue b/plugin-designs/country-posters/country-posters-mfe.vue index 88bc2dd64..3d17c37c1 100644 --- a/plugin-designs/country-posters/country-posters-mfe.vue +++ b/plugin-designs/country-posters/country-posters-mfe.vue @@ -35,6 +35,7 @@ import { buildPaneColumn, PANE_GRID_STYLES } from '@map/layouts/pane-wrapper/pan import { useShadowStyles } from '@map/composables/useShadowStyles.js' import ViewSelect from '@map/components/ViewSelect.vue' import { countryName } from '@map/utils/countryName.js' +import { previewOptedIn } from '@map/packet/countrySheet.js' import { getRuntimeLocale, detectLocale } from '@map/i18n/index.js' import ShareButton from '@map/components/map-controls/ShareButton.vue' import { useI18n } from 'vue-i18n' @@ -70,8 +71,9 @@ const spec = computed(() => ({ // strip desynchronises from the scroll column. panes: [ { id: 'wall', label: t('tabs.countryPosters'), at: [0, 0], config: { profile: 'country-poster-wall' } }, - // the same DEV-gated read as countrySheet.js previewUnreleased(), INLINE so every build drops the pane entirely - ...(import.meta.env && import.meta.env.DEV && globalThis.DXM_PREVIEW_UNRELEASED === true ? [INDIA_PANE] : []), + // the same DEV-gated read as countrySheet.js previewUnreleased() (dev server AND the explicit opt-in), INLINE so + // every build drops the pane entirely + ...(import.meta.env && import.meta.env.DEV && globalThis.DXM_PREVIEW_UNRELEASED === true && previewOptedIn() ? [INDIA_PANE] : []), { id: 'map', label: t('tabs.map'), at: [2, 0], config: { profile: 'countries-map' } }, ], // The switcher lives in this element's own header, so the layout's "… below ↓" @@ -80,9 +82,10 @@ const spec = computed(() => ({ })) // INDIA: the whole-India poster (66x66 by default) on its own tab, the same wall filtered to India, labelled with -// India's own name in the reader's language (Intl). Shown ONLY in a preview context (countrySheet.js -// previewUnreleased(): the Plugin Studio on the pilot), because the poster is not released; everywhere else the tab -// is absent and India's card keeps the withheld sentence. +// India's own name in the reader's language (Intl). Shown ONLY in an opted-in preview (countrySheet.js +// previewUnreleased(): the pilot's dev server AND ?preview=india or localStorage 'dxm-preview'), because the poster is +// not released; everywhere else (the pilot without the opt-in included) the tab is absent and India's card keeps the +// withheld sentence. const INDIA_PANE = { id: 'india', label: countryName('IND', getRuntimeLocale() || detectLocale() || 'en') || 'India', at: [1, 0], config: { profile: 'country-poster-wall', countries: ['IND'] } } const stage = ref(null) diff --git a/plugin-designs/country-posters/scripts/check-country-sheet-one-path.mjs b/plugin-designs/country-posters/scripts/check-country-sheet-one-path.mjs index 8bc30174e..b56aba054 100644 --- a/plugin-designs/country-posters/scripts/check-country-sheet-one-path.mjs +++ b/plugin-designs/country-posters/scripts/check-country-sheet-one-path.mjs @@ -221,7 +221,7 @@ export async function armG(cs, srcs, mfeSrc = '') { if (!/rowsWithheld\((?:c|item)\) \? (?:withheld\(\)|Promise\.reject\(keyedError\('messages\.countryPosterTooMany')/.test(src)) fails.push(`${name}: withheld ROWS are not withheld (its list/packet/cards would print)`) if (/sheetWithheld\(/.test(src)) fails.push(`${name}: still reads sheetWithheld() (ambiguous: poster or rows?)`) } - if (mfeSrc && !/\.\.\.\(import\.meta\.env && import\.meta\.env\.DEV && globalThis\.DXM_PREVIEW_UNRELEASED === true \? \[INDIA_PANE\] : \[\]\)/.test(mfeSrc)) fails.push('country-posters-mfe: the India tab is not gated on the DEV-only preview read') + if (mfeSrc && !/\.\.\.\(import\.meta\.env && import\.meta\.env\.DEV && globalThis\.DXM_PREVIEW_UNRELEASED === true && previewOptedIn\(\) \? \[INDIA_PANE\] : \[\]\)/.test(mfeSrc)) fails.push('country-posters-mfe: the India tab is not gated on the DEV-only preview read AND the explicit opt-in (previewOptedIn())') // the BUILT tree never mentions the flag (every read is compiled away) try { const pub = join(ROOT, 'public/dx-maps') @@ -298,7 +298,8 @@ async function main() { ['an opener argument comes back', async () => armG({ ...CS, posterWithheld: (it, open) => CS.posterWithheld(it) }, lists(files[wall]), MFE)], ['the preview read without DEV (a global opens production)', async () => armG({ ...CS, previewUnreleased: () => globalThis.DXM_PREVIEW_UNRELEASED === true }, lists(files[wall]), MFE)], ['India\'s rows un-withheld', async () => armG({ ...CS, rowsWithheld: () => false }, lists(files[wall]), MFE)], - ['the India tab shown without the preview flag', async () => armG(CS, lists(files[wall]), MFE.replace('...(import.meta.env && import.meta.env.DEV && globalThis.DXM_PREVIEW_UNRELEASED === true ? [INDIA_PANE] : []),', 'INDIA_PANE,'))], + ['the India tab shown without the preview flag', async () => armG(CS, lists(files[wall]), MFE.replace('...(import.meta.env && import.meta.env.DEV && globalThis.DXM_PREVIEW_UNRELEASED === true && previewOptedIn() ? [INDIA_PANE] : []),', 'INDIA_PANE,'))], + ['the India tab shown on the pilot by dev mode alone', async () => armG(CS, lists(files[wall]), MFE.replace(' && previewOptedIn() ? [INDIA_PANE]', ' ? [INDIA_PANE]'))], ['the wall prints the list of a country whose rows are withheld', async () => armG(CS, lists(files[wall].replace('rowsWithheld(c) ? withheld() : ', '')))], ['the library type prints withheld rows', () => armH(TYPE.replace('rowsWithheld(c) ? ', 'false ? '))], ['the library type shows a withheld poster', () => armH(TYPE.replace('posterWithheld(c) ||', 'false ||'))], diff --git a/plugin-designs/country-posters/scripts/check-poster-section.mjs b/plugin-designs/country-posters/scripts/check-poster-section.mjs index b432f21c7..942504689 100644 --- a/plugin-designs/country-posters/scripts/check-poster-section.mjs +++ b/plugin-designs/country-posters/scripts/check-poster-section.mjs @@ -51,17 +51,31 @@ async function arms({ types, country, element, sectionSrc, viewSrc, cs }) { if (!/countryPosterTooMany/.test(String(why))) fails.push(`C: withheld India's rows are not withheld (${why})`) if (fetched) fails.push('C: withheld India fetched records') } - // C2. THE PREVIEW, under Vite (import.meta.env.DEV is true here, as on the pilot's dev server): only with the global - // set does India's whole-country poster open, at 60x96; its rows stay withheld; without it, withheld. + // C2. THE PREVIEW, under Vite (import.meta.env.DEV is true here, as on the pilot's dev server): DEV MODE ALONE (the + // global set by the dev bootstrap) does NOT open India: the pilot shows what the release shows. Only with the + // explicit opt-in too (?preview=india, or localStorage 'dxm-preview') does the poster open, at 60x96; its rows + // stay withheld; without the global, withheld. const was = globalThis.DXM_PREVIEW_UNRELEASED + const loc = Object.getOwnPropertyDescriptor(globalThis, 'location'), ls = Object.getOwnPropertyDescriptor(globalThis, 'localStorage') + const setLoc = (search) => Object.defineProperty(globalThis, 'location', { value: { search }, configurable: true, writable: true }) + const setLs = (v) => Object.defineProperty(globalThis, 'localStorage', { value: { getItem: (k) => (k === 'dxm-preview' ? v : null) }, configurable: true, writable: true }) try { globalThis.DXM_PREVIEW_UNRELEASED = true + setLoc(''); setLs(null) + if (!cs.posterWithheld({ iso3: 'IND' }) || cs.largeSheet({ iso3: 'IND' })) fails.push('C2: dev mode ALONE (no opt-in) opens India: the pilot would show the withheld poster') + setLs('india') + if (cs.posterWithheld({ iso3: 'IND' })) fails.push("C2: localStorage 'dxm-preview'='india' does not open the preview") + setLs(null); setLoc('?lang=en&preview=india') const big = cs.largeSheet({ iso3: 'IND' }) if (cs.posterWithheld({ iso3: 'IND' }) || !big || big.format !== '60x96') fails.push(`C2: in a DEV preview India's poster does not open at 60x96 (${big && big.format})`) if (!cs.rowsWithheld({ iso3: 'IND' })) fails.push("C2: the preview also released India's rows") delete globalThis.DXM_PREVIEW_UNRELEASED if (!cs.posterWithheld({ iso3: 'IND' })) fails.push('C2: without the preview global India is not withheld') - } finally { if (was === undefined) delete globalThis.DXM_PREVIEW_UNRELEASED; else globalThis.DXM_PREVIEW_UNRELEASED = was } + } finally { + if (was === undefined) delete globalThis.DXM_PREVIEW_UNRELEASED; else globalThis.DXM_PREVIEW_UNRELEASED = was + if (loc) Object.defineProperty(globalThis, 'location', loc); else delete globalThis.location + if (ls) Object.defineProperty(globalThis, 'localStorage', ls); else delete globalThis.localStorage + } // D const fake = (attrs) => ({ getAttribute: (n) => (n in attrs ? attrs[n] : null), hasAttribute: (n) => n in attrs }) const d0 = element.posterSectionConfig(fake({ scope: 'africa' })) @@ -121,6 +135,7 @@ try { ["a registry without 'country'", { ...real, types: { ...types, posterTypeNames: () => [] } }, /^A:/], ['a region key without "&" → "and"', { ...real, country: { ...country, items: (s, l) => (/-and-/.test(String(s)) ? [] : country.items(s, l)) } }, /^B:/], ['the preview ignored under DEV', { ...real, cs: { ...cs, posterWithheld: () => true, largeSheet: () => null } }, /^C2:/], + ['dev mode alone opens India (no opt-in)', { ...real, cs: { ...cs, posterWithheld: () => false, largeSheet: () => ({ format: '60x96', formats: ['60x96'] }) } }, /^C2: dev mode ALONE/], ['a section that uses a name it does not inject', { ...real, sectionSrc: sectionSrc.replace('', '{{ notProvided }}') }, /^E:/], ['an element config that drops show-header', { ...real, element: { posterSectionConfig: (el) => { const c = element.posterSectionConfig(el); delete c.showHeader; return c } } }, /^D: show-header="false"/], ['a view that does not pass show-header to the wall', { ...real, viewSrc: viewSrc.replace(':section-heads="showHeader"', '') }, /^F:/],