This guide is for developers building custom storefront pages. If you only want a ready-made loyalty page, use the Shopify Loyalty Hub instead.
The Storefront SDK is a small JavaScript library that the JeriCommerce app adds to your Shopify theme. It gives your own pages the same loyalty data and actions the Loyalty Hub uses: the customer's points, tier and progress, rewards, discount codes, ways to earn, points history and wallet card.
You write the HTML, the styles and the layout. The SDK takes care of the rest: it signs the customer in through Shopify, calls the JeriCommerce API, caches what you read, refreshes your data after every action and tells your page when something changes.
When to use the SDK ¶
- You want a loyalty page with your own design, beyond what the Loyalty Hub settings allow.
- You want to show loyalty data in other places of your theme, such as the header, the cart or a product page.
- You want to open the JeriCommerce widget from your own buttons, or hide its launcher on some pages.
Set it up ¶
Turn on the SDK app embed
Go to Shopify Admin > Online Store > Themes > Customize > App embeds and turn on SDK from the JeriCommerce app. Save the theme.
This app embed loads the SDK on every page of your store. It is the same embed that shows the Storefront Widget. The widget is optional: you can turn it off in JeriCommerce and the SDK keeps working.
Add your code to the theme
Add your page as a theme section (for example sections/jeri-loyalty.liquid) and add that section to a page template. The custom loyalty page guide walks through every step with a complete template.
Wait for the SDK before using it
Run your code as a deferred script, a module, or after DOMContentLoaded. Then check that the SDK is installed and wait for it with JeriCommerce.ready().
When the SDK app embed is turned off, window.JeriCommerce does not exist. Always check for it and show a short message instead of a broken page.
Quick start ¶
<div data-jeri-points>Loading your points...</div>
<script type="module">
const box = document.querySelector("[data-jeri-points]");
if (!window.JeriCommerce) {
box.textContent = "Loyalty is not available right now.";
} else {
const jeri = await window.JeriCommerce.ready();
const session = await jeri.auth.getSession();
if (!session.authenticated) {
box.innerHTML = '<button type="button">Sign in to see your points</button>';
box.querySelector("button").addEventListener("click", () => jeri.auth.login());
} else {
const render = data => {
box.textContent = `You have ${data?.customer.loyaltyData?.balance ?? 0} points`;
};
render(await jeri.customer.get());
jeri.on("customer:updated", render);
}
}
</script>
JeriCommerce.ready() downloads the SDK the first time you call it and always resolves with the same instance. It does not load any data until you call a method.
How sign-in works ¶
The SDK always acts for the customer signed in to your Shopify store. There are only two states:
- Visitor:
auth.getSession()resolves with{ authenticated: false }. Public data works (program, tiers, public rewards, ways to earn, public boosts).customer.get()andcustomer.getTierProgress()resolve withnull. The methods marked Customer in the reference reject with theauthentication_requirederror code. - Signed-in customer:
{ authenticated: true }. Everything works for that customer.
The SDK never accepts a token, an email or a customer id, so a page can never read or change another customer's data. To ask a visitor to sign in, call jeri.auth.login(). It sends them to the Shopify login and back to the current page, or to the page you pass as returnTo.
What happens after an action ¶
When a customer claims a reward, completes a way to earn, or updates their profile:
- The promise resolves with the result of that action right away, for example the discount code of a claimed reward. Show it immediately.
- The SDK marks the affected data as out of date, so the next read never returns the data from before the action.
- In the background, it reads again the data your page already loaded and fires the matching events, such as
customer:updatedwith the new balance.
Your page never needs to call refresh() after an action. Subscribe to the events you display and render them again when they fire. The points history, the referral link and the wallet links have no event. To keep the history up to date, call history.list() again when customer:updated fires.
Methods ¶
Every method returns a promise. Methods marked Customer need a signed-in customer and reject with authentication_required for visitors. Methods marked Public work for everyone.
Session and program
| Method | Access | Resolves with |
|---|---|---|
auth.getSession() |
Public | { authenticated: boolean } |
auth.login({ returnTo? }) |
Public | Sends the visitor to the Shopify login. returnTo is a path of your store starting with /, otherwise the call rejects with invalid_argument. By default, the current page |
program.get() |
Public | Program |
config.get() |
Public | ProgramConfig. widget holds the active widget settings |
Customer
| Method | Access | Resolves with |
|---|---|---|
customer.get() |
Public | CustomerData, or null for a visitor |
customer.getTierProgress() |
Public | TierProgressStatus, or null for a visitor |
customer.updateProfile(fields) |
Customer | Customer. fields: { firstName?, lastName?, phone?, birthdate? } |
customer.updatePreferences(preferences) |
Customer | Customer. See preferences |
Tiers and rewards
| Method | Access | Resolves with |
|---|---|---|
tiers.list() |
Public | Tier[] |
rewards.list({ scope: "public" }) |
Public | PublicRewardDto[], the catalog for visitors |
rewards.list({ scope: "customer" }) |
Customer | Reward[], with redeemable for this customer |
rewards.claim({ rewardId, pointsToRedeem? }) |
Customer | ClaimedReward. pointsToRedeem, a positive whole number, only for rewards of type dynamic |
Discount codes and ways to earn
| Method | Access | Resolves with |
|---|---|---|
discounts.list({ page?, limit? }) |
Customer | PaginatedCustomerCouponCodes |
earnings.list() |
Public | EngagementFlowWithProgramEngagementFlowDto[] |
earnings.execute({ earningId, action: "flow" }) |
Customer | ExecuteCustomEarningFlowResponseDto |
earnings.execute({ earningId, action: "click", linkId }) |
Customer | Nothing. Records the click on a social network of a Follow us earning |
Boosts, referrals, history and wallet
| Method | Access | Resolves with |
|---|---|---|
boosts.list({ scope, window? }) |
Public or Customer | CustomerBoostsResponseDto. scope is "public" or "customer" |
referrals.getLink() |
Public | The customer's referral link, or null |
history.list({ page?, limit? }) |
Customer | PaginatedTransactions |
wallet.getLinks() |
Public | { ios, android, generic } links to add the loyalty card to a wallet, or null |
wallet.sendEmail() |
Customer | Nothing. Emails the wallet card to the customer |
Widget, refresh and events
| Method | Access | Resolves with |
|---|---|---|
widget.open({ page? }) |
Public | Opens the widget. page is one of the widget pages |
widget.close() |
Public | Closes the widget |
widget.showLauncher() / widget.hideLauncher() |
Public | Shows or hides the widget button on this page view |
refresh() |
Public | Reads the session and every resource your page already loaded again |
on(event, callback) |
Public | A function that removes the subscription |
When you leave out page and limit, the API uses its own defaults.
Data you can render ¶
All results are the objects of the JeriCommerce API, unchanged. These are the fields you will use most.
CustomerData
Returned by customer.get() and sent with customer:updated.
| Field | Type | Description |
|---|---|---|
customer.email |
string | Customer email |
customer.userData.firstName / lastName |
string | Profile name |
customer.loyaltyData.balance |
number | Points the customer can spend |
customer.loyaltyData.referralLink |
string | Referral link (also available with referrals.getLink()) |
customer.customerTier.currentTier |
Tier or null |
Current VIP tier |
customer.customerTier.currentTierPoints |
number | Points that count towards tiers |
customer.balanceNotifications, tierNotifications, locationNotifications, rewardAvailableNotifications, marketingNotifications |
boolean | Notification preferences |
engagementFlows |
object | Progress per way to earn, keyed by earning type (for example CUSTOM_1). count is how many times the customer completed it |
TierProgressStatus
Returned by customer.getTierProgress(). It is the same calculation the Loyalty Hub uses.
| Field | Type | Description |
|---|---|---|
currentTier |
Tier or null |
Current tier |
nextTier |
Tier or null |
Next tier, or null at the top tier |
pointsToNextTier |
number | Tier points missing to reach nextTier |
progressValue |
number | Progress across all tiers, from 0 (first tier) to 100 (top tier). Use it as a percentage for a progress bar |
sortedTiers |
Tier[] |
All tiers from lowest to highest |
Tier
| Field | Type | Description |
|---|---|---|
id |
string | Tier id |
name |
string | Tier name |
requiredPoints |
number | Tier points needed to reach it |
content |
string | Benefits text set in JeriCommerce (HTML). Sanitize it before you insert it with innerHTML |
image |
string or null |
Tier image |
Reward and PublicRewardDto
rewards.list({ scope: "customer" }) returns Reward, and rewards.list({ scope: "public" }) returns PublicRewardDto.
| Field | Type | Description |
|---|---|---|
id |
string | Use it as rewardId to claim |
title, description, image |
string | Reward content |
type |
"fixed" or "dynamic" |
Fixed reward or Points as cash |
cost (customer) / redeemCost (public) |
number | Points a fixed reward costs |
minPoints, maxPoints, currencyPerPoint |
number or null |
Limits and value of a Points as cash reward |
redeemable (customer) |
boolean | The customer can claim it now |
nonRedeemableReason (customer) |
string | Why not, for example NOT_ENOUGH_BALANCE |
ClaimedReward
Returned by rewards.claim().
| Field | Type | Description |
|---|---|---|
code |
string | Discount code to show the customer |
pointsRedeemed |
number, optional | Points spent |
discountValue |
number, optional | Value of the discount |
expiresAt |
string or null |
Expiry date |
PaginatedCustomerCouponCodes
Returned by discounts.list(). data is the list of codes; count, total, page and pageCount describe the page. Each code has value (the code), expiresAt, discountValue, claimed and coupon (the coupon it belongs to).
Ways to earn
earnings.list() returns one entry per earning type. Each entry has name (for example FOLLOW_US, MEMBER_GET_MEMBER, CUSTOM_1), active, and programEngagementFlows, the program's setup for that type. Use the first item:
Field of programEngagementFlows[0] |
Description |
|---|---|
id |
Use it as earningId in earnings.execute() |
active |
Only show active earnings |
title, description, buttonText, url |
Content set in JeriCommerce |
displayIconUrl |
Icon |
properties.points |
Points awarded |
properties.executeOnClick |
true when the customer earns by pressing a button: call earnings.execute({ earningId, action: "flow" }). It never applies to FOLLOW_US, MEMBER_GET_MEMBER or COMPLETE_USER_PROFILE, which reject with invalid_argument |
properties.followUs |
Social networks of a Follow us earning, keyed by network (instagram, facebook, ...). Each one has username and points |
properties.requiredFields |
Profile fields a Complete profile earning needs |
A click needs the network key as linkId, for example earnings.execute({ earningId, action: "click", linkId: "instagram" }). The SDK records the link set in JeriCommerce; it never opens a window, so open the social page yourself.
earnings.execute() with action: "flow" resolves with pointsAwarded, remainingExecutions and recurrence.
PaginatedTransactions
Returned by history.list(). Each item of data has name, amount, balanceChange, status and createdAt.
Boosts
boosts.list() resolves with { boosts }. Each boost has name, startsAt, endsAt and isActive. The SDK refreshes the list when a boost starts or ends.
Update preferences ¶
customer.updatePreferences() needs every notification flag, plus language if you want to change it:
const data = await jeri.customer.get();
await jeri.customer.updatePreferences({
balanceNotifications: data.customer.balanceNotifications,
tierNotifications: data.customer.tierNotifications,
locationNotifications: data.customer.locationNotifications,
rewardAvailableNotifications: data.customer.rewardAvailableNotifications,
marketingNotifications: true,
});
Requests always use the language of the page. Saving a language preference refreshes the customer's data but does not change the language of the page.
Events ¶
jeri.on(event, callback) returns a function that removes the subscription. Events also fire the first time your page reads that data, and on() doesn't send the last value again. Render what each read resolves with, as in the quick start, and use the events for later changes.
| Event | When it fires | Callback receives |
|---|---|---|
session:changed |
The session is first known, and whenever it changes | { authenticated } |
customer:updated |
The customer's data changed, for example the balance after a claim | CustomerData, or null for a visitor |
rewards:updated |
A list of rewards your page read was refreshed | { scope, rewards }, with the same list as rewards.list({ scope }) |
discounts:updated |
A list of discount codes your page read was refreshed | { query, discounts }, with the { page, limit } you read and a PaginatedCustomerCouponCodes |
earnings:updated |
The ways to earn were refreshed | The same list as earnings.list() |
boosts:updated |
A boost started or ended, or the boosts were refreshed | { scope, boosts }, with the same value as boosts.list({ scope }) |
Events only report data changes. The result of an action is the value its promise resolves with.
Errors ¶
Every failed call rejects with a StorefrontSdkError. Check its code, not the message text:
| Code | Meaning |
|---|---|
invalid_argument |
The call has a missing or wrong value, for example an empty rewardId |
authentication_required |
The call needs a signed-in customer, or the customer's session expired |
session_unavailable |
The customer's session could not be loaded. Try again later; it is never treated as a visitor |
api_error |
JeriCommerce rejected the request. status has the HTTP status and serverCode the API code, such as NOT_ENOUGH_POINTS |
network_error |
A read did not reach JeriCommerce. An action that doesn't reach it rejects with mutation_unconfirmed |
widget_unavailable |
The widget is turned off, or its settings or code could not load, so it cannot open |
mutation_unconfirmed |
The action may not have been applied, for example because it did not reach JeriCommerce. Read the data again before retrying |
The widget and the SDK ¶
The SDK can control the Storefront Widget on the current page: jeri.widget.open(), close(), showLauncher() and hideLauncher(). These calls only affect the page view; the saved widget settings do not change. When the widget is turned off in JeriCommerce, widget.open() rejects with widget_unavailable and everything else keeps working.
Widget pages ¶
widget.open({ page }) accepts these pages:
page |
Opens |
|---|---|
advantages |
Program benefits |
profile |
Profile |
orders |
Orders |
coupons |
Discount codes |
rewards |
Rewards |
loyalty |
Ways to earn |
referrals |
Referrals |
Visitors always see the program benefits and sign-up page.
Limitations ¶
- The SDK is only available on Shopify storefronts through the app embed. There is no npm package.
- Shopify's Canvas theme editor does not support app embeds yet, so the SDK is not available on Canvas themes.
- The SDK does not create accounts or sign customers up. Visitors sign in through Shopify.
- Points are never calculated in the browser. If the API still returns the previous balance, that is the latest data until the next read.
Examples ¶
Claim a reward and show the code
try {
const claimed = await jeri.rewards.claim({ rewardId: reward.id });
showCode(claimed.code); // The balance updates through "customer:updated".
} catch (error) {
if (error.code === "api_error" && error.serverCode === "NOT_ENOUGH_POINTS") {
showMessage("You need more points for this reward.");
} else {
showMessage("Something went wrong. Please try again.");
}
}
Claim a Points as cash reward
await jeri.rewards.claim({ rewardId: reward.id, pointsToRedeem: 200 });
Earn points with a button
const result = await jeri.earnings.execute({ earningId: earning.id, action: "flow" });
showMessage(`+${result.pointsAwarded} points`);
Open the widget on the rewards page
document.querySelector("[data-open-rewards]").addEventListener("click", () => {
jeri.widget.open({ page: "rewards" }).catch(() => {
window.location.href = "/pages/loyalty"; // The widget is turned off.
});
});
Keep the balance in the header up to date
const badge = document.querySelector("[data-points-badge]");
const show = data => {
badge.hidden = !data;
if (data) badge.textContent = data.customer.loyaltyData.balance;
};
show(await jeri.customer.get());
jeri.on("customer:updated", show);
A complete page built with these methods is in the custom loyalty page guide.