UNPKG

@provenance/provenance-sdk

Version:
297 lines (247 loc) 9.01 kB
import { encodeBundleContentsId, findBundleContentsId } from './structured-data.js' import { frameFactory } from './frame-factory.js' import Modal from './modal.js' import { onChildEvent } from './events.js' class Bundle { constructor (options = {}) { this.setupOptions(options) if (!this.validateOptions(options)) return this.addLocaleToEmbedURL(options) this.initializeBundle(options) } setupOptions (options) { options.type = 'bundle' this.container = options.container || document.querySelector('provenance-bundle') } validateOptions (options) { if (!this.container) { return console.error('Provenance: Bundle container not found.') } if (!options.url) { return console.error('Provenance: Bundle url not found.') } return true } addLocaleToEmbedURL (options) { // Get the preferred language of the user from the browser let language = navigator.language.slice(0, 2) // For Mybacs, get the page language instead if (location.hostname === 'mybacs.com') { language = document.documentElement.getAttribute('lang') } // Parse the original URL const url = new URL(options.url) // Get the path segments as an array const pathSegments = url.pathname.split('/') // Don't do anything if there are two chars in the first path segment. // It's an assumption, but likely it means that the language has already been set. if (pathSegments.length > 1 && pathSegments[1].length === 2) return // Append the locale to the URL, assuming it's not already there url.pathname = `${language}${url.pathname}` options.url = url.toString() } initializeBundle (options) { this.hasRendered = false if (this.isDataDrivenEmbed(options)) { this.createFromBundleContentsId(options).catch(console.log) } else { this.createFromUrl(options) if (options.identifier) this.setIdentifierAttribute(options.identifier) } } isDataDrivenEmbed ({ url, schema }) { if (schema) return true const urlComponents = new URL(url).pathname.split('/') return !/product|user|example/.test(urlComponents) } async createFromBundleContentsId (options) { const id = await findBundleContentsId(options.schema) if (!id) return console.error(`Provenance: Could not find identifier from ${options.schema} structured data`) options.url += '/' + encodeBundleContentsId(id.schema, id.identifier) this.createFromUrl(options) this.setIdentifierAttribute(id.identifier) } createFromUrl (options) { if (this.isShopifyUrl(options.url)) addVariantListener() console.debug('Provenance: createFromUrl', this.container, options) this.renderZoidComponent(this.container, options) } isShopifyUrl (url) { return url.includes('shop=') } renderZoidComponent (container, options) { this.initializeComponent(container, options) let observer if (!options.autoRenderBundle) { observer = this.createIntersectionObserver(container) observer.observe(container) } else { this.embed = this.component.render(container).catch(console.error) this.hasRendered = true } this.handleBadgeClick(container, observer, options.autoRenderBundle) } initializeComponent (container, options) { const bundleOptions = { url: options.url, version: VERSION, onChildEvent: event => onChildEvent(event, container, this.component.onParentEvent), openModal: modalOptions => this.openModal(modalOptions) } const frame = frameFactory.getFrame('embed', options.url, options.autoRenderBundle) this.component = frame.component(bundleOptions) } createIntersectionObserver (container) { this.hasRendered = false return new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting && document.body.contains(container)) { this.renderComponent(container, observer) } }) }) } handleBadgeClick (container, observer, autoRenderBundle = false) { const badge = document.querySelector('provenance-trust-badge') if (badge) { badge.addEventListener('click', async e => { e.preventDefault() if (autoRenderBundle === false) { await this.renderComponent(container, observer) } container.scrollIntoView({ block: 'center', behavior: 'smooth' }) }) } } // Call only this function whenever you need to render the zoid component. // It ensures that the component is rendered once avoiding race conditions. async renderComponent (container, observer) { if (this.isBeingRendered || this.hasRendered) return this.isBeingRendered = true try { await this.component.render(container) this.hasRendered = true observer?.unobserve(container) } catch (error) { console.error(error) } finally { this.isBeingRendered = false } } openModal (options) { this.modal = new Modal({ frameFactory }) this.modal.open(options) } setIdentifierAttribute (value) { this.container.setAttribute('identifier', value) } } /** * A function that, given a valid product identifier, displays the * Proof Point bundle relevant to the product. * * @param {string} id - A valid product identifier - usually a SKU or GTIN. */ export function setProductID (id) { if (!id) return console.error(`Provenance: invalid product ID: ${id}.`) console.debug('Provenance: setProductID called with arg:', id) hideAllBundles() manageBundleForId(id) } function hideAllBundles () { document.querySelectorAll('provenance-bundle').forEach(elem => { elem.hidden = true elem.id = '' }) } function manageBundleForId (id) { // Remove the trust badge ID from existing bundles if they have it resetTrustBadgeId() const bundle = document.querySelector(`provenance-bundle[identifier="${id}"]`) // Either display the existing bundle or initialize a new one if (bundle) { console.debug('Provenance: bundle already exists:', bundle) displayExistingBundle(bundle, 'provenance-trust-badge') } else { initializeNewBundle('provenance-trust-badge', id) } } function resetTrustBadgeId () { // Check for an existing bundle with the trust badge ID const existingTrustBadgeBundle = document.querySelector('provenance-bundle#provenance-trust-badge') // If found, clear the ID so it can be reassigned if (existingTrustBadgeBundle) { existingTrustBadgeBundle.id = '' } } function displayExistingBundle (bundle, idForNewBundle) { bundle.hidden = false bundle.id = idForNewBundle } function initializeNewBundle (idForNewBundle, id) { const container = document.createElement('provenance-bundle') container.id = idForNewBundle const newURL = buildBundleURL(id) appendNewBundle(container) const options = { url: newURL, container, identifier: id, autoRenderBundle: true } console.debug('Provenance: initializing new bundle:', options) ProvenanceBundle(options) } function buildBundleURL (id) { const url = document.querySelector('provenance-bundle').getAttribute('url') return `${url}/${encodeBundleContentsId('product', id)}` } function appendNewBundle (container) { document.querySelector('provenance-bundle:last-of-type') .insertAdjacentElement('afterend', container) } function observeUrlForVariantChanges (variants) { let lastUrl = location.href new MutationObserver(() => { const url = location.href if (url !== lastUrl) { lastUrl = url const urlVariantId = new URLSearchParams(location.search).get('variant') if (urlVariantId) { const newSku = variants.find(v => v.id.toString() === urlVariantId).sku setProductID(newSku) } } }).observe(document, { subtree: true, childList: true }) } export function listenForVariantChangeEvent () { document.addEventListener('provenanceVariantChange', (event) => { setProductID(event.detail.identifier) }) } export function addVariantListener () { if (shopifyVariants()) { listenForShopifyVariantChange() } else { listenForVariantChangeEvent() } } function shopifyVariants () { return window.ShopifyAnalytics?.meta?.product?.variants } function listenForShopifyVariantChange () { const variants = shopifyVariants() if (variants) { document.addEventListener('DOMContentLoaded', () => { observeUrlForVariantChanges(variants) }) } } /** * An initializer function that displays a Proof Point Bundle. * * Only a `url` is required to construct a Bundle. * * If no `container` is specified, the Bundle will be loaded * into the first `<provenance-bundle>` tag found in the DOM. * * @param {{ * url: string, * schema?: 'Product' | 'Brand', * container?: Element, * autoRenderBundle: Boolean * }} options - An object containing initialization options. */ export function ProvenanceBundle (options) { /* eslint-disable-next-line no-new */ new Bundle(options) }