Skip to main content
Version: v1

Cart Setup

theme.liquid

theme.liquid
<html>
<head>...</head>
<body>
...

<!-- BEAM START -->
{% render 'beam-config' %}
{% render 'beam-cart-setup' %}
<!-- BEAM END -->
</body>
</html>

Base code

beam-cart-setup.liquid

The snippet below will enable automatic tracking of Shopify cart changes. This snippet must be added to theme.liquid, or to another template file which is included on all storefront pages.

beam-cart-setup.liquid
<script type="module" async crossorigin>
import { registerCartIntegration } from "https://sdk.beamimpact.com/web-sdk/{{ settings.beam_sdk_version }}/dist/integrations/shopify.js";
import { getConfig } from "https://sdk.beamimpact.com/web-sdk/{{ settings.beam_sdk_version }}/dist/integrations/beam.js";

const beam = getConfig();
await beam.readyPromise;
registerCartIntegration({
apiKey: "{{ settings.beam_api_key }}",
storeId: {{ settings.beam_store_id }},
domain: "{{ settings.beam_store_domain }}" || undefined,
});
</script>

If you are using Shopify Checkout Extensibility, you will see that all orders will include a beam attribute on them after including this script. This is necessary in order for the Beam Shopify App to record nonprofit selections by users correctly. If your store is not yet using Shopify Checkout Extensibility, you can turn off this functionality by modifying the base code above like this:

registerCartIntegration({
apiKey: "{{ settings.beam_api_key }}",
storeId: {{ settings.beam_store_id }},
domain: "{{ settings.beam_store_domain }}" || undefined,
}, false); // Using false here turns off automatic Shopify cart metadata updates for the Beam attribute

Tracking custom carts

In cases where your site is using a custom cart, or the GraphQL implementation of Shopify’s API, Beam listens for cart changes differently than using registerCartIntegration as seen above. The code below is a way to do this.

The code assumes you have access to the Shopify Storefront AJAX API (/cart.js, etc.).

beam-cart-setup.liquid

Replace

  • cartItemObserveElement - this will be the element that will be watched. Make sure that this element is as close to where the Beam widget should display as possible; since any changes to that element will trigger the observer code to run, we want to make sure that the observer callback is not called unnecessarily.
<script type="module" async crossorigin>
import {
addBeamAttributesToCart,
getCurrentCart,
trackCart,
} from "https://sdk.beamimpact.com/web-sdk/{{ settings.beam_sdk_version }}/dist/integrations/shopify.js";
import {
events,
getCookieValue,
waitForElement,
createScopedLocalStorage,
getCookieValue,
} from "https://sdk.beamimpact.com/web-sdk/{{ settings.beam_sdk_version }}/dist/integrations/utils.esm.js";
import { getConfig } from "https://sdk.beamimpact.com/web-sdk/{{ settings.beam_sdk_version }}/dist/integrations/beam.js";

// CSS Selector for the Cart DOM element that will have a MutationObserver
const cartItemObserverTargets = [".drawer__inner", ".cart-items"];

const beam = await getConfig();
await beam.readyPromise;

const beamLocalStorage = createScopedLocalStorage({ apiKey: beam.apiKey });

const getExternalCartId = function () {
return (
beamLocalStorage?.getItemJson("cart")?.cartId ?? getCookieValue("cart")
);
};

const getBeamCartId = function () {
return (
beamLocalStorage?.getItemJson("cart")?.beamCartId ??
getCookieValue("beam_cart")
);
};

const getBeamCartAttributes = async function () {
const { attributes } = await window
.fetch("/cart.js", {
method: "GET",
headers: { "Content-Type": "application/json" },
})
.then((res) => res.json());

// Get only the Beam attribute, if it exists
const existingBeamCartAttr = attributes?.beam;
let existingBeamCartAttrJson = {};
try {
if (typeof existingBeamCartAttr === "string") {
existingBeamCartAttrJson = JSON.parse(existingBeamCartAttr);
} else if (typeof existingBeamCartAttr === "object") {
existingBeamCartAttrJson = existingBeamCartAttr;
}
} catch (err) {
console.error(err);
}
return existingBeamCartAttrJson;
};

let lastSelectedNonprofitId = null; // used to avoid duplicate calls
window.addEventListener(
events.BeamNonprofitSelectEvent.eventName,
async (event) => {
const { selectedNonprofitId, selectionId } = event.detail;
if (selectedNonprofitId !== lastSelectedNonprofitId) {
await addBeamAttributesToCart({
selectedNonprofitId,
selectionId,
cartId: getExternalCartId(),
beamCartId: getBeamCartId(),
storeId: beam.storeId,
chainId: beam.chainId,
});
}
lastSelectedNonprofitId = selectedNonprofitId;
}
);

window.addEventListener(
events.BeamCartCreatedEvent.eventName,
async (event) => {
const existingBeamCartAttributes = await getBeamCartAttributes();

addBeamAttributesToCart({
...existingBeamCartAttributes,
chainId: beam.chainId,
storeId: beam.storeId,
cartId: getExternalCartId(),
beamCartId: getBeamCartId(),
});
}
);

let pauseObserver = false; // used to control throttling of API call
const cartChangeObserver = new MutationObserver(
async (mutationsList, observer) => {
if (pauseObserver) return; // throttled
await trackCart(beam, (await getCurrentCart(beam)).cart);
pauseObserver = true; // throttle for a few ms
setTimeout(() => {
pauseObserver = false;
}, 100);
}
);

cartItemObserverTargets.forEach(async (target) => {
const observerElement = await waitForElement(target);
if (observerElement) {
cartChangeObserver.observe(observerElement, {
attributes: false,
childList: true,
subtree: true,
});
}
});

// Make sure initial cart value is registered
await trackCart(beam, (await getCurrentCart(beam)).cart);
</script>