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

1

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.

2

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.

3

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() and customer.getTierProgress() resolve with null. The methods marked Customer in the reference reject with the authentication_required error 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:

  1. The promise resolves with the result of that action right away, for example the discount code of a claimed reward. Show it immediately.
  2. The SDK marks the affected data as out of date, so the next read never returns the data from before the action.
  3. In the background, it reads again the data your page already loaded and fires the matching events, such as customer:updated with 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.