@provenance/provenance-sdk
Version:
Enables provenance.org content on other domains
297 lines (247 loc) • 9.01 kB
JavaScript
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)
}