Skip to main content

Bigcommerce Real-Time-Pricing configuration

Operation process

BigCommerce Setting(Optional)

If you need to use it in cart or checkout, you should change this behavior to allow promotions on overridden prices in Store Settings under Promotions and Coupons in the control panel.

Set default sales location ID in Shophyve

Add script for Store

For the config:

  • secretKey: The secret key to connect the connection. (Contact our team to get it)

  • apiUrl: Shophyve project address.

For price list:

  • productIdAttribute: Locate the element corresponding element to the bigcommerce product id and get the latest prices.

  • priceSelector: When retrieving the latest prices, the elements matching this selector will be loaded and updated with the latest prices once the API request is successful.

For cart:

  • cartIdAttribute: Locate the element corresponding to the BigCommerce cart ID and retrieve the latest prices for the products in this cart.

For checkout:

  • needRefreshPage: If it's true, the page will refresh after getting the latest price.
  • location_name: To avoid multiple page refreshes, store the name in localstorage.
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
<script>
	const config = {
	  secretKey: 'xxx',
	  url: 'https://a1.shophive.ai',
	  // product list setting
	  productList: {
	    isActive: true,
	    productIdAttribute: 'data-entity-id',
	    priceSelector: '[data-product-price-without-tax]',
	    productPreviewSelector: '.productPreview',
	  },
	  // cart setting
	  cart: {
	    isActive: true,
	    cartIdAttribute: 'a1-cart-id',
	    productIdAttribute: 'a1-cart-product-id'
	  },
	  // cart preview setting
	  cartPreview: {
	    isActive: true,
	    productIdAttribute: 'a1-preview-cart-product-id',
	    priceSelector: '.previewCartItem-price span',
	    quantitySelector: '.previewCartItem-price'
	  },
	  // checkout preview setting
	  checkoutPreview: {
	    isActive: true,
	    cartIdAttribute: 'a1-preview-checkout-id',
	    totalPriceSelector: '.previewCartCheckout-price',
	    productIdAttribute: 'a1-preview-checkout-product-id',
	    priceSelector: '.productView-price'
	  },
	  need_websocket: false,
	  defaultPriceNotification: 'Failed to update prices. Please try again later.',
	  // when get error, tip after price
	  priceTip: ' (Not real time price from ERP)',
	  // use for store refresh time
	  sessionName: 'a1-cart-timestamp',
	  // use for store original price
	  priceAttribute: 'a1-original-price',
	  localCacheTime: 86400, // unit: second
	}

	class RealTimePricing {
	  constructor() {
	    this.customerID = null
	    this.initData()
	    this.init()
	  }

	  initData(){
	    this.products = []
	    this.previewProducts = []
	    this.cartQuantities = []
	    this.cartID = null
	    this.productDetailQuantity = 0
	    this.chekcoutPreviewCartID = null
	    this.priceCache = new Map()
	  }

	  async init() {
	    setInterval(() => {
	      this.getCustomerID()
	      this.initB2BShophiveConfig()
	      if (this.customerID && this.companyID) {
	        this.localCacheKey = `shophive-product-price-cache-${this.companyID}`
	        switch(true){
	          case(config.productList.isActive):
	            this.checkProductList()
	          case(config.cartPreview.isActive):
	            this.checkCartPreviewProductList()
	          case(config.cart.isActive):
	            this.checkCart()
	          case(config.checkoutPreview.isActive):
	            this.checkcheckoutPreview()
	        }
	      }
	    }, 10)
	  }

	  initB2BShophiveConfig() {
	    window.shophive = window.shophive || {};

	    let masqueradeCompany = null;
	    let companyId = null;

	    const rawMasquerade = localStorage.getItem("persist:b2bFeatures");
	    if (rawMasquerade) {
	      const parsedMasquerade = JSON.parse(rawMasquerade);
	        if (parsedMasquerade?.masqueradeCompany) {
	        masqueradeCompany = JSON.parse(parsedMasquerade.masqueradeCompany);
	      }
	    }

	    if (masqueradeCompany?.isAgenting) {
	      companyId = masqueradeCompany.id;
	    } else {
	      const rawCompany = sessionStorage.getItem("persist:company");
	      if (rawCompany) {
	        const parsedCompany = JSON.parse(rawCompany);
	        if (parsedCompany?.companyInfo) {
	          const companyInfo = JSON.parse(parsedCompany.companyInfo);
	          companyId = companyInfo?.id || null;
	        }
	      }
	    }

	    window.shophive.config = {
	      secretKey: config.secretKey,
	      url: config.url,
	      customerID: this.customerID,
	      companyID: companyId,
	    }

	    this.companyID = companyId
	  }

	  getProductKey(product) {
	    return `${product.ecommerce_product_id}_${product.quantity}`;
	  }

	  // PDP & PLP
	  getProducts() {
	    const parseProduct = (element) => {
	      const ecommerce_product_id = element.getAttribute(config.productList.productIdAttribute)
	      const priceElement = element.querySelector(config.productList.priceSelector)
	      const quantityElement = document.getElementById('qty[]')
	      const quantity = quantityElement ? Number(quantityElement.value || 1) : 1
	      this.setOriginPrice(priceElement)
	      const shophive_unit_id = this.getProductKey({
	        ecommerce_product_id,
	        quantity,
	      })
	      const currentShophiveUnitId = element.getAttribute('shophive-unit-id')
	      const needUpdatePrice = currentShophiveUnitId !== shophive_unit_id
	      if (needUpdatePrice) {
	        element.setAttribute('shophive-unit-id', shophive_unit_id)
	      }

	      return {
	        ecommerce_product_id,
	        shophive_unit_id,
	        priceElement,
	        quantity,
	        needUpdatePrice,
	      }
	    }

	    const productViewModal = document.querySelector(config.productList.productPreviewSelector)
	    if (productViewModal) {
	      return [parseProduct(productViewModal)]
	    }
	    return Array.from(document.querySelectorAll(`[${config.productList.productIdAttribute}]`)).map(parseProduct)
	  }

	  checkProductList() {
	    const newProducts = this.getProducts()
	    if (newProducts.length) {
	      const needUpdateProducts = newProducts.filter((newProduct) => {
	        return newProduct.needUpdatePrice
	      })

	      if (needUpdateProducts.length) {
	        this.products = newProducts
	        this.updatePrices(this.products)
	      }
	    }
	  }

	  loadCacheMap() {
	    if (this.priceCache && this.priceCache.size > 0) {
	      return this.priceCache
	    }

	    try {
	      const raw = localStorage.getItem(this.localCacheKey)
	      if (!raw) {
	        this.priceCache = new Map()
	        return this.priceCache
	      }

	      const parsed = JSON.parse(raw)
	      const now = Date.now()
	      const filtered = parsed.filter(([_, value]) => {
	        return !value.timestamp || now - value.timestamp < config.localCacheTime * 1000
	      })

	      this.priceCache = new Map(filtered)
	      return this.priceCache
	    } catch (e) {
	      this.priceCache = new Map()
	      return this.priceCache
	    }
	  }

	  saveCacheMap(map) {
	    this.priceCache = map
	    try {
	      const arr = Array.from(map.entries())
	      localStorage.setItem(this.localCacheKey, JSON.stringify(arr))
	    } catch (e) {}
	  }

	  getCachedPrice(shophive_unit_id) {
	    const map = this.loadCacheMap()
	    const entry = map.get(shophive_unit_id)
	    return entry?.data ?? null
	  }

	  setCachedPrice(shophive_unit_id, data) {
	    const map = this.loadCacheMap()
	    map.set(shophive_unit_id, { data, timestamp: Date.now() })
	    this.saveCacheMap(map)
	  }

	  async handlePriceDOM(params) {
	    const {
	      newProducts,
	      preProducts
	    } = params

	    preProducts.forEach((preProduct) => {
	      const existProduct = newProducts.find((newProduct) => newProduct.shophive_unit_id === preProduct.shophive_unit_id && newProduct.is_success)
	      if (existProduct) {
	        preProduct.priceElement.textContent = `$${parseFloat(existProduct.net_price).toFixed(2)}`
	      } else {
	        preProduct.priceElement.textContent = preProduct.priceElement.getAttribute(config.priceAttribute) + config.priceTip
	      }
	    })
	  }

	  async updatePrices(products) {
	    const preProducts = []
	    const needGetPriceProducts = []
	    products.forEach((product) => {
	      const cachedProduct = this.getCachedPrice(product.shophive_unit_id)
	      if(product.priceElement){
	        if (cachedProduct) {
	          product.priceElement.textContent = `$${parseFloat(cachedProduct.net_price).toFixed(2)}`
	          preProducts.push(product)
	        } else {
	          product.priceElement.textContent = 'Loading...'
	          needGetPriceProducts.push(product)
	        }
	      }
	    })

	    if (!needGetPriceProducts.length) return

	    try {
	      const data = await this.fetchRealTimePrice({
	        products: needGetPriceProducts.map((product) => ({
	          ecommerce_product_id: product.ecommerce_product_id,
	          shophive_unit_id: product.shophive_unit_id,
	          quantity: product.quantity,
	          price: String(product.priceElement.getAttribute(config.priceAttribute)).split('$')?.[1]
	        })),
	      })

	      await this.handlePriceDOM({
	        newProducts: data.products || [],
	        preProducts: needGetPriceProducts
	      })

	      try {
	        (data.products || []).forEach(product => {
	          if(product.is_success){
	            this.setCachedPrice(product.shophive_unit_id, product)
	          }
	        })
	      } catch(error){
	        console.log('Cache Error: ', error)
	      }
	    } catch (error) {
	      this.showNotification()
	      this.handlePriceDOM({
	        newProducts: [],
	        preProducts: needGetPriceProducts
	      })
	    }
	  }

	  // Cart
	  getCartId(cartAttribute) {
	    const cartId = document.querySelector(`[${cartAttribute}]`)?.getAttribute(cartAttribute) || "{{checkout.id}}" || null
	    return cartId
	  }

	  checkCart() {
	    const cartId = this.getCartId(config.cart.cartIdAttribute)
	    if (cartId) {
	      if (cartId !== this.cartID) {
	        const newQuantities = this.getCartQuantities()
	        this.cartQuantities = newQuantities
	        if (this.shouldUpdateCart()) {
	          this.cartID = cartId
	          this.getRealTimeCart()
	        } else {
	          this.cartID = cartId
	          if(cartId){
	            this.sendMessage({
	              ecommerce_customer_id: String(this.customerID),
	              cart_id: this.chekcoutPreviewCartID || this.cartID || '',
	              need_origin_data: false,
	              need_cart_info: true,
	              products: [],
	            })

	            if(localStorage.getItem('a1-failed-products')){
	              this.showNotification('The prices of some products are not the latest prices.')
	              this.showBlockingModalIfCheckout()
	              localStorage.removeItem('a1-failed-products')
	            }
	          }
	        }
	      } else {
	        const newQuantities = this.getCartQuantities()

	        const hasChanged = this.cartQuantities.length !== newQuantities.length
	        || newQuantities.some((newQuantity) => {
	          const existQuantity = this.cartQuantities.find((quantity) => quantity.id === newQuantity.id)
	          return !existQuantity || existQuantity.quantity !== newQuantity.quantity
	        })

	        const isAnyInputFocused = () => {
	          const inputs = document.querySelectorAll(`[${config.cart.productIdAttribute}] input`);
	          return Array.from(inputs).some(input => input === document.activeElement);
	        }

	        if (hasChanged && !isAnyInputFocused()) {
	          this.cartQuantities = newQuantities
	          this.getRealTimeCart()
	        }
	      }
	    }
	  }

	  getCartQuantities() {
	    return Array.from(document.querySelectorAll(`[${config.cart.productIdAttribute}] input`)).map((element) => ({
	      id: element.name,
	      quantity: element.value
	    }))
	  }

	  async getRealTimeCart() {
	    this.showPageLoading()
	    try {
	      const data = await this.fetchRealTimePrice({
	        cart_id: this.cartID || this.chekcoutPreviewCartID || '',
	      })
	      const failedProducts = (data?.products || []).filter((item) => !item.is_success)
	      if(failedProducts.length){
	        localStorage.setItem('a1-failed-products', JSON.stringify(failedProducts));
	      }


	      if (data) {
	        this.refreshPage()
	      } else {
	        this.hidePageLoading()
	      }

	    } catch (error) {
	      this.hidePageLoading()
	      this.showNotification()
	      this.showBlockingModalIfCheckout()
	    }
	  }

	  // Checkout preview
	  async checkcheckoutPreview() {
	    const cartId = document.querySelector(`[${config.checkoutPreview.cartIdAttribute}]`)?.getAttribute(config.checkoutPreview.cartIdAttribute)
	    if (cartId !== this.chekcoutPreviewCartID) {
	      this.chekcoutPreviewCartID = cartId

	      const totalPriceElement = document.querySelector(config.checkoutPreview.totalPriceSelector)
	      const itemElement = document.querySelector(`[${config.checkoutPreview.productIdAttribute}]`)

	      if (cartId && totalPriceElement) {
	        const priceElement = itemElement.querySelector(config.checkoutPreview.priceSelector)
	        const quantity = priceElement.textContent.split('×')[0] + '×'
	        const price = priceElement.textContent.split('×')[1]
	        try {
	          this.setOriginPrice(totalPriceElement)
	          this.setOriginPrice(priceElement, price)

	          totalPriceElement.textContent = 'Loading...'
	          priceElement.textContent = 'Loading...'

	          const data = await this.fetchRealTimePrice({
	            cart_id: cartId,
	          })

	          const newProduct = (data?.cart_info?.items || []).find((item) => String(item.ecommerce_product_id) === itemElement.getAttribute(config.checkoutPreview.productIdAttribute))
	          if (data) {
	            totalPriceElement.textContent = '$' + data.cart_info.cart_amount
	          }
	          if (newProduct) {
	            priceElement.textContent = quantity + '$' + newProduct.price
	          } else {
	            throw new Error("Can't get real time price")
	          }
	        } catch (error) {
	          totalPriceElement.textContent = totalPriceElement.getAttribute(config.priceAttribute) + config.priceTip
	          priceElement.textContent = quantity + priceElement.getAttribute(config.priceAttribute) + config.priceTip
	          this.showNotification()
	        }
	      }
	    } else {
	      this.chekcoutPreviewCartID = cartId
	    }
	  }

	  // Cart Priview
	  checkCartPreviewProductList() {
	    const newProducts = this.getCartPreviewProducts() || []
	    if (newProducts.length) {
	      const needUpdateProducts= newProducts.filter((newProduct) => {
	        return newProduct.needUpdatePrice
	      })
	      if (needUpdateProducts.length) {
	        this.previewProducts = newProducts
	        this.updatePrices(this.previewProducts)
	      }
	    }
	  }

	  getCartPreviewProducts() {
	    const parseProduct = (element) => {
	      const ecommerce_product_id = element.getAttribute(config.cartPreview.productIdAttribute)
	      const priceElement = element.querySelector(config.cartPreview.priceSelector)
	      const quantityElement = element.querySelector(config.cartPreview.quantitySelector);
	      let quantity = 1
	      if (quantityElement) {
	        const quantityText = quantityElement.textContent.trim();
	        const quantityMatch = quantityText.match(/(\d+)\s×/);

	        if (quantityMatch && quantityMatch[1]) {
	          quantity = parseInt(quantityMatch[1], 10);
	        }
	      }

	      this.setOriginPrice(priceElement)
	      const shophive_unit_id = this.getProductKey({
	        ecommerce_product_id,
	        quantity,
	      })

	      const currentShophiveUnitId = element.getAttribute('shophive-unit-id')
	      const needUpdatePrice = currentShophiveUnitId !== shophive_unit_id
	      if (needUpdatePrice) {
	        element.setAttribute('shophive-unit-id', shophive_unit_id)
	      }

	      return {
	        ecommerce_product_id,
	        priceElement,
	        quantity,
	        needUpdatePrice,
	      }
	    }

	    return Array.from(document.querySelectorAll(`[${config.cartPreview.productIdAttribute}]`)).map(parseProduct)
	  }

	  // Global
	  getCustomerID() {
	    const customer = {{{json customer}}}
	         if(customer?.id){
	     this.customerID = customer.id
	    }
	  }

	  setOriginPrice(priceElement, price) {
	    if (priceElement && !priceElement.getAttribute(config.priceAttribute)) {
	      priceElement.setAttribute(config.priceAttribute, price || priceElement.textContent)
	    }
	  }

	  subscribeToUpdates() {
	    if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
	      this.websocket.send(JSON.stringify({ action: "real-time-pricing", data: {} }));
	    }
	  }

	  async sendMessage(body) {
	    if(!config.need_websocket){
	      return
	    }
	    if (!this.socket) {
	      this.connectWebSocket()
	    } else {
	      await this.sleep(10 * 1000)
	    }
	    const message = {
	      a1_secret_key: config.secretKey,
	      body,
	    };
	    this.socket.emit("real-time-pricing", message);
	  }

	  async sleep(milliseconds) {
	    return new Promise((resolve) => setTimeout(resolve, milliseconds));
	  };

	  connectWebSocket() {
	    if(this.socket){
	      return
	    }
	    this.socket = io(`${config.url}/api/v1/socket`, {
	        transports: ["websocket"],
	        path: "/api/v1/socket"
	    });
	    this.socket.on("real-time-pricing", async (data) => {
	      if(data.had_price_changed){
	        this.showNotification('The product price has changed, the latest price will be updated soon', 'tip')
	        await this.sleep(3000)
	        this.initData()
	      }
	    });
	  }

	  async fetchRealTimePrice(params) {
	    const {
	      products = [],
	      cart_id,
	    } = params
	    const headers = {
	      'Content-Type': 'application/json',
	      'A1-Secret-Key': config.secretKey,
	    }
	    console.log('fetch real time price: ', {
	      ecommerce_customer_id: this.customerID,
	      cart_id: this.cartID,
	      products,
	    })
	    const requestBody = {
	      ecommerce_customer_id: String(this.customerID),
	      ecommerce_company_id: String(this.companyID || ''),
	      cart_id,
	      need_origin_data: false,
	      need_cart_info: true,
	      products,
	    }
	    const response = await fetch(config.url + '/api/v1/products/real-time-pricing', {
	      method: 'POST',
	      headers,
	      body: JSON.stringify(requestBody),
	    })
	    if (!response.ok) {
	      throw new Error('Failed to fetch prices')
	    }
	    const data = await response.json()
	    try {
	      if(!this.cartID){
	        const newPriceProducts = products.map((item) => {
	          const product = (data.data.products || []).find((i) => i.shophive_unit_id === item.shophive_unit_id && i.is_success)
	          return product ? {
	            ...item,
	            price: String(product.net_price),
	          } : item
	        })

	        this.sendMessage({
	          ...requestBody,
	          products: newPriceProducts,
	        })
	      }

	    }catch(error){
	      console.log('Websocket error: ', error)
	    }
	    return data?.data
	  }

	  refreshPage() {
	    sessionStorage.setItem(config.sessionName, this.getCurrentTimestamp())
	    location.reload()
	  }

	  getCurrentTimestamp() {
	    return new Date().getTime();
	  }

	  shouldUpdateCart() {
	    const lastUpdated = sessionStorage.getItem(config.sessionName);

	    if (!lastUpdated) {
	      return true;
	    }

	    const currentTimestamp = this.getCurrentTimestamp();
	    const timeDifferenceInSec = (currentTimestamp - lastUpdated) / 1000;

	    if (timeDifferenceInSec < 5) {
	      return false;
	    }
	    return true;
	  }

	  showNotification(message = config.defaultPriceNotification, type = "error") {
	    const notification = document.createElement('div')
	    notification.className = 'a1-notification'
	    notification.textContent = message

	    const colorObject = {
	      error: '#ff4d4f',
	      tip: '#1890ff',
	    }

	    Object.assign(notification.style, {
	      position: 'fixed',
	      top: '20px',
	      right: '20px',
	      backgroundColor: colorObject[type],
	      color: '#fff',
	      padding: '10px 20px',
	      borderRadius: '5px',
	      boxShadow: '0 2px 5px rgba(0, 0, 0, 0.2)',
	      zIndex: 10001,
	      fontSize: '14px',
	      animation: 'fade-in-out 3s forwards',
	    })
	    document.body.appendChild(notification)
	    setTimeout(() => {
	      notification.remove()
	    }, 3000)
	  }

	  showBlockingModalIfCheckout() {
	    if (!window.location.href.includes('checkout')) return;

	    const overlay = document.createElement('div');
	    const modal = document.createElement('div');
	    const headerRow = document.createElement('div');
	    const icon = document.createElement('span');
	    const title = document.createElement('h2');
	    const message = document.createElement('p');
	    const buttonContainer = document.createElement('div');
	    const cartButton = document.createElement('button');
	    const refreshButton = document.createElement('button');

	    icon.innerHTML = '⚠️';
	    title.textContent = 'Something went wrong';
	    message.innerHTML = `The prices of some products are not the latest prices.<br>Please contact us or go back to the cart.`;
	    cartButton.textContent = 'Back to Cart';
	    cartButton.onclick = () => {
	      window.location.href = config.cartUrl;
	    };
	    refreshButton.textContent = 'Retry';
	    refreshButton.onclick = () => {
	      location.reload();
	    };

	    this.applyStyles(headerRow, {
	      display: 'flex',
	      alignItems: 'center',
	      gap: '10px',
	      marginBottom: '16px'
	    });

	    this.applyStyles(overlay, {
	      position: 'fixed',
	      top: '0',
	      left: '0',
	      width: '100vw',
	      height: '100vh',
	      backgroundColor: 'rgba(255, 255, 255, 0.6)',
	      zIndex: '9999',
	      display: 'flex',
	      justifyContent: 'center',
	      alignItems: 'center',
	      pointerEvents: 'auto'
	    });

	    this.applyStyles(modal, {
	      backgroundColor: '#fff',
	      padding: '32px 40px',
	      borderRadius: '8px',
	      boxShadow: '0 0 20px rgba(0,0,0,0.2)',
	      maxWidth: '700px',
	      width: '90%',
	      textAlign: 'left',
	      fontFamily: 'sans-serif',
	      position: 'relative'
	    });

	    this.applyStyles(icon, {
	      fontSize: '32px',
	      color: '#e02b1d',
	      marginBottom: '12px'
	    });

	    this.applyStyles(title, {
	      fontSize: '22px',
	      fontWeight: '600',
	      marginBottom: '10px'
	    });

	    this.applyStyles(message, {
	      fontSize: '16px',
	      lineHeight: '1.6',
	      color: '#333',
	      marginBottom: '24px'
	    });

	    this.applyStyles(buttonContainer, {
	      display: 'flex',
	      justifyContent: 'flex-end',
	      gap: '10px'
	    });

	    this.applyStyles(cartButton, {
	      padding: '10px 20px',
	      backgroundColor: '#fff',
	      color: '#292929',
	      border: '1px solid #8a8a8a',
	      boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)',
	      borderRadius: '4px',
	      cursor: 'pointer',
	      fontSize: '14px'
	    });

	    this.applyStyles(refreshButton, {
	      padding: '10px 20px',
	      backgroundColor: '#fff',
	      color: '#292929',
	      border: '1px solid #8a8a8a',
	      boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)',
	      borderRadius: '4px',
	      cursor: 'pointer',
	      fontSize: '14px'
	    });

	    headerRow.appendChild(icon);
	    headerRow.appendChild(title);
	    modal.appendChild(headerRow)
	    modal.appendChild(message);
	    buttonContainer.appendChild(refreshButton);
	    buttonContainer.appendChild(cartButton);
	    modal.appendChild(buttonContainer);
	    overlay.appendChild(modal);
	    document.body.appendChild(overlay);

	    document.body.style.overflow = 'hidden';
	  }

	  applyStyles(element, styles) {
	    for (const key in styles) {
	      element.style[key] = styles[key];
	    }
	  }

	  showPageLoading() {
	    if (document.getElementById('a1-loading-overlay')) return;

	    const loadingOverlay = document.createElement('div');
	    loadingOverlay.id = 'a1-loading-overlay';
	    loadingOverlay.style.position = 'fixed';
	    loadingOverlay.style.top = '0';
	    loadingOverlay.style.left = '0';
	    loadingOverlay.style.width = '100%';
	    loadingOverlay.style.height = '100%';
	    loadingOverlay.style.backgroundColor = 'rgba(255,255,255,1)';
	    loadingOverlay.style.zIndex = '999999';
	    loadingOverlay.style.display = 'flex';
	    loadingOverlay.style.justifyContent = 'center';
	    loadingOverlay.style.alignItems = 'center';
	    loadingOverlay.style.color = 'black';
	    loadingOverlay.style.fontSize = '24px';

	    loadingOverlay.innerHTML = 'Loading for latest price...';

	    document.body.appendChild(loadingOverlay);
	  }

	  hidePageLoading() {
	    const loadingOverlay = document.getElementById('a1-loading-overlay');
	    if (loadingOverlay) {
	      loadingOverlay.remove();
	    }
	  }
	}

	const style = document.createElement('style')
	style.textContent = `
	  @keyframes fade-in-out {
	    0% { opacity: 0; transform: translateY(-10px); }
	    10% { opacity: 1; transform: translateY(0); }
	    90% { opacity: 1; transform: translateY(0); }
	    100% { opacity: 0; transform: translateY(-10px); }
	  }
	`
	document.head.appendChild(style)

	let realTimePricing
	if (!realTimePricing) {
	  realTimePricing = new RealTimePricing()
	}
</script>

Add attribute

Go to the Modify page:

Add the attribute corresponding to config (You can customize the names, but they should correspond to each other):

Cart preview:

Cart:

Checkout preview:

Bundle B2B

Delete B2B scripts

Add Header Script

<script>
  {{#if customer.id}}
  {{#contains page_type "account"}}
    window.shophive = window.shophive || {};

    let masqueradeCompany = null;
    let companyId = null;

    const rawMasquerade = localStorage.getItem("persist:b2bFeatures");
    if (rawMasquerade) {
      const parsedMasquerade = JSON.parse(rawMasquerade);
        if (parsedMasquerade?.masqueradeCompany) {
        masqueradeCompany = JSON.parse(parsedMasquerade.masqueradeCompany);
      }
    }

    if (masqueradeCompany?.isAgenting) {
      companyId = masqueradeCompany.id;
    } else {
      const rawCompany = sessionStorage.getItem("persist:company");
      if (rawCompany) {
        const parsedCompany = JSON.parse(rawCompany);
        if (parsedCompany?.companyInfo) {
          const companyInfo = JSON.parse(parsedCompany.companyInfo);
          companyId = companyInfo?.id || null;
        }
      }
    }
    
    window.shophive.config = {
      secretKey: 'xxx',
      url: 'xxx',
      customerID: {{customer.id}},
      companyID: companyId,
    }
  {{/contains}}
  {{/if}}
</script>
<script>
  {{#if customer.id}}
  {{#contains page_type "account"}}
  var b2bHideBodyStyle = document.createElement('style');
  b2bHideBodyStyle.id = 'b2b-account-page-hide-body';
  b2bHideBodyStyle.innerHTML = 'body { display: none !important }';
  document.head.appendChild(b2bHideBodyStyle);
  {{/contains}}
  {{/if}}
</script>

<script>
  window.b3CheckoutConfig = {
    routes: {
      dashboard: '/account.php?action=order_status',
    },
  }
  window.B3 = {
    setting: {
      store_hash: '{{settings.store_hash}}',  
      channel_id: {{settings.channel_id}}, 
      platform: 'bigcommerce'
    },
    'dom.checkoutRegisterParentElement': '#checkout-app',
    'dom.registerElement':
      '[href^="/login.php"], #checkout-customer-login, [href="/login.php"] .navUser-item-loginLabel, #checkout-customer-returning .form-legend-container [href="#"]',
    'dom.openB3Checkout': 'checkout-customer-continue',
    before_login_goto_page: '/account.php?action=order_status',
    checkout_super_clear_session: 'true',
    'dom.navUserLoginElement': '.navUser-item.navUser-item--account',
  }
</script>
<script
  type="module"
  crossorigin=""
  src="https://a1buyerportal.silkdigital.io/index.js"
></script>
<script
  nomodule=""
  crossorigin=""
  src="https://a1buyerportal.silkdigital.io/polyfills-legacy.js"
></script>
<script
  nomodule=""
  crossorigin=""
  src="https://a1buyerportal.silkdigital.io/index-legacy.js"
></script>

Unsubscribe Buyer portal

API Document

API Request Body ( filter )

Example:

{
	"ecommerce_customer_id": "7566",
	"location_id": "40",
	"products": [
		{
			"ecommerce_product_id": "5165",
			"quantity": 10
		}
	]
}
Field NameIs Required?TypeDescription
location_idStringERP location ID. The default value is Shophyve default sales location ID.
ecommerce_customer_idStringEcommerce Customer ID. Shophyve will find the ERP Customer ID if it comes from Shophyve.
need_cart_infoBooleanIf set to true, the cart information will be returned. (The cart_id should be required.)
need_origin_dataBooleanIf set to true, the original ERP data will be returned.
cart_idStringEcommerce Cart ID. If provided with the cart id, the product price of this shopping cart will be updated.
contract_uidStringOptional. If contract-specific prices are required, populate this tag with contract_uid. Value must exist in job_price_hdr.job_price_hdr_uid and must be a valid active contract.
ship_to_idStringOptional. Populate this tag with a ship_to_id if ship-to-specific prices are required.
extraObjectUsed to place custom fields that can copy all parameters.
productsArrayList of product items. Example:
[{ ecommerce_product_id: '5165', quantity: 10 }]

For the product fields:

Field NameIs Required?TypeDescription
ecommerce_product_idStringEcommerce Product ID. Shophyve will find the ERP Product ID if it comes from Shophyve.
quantityNumberQuantity tags should be populated with the order quantity.
unit_nameStringOptional. Should be populated with the Unit name of the order quantity. Must match one of the unit names associated with the item. Defaults to P21's default sales UOM if omitted.
unit_sizeStringOptional. Should be populated with the unit size corresponding to unit_name. Defaults to P21's default sale unit size if omitted.
default_pricing_uomStringOptional. If pricing UOM is different from order quantity UOM, specify it here.
default_pricing_unit_sizeStringOptional. Unit size associated with the UOM passed in default_pricing_uom.
net_priceNumberLeave blank to let P21 calculate the price. If populated, P21 will return the specified price.
priceStringUsed to compare prices by socket.
extraObjectUsed to place custom fields that can copy all parameters.

API Response Body ( filter )

Example:

{
	"server_time": "521351355",
	"cart_info": {
		"base_amount": 405.65,
		"cart_amount": 405.65,
		"currency": {
			"code": "USD"
		},
		"items": [
			{
				"id": "907a0ef4-95d4-4bd1-b81e-08307397c0e5",
				"ecommerce_product_id": 1645,
				"price": 125.55,
				"quantity": 3
			}
		]
	},
	"products": [
		{
			"is_success": true,
			"failed_reason": null,
			"ecommerce_product_id": "1645",
			"item_id": "WKLKM546",
			"inv_mast_uid": "531",
			"list_price": "200.00",
			"net_price": "125.55",
			"location_name": "Los Angeles",
			"got_price_at": "5643513213",
			"free_quantity": "2521",
			"unit_name": "",
			"unit_size": "",
			"pricing_uom": "",
			"default_pricing_uom": "",
			"remaining_free_quantity": "",
			"pricing_unit_size": "",
			"default_pricing_unit_size": "",
			"list_of_item_location_quantities": {
				"item_location_quantity": {
					"location_id": "100220",
					"free_quantity": "135.00000",
					"lead_time_days": 0
				}
			}
		}
	]
}
Field NameTypeDescription
server_timeNumberThe timestamp when Shophyve got the price.
is_cart_changedBooleanIndicates whether the cart has been updated.

Products

Field NameTypeDescription
is_successBooleanWhether P21 was successfully requested or the price from cache was obtained.
failed_reasonStringShows the failure reason if an error occurred.
ecommerce_product_idStringEcommerce Product ID.
item_idStringERP item ID.
inv_mast_uidStringERP inv_mast_uid.
list_priceStringList price.
net_priceStringNet price.
location_nameStringLocation name associated with the location ID passed in the request.
got_price_atNumberThe timestamp when Shophyve got the price.
unit_nameStringOrder quantity UOM. Defaults to P21's sales UOM if not specified in request.
unit_sizeStringUnit size associated with the unit name (UOM).
unit_costStringSupplier cost of the item if GetCostType is set to "Supplier" in the request.
free_quantityStringFree quantity at the location.
pricing_uomStringUnit used for pricing.
remaining_free_quantityStringReturned only if GetRemainingFreeQuantity is set to TRUE.
pricing_unit_sizeStringUnit size associated with the pricing UOM.
default_pricing_uomStringTurnaround tag. Returns the DefaultPricingUOM sent in the request.
default_pricing_unit_sizeStringTurnaround tag. Returns the DefaultPricingUnitSize sent in the request.

Cart Info (required cart id and need_cart_info field)

Field NameTypeDescription
base_amountNumberSum of cart line-item amounts before discounts, coupons, or taxes.
cart_amountNumberSum of cart line-item amounts minus discounts and coupons, including taxes.
currencyObjectThe currency info for cart and checkout. ISO-4217 code, e.g. "USD".
itemsArrayList of items with id, price, ecommerce_product_id, quantity.

currency 示例 JSON:

{
	"code": "USD"
}

items 示例 JSON:

{
	"id": "907a0ef4-95d4-4bd1-b81e-08307397c0e5",
	"ecommerce_product_id": 1645,
	"price": 125.55,
	"quantity": 3
}

Socket Request Message

Example:

{
	"a1_secret_key": "xxx",
	"body": {
		"cart_id": "",
		"ecommerce_customer_id": "xxx",
		"products": [
			{
				"ecommerce_product_id": "1645",
				"quantity": 1,
				"price": "186.000000"
			}
		]
	}
}
Field NameTypeDescription
a1_secret_keyStringSecret key in Shophyve.
bodyObjectSame as API request body.
need_detailsBooleanNeed more details in response or not.

Socket Response Message

Example:

{
  is_success: true,
  had_price_changed: true,
}

{
  is_success: false,
  message: 'Please enter correct secret key and body.'
}
Field NameTypeDescription
is_successBooleanRequest success or not.
messageStringError message.
had_price_changedBooleanProduct list: The price has changed, the page needs to get the latest price again. Cart or Checkout: The latest price has been updated in BC/Shopify, the page needs to be refreshed.
dataObjectSame as API response data.

Loading content, please wait...