SimplyClub

Frontend Embedding Guide

This guide walks you through embedding the Dropins into a website (custom HTML, WordPress, WooCommerce, or any non-Shopify storefront). It is the practical companion to the Technical Documentation — start here if you want the shortest path from “we want loyalty on our site” to “it’s live”.

Shopify? Don’t follow this guide. Install the official SimplyClub Shopify app — it wires the snippet, cart sync, webhooks, and transactionCode capture for you. The guide below targets WordPress, WooCommerce, custom HTML, and any other host system.


1. Before you embed — get your poiId

Every Dropin call is scoped to a POI (Point of Interaction) — your store/location. You cannot initialize the Dropin without one.

  1. Contact SimplyClub support / your account manager.
  2. You will receive a poiId (an opaque string, e.g. 6821f55303f86ff040461ef4).
  3. Keep it somewhere you can paste into your site’s <head> or footer script block.

That’s it for the manual step — the rest is copy-paste.


2. Minimal embed — copy this into your site

Paste this snippet once, near the end of <body> (or anywhere a <script type="module"> is allowed). Replace YOUR_POI_ID with the value from support.

<script type="module">
  import { dropInLoader } from "https://dropins.simplyclub.co.il/drop-ins/drop-in-loader/index.mjs";

  const instance = await dropInLoader.setConfig({
    poiId: "YOUR_POI_ID",
    // env: "prod",   // "prod" (default) | "test" | "local" — leave default unless support tells you otherwise
  });

  // Event wiring goes here — see §4
</script>

Tip — staging: if SimplyClub support gives you a test poiId, also pass env: "test" so the Dropin talks to the staging API.

That is enough to render SimplyClub’s default starter button and run the OTP / dashboard flows. Everything beyond this is opt-in: cart sync, user sync, custom button, embedded CTA, event handlers.

2.1 Server-handoff — skip the phone+OTP screen entirely

If your backend has already authenticated the shopper, you can hand them into SimplyClub with no shopper-visible login UI at all. Mint an HMAC-signed token on your server, pass it into setConfig({ handoffToken }), and the widget mounts the authenticated UI directly.

<script type="module">
  import { dropInLoader } from "https://dropins.simplyclub.co.il/drop-ins/drop-in-loader/index.mjs";

  await dropInLoader.setConfig({
    poiId: "YOUR_POI_ID",
    handoffToken: "{{ token your backend just minted }}",
  });
</script>

Prereqs (one-time, in the Dashboard’s Integrations tab):

  1. Mint at least one HMAC key — copy the secret from the show-once modal into your backend’s secret store.
  2. Flip the handoffEnabled toggle on.

Behavior:

  • The token is evaluated before fast-login or legacy. A valid handoff overrides both.
  • A stale, expired, or replayed token → widget falls back silently to phone+OTP (no shopper-visible error).
  • Tokens are single-use, max-age 60s, and identity is customerId + a contact (phone or email) cross-check — see the Server Handoff Login doc. Mint a fresh token per page-render — never cache.

Full spec (token format, signing algorithm, error envelope, admin keys CRUD) is in the dedicated Login From Backend article.


3. Sync host state into the Dropin

The Dropin can only personalize what you tell it about. Call these whenever native state changes:

// When the user logs in to your site
instance.commands.setUser({
  id: "host-user-123",
  firstName: "Dana",
  lastName: "Cohen",
  email: "dana@example.com",
  phone: "+972501234567", // prefilled in OTP screen
});

// When the user logs out
instance.commands.setUser(null);

// Whenever the cart changes (add / remove / quantity / discount)
instance.commands.setCart({
  token: "host-cart-token", // optional but recommended — must be `token`, not `cartToken`
  items: [
    { id: "line-1", sku: "ABC-001", title: "Olive Oil 500ml", price: 39.9, quantity: 2 },
    { id: "line-2", sku: "XYZ-777", title: "Pickles Jar",     price: 12.0, quantity: 1 },
  ],
});

Important:

  • price must be the net unit price after any native host-side discounts. SimplyClub calculates benefits on top of what the customer would actually pay.
  • Either title or name works for the display field — title is preferred (matches the Shopify cart shape, so you can usually pass the native cart object through with minimal mapping). If both are present, title wins.
  • You can include any extra Shopify/Woo fields you already have (variant_id, image, url, description, extended_attributes, etc.) — they’re stored and used for richer UI / rules. Only the fields above are required.

If the cart changes after the user has already applied benefits, call instance.commands.resetBenefitSelections() to force a recalculation.


4. Connect to events

These are the events your site must subscribe to in order to apply discounts, capture transaction codes, and route auth requests. They are exposed via instance.On.* (returns { off() } for cleanup).

EventWhen it firesWhat to do
On.benefitSelectionsChanged(data)User clicks “Apply” inside the rewards screen or embedded CTAApply data.simply.transaction.selectedDiscountsAndActions.totalAmount as a host-side discount named sc_club_discount. See §5 of the Technical Doc for the full payload.
On.transactionCodeCreated(code)A SimplyClub transaction is openedPersist code as order metadata. You will send it back in the payment webhook.
On.redirectToLogin()A guest tries to access a gated DropinRedirect to your login page or open your login modal.
On.addItemToCart(e) (reserved — not currently dispatched in the current build)Dropin asks the host to add a free / reward item to the cartAdd e.product to your native cart → call setCart(...) → call e.resolve() on success.
On.membershipProductSelected(e)User picks a membership SKU in the DropinAdd e.productsetCart(...)e.resolve().
On.userLoggedIn(member)OTP succeeded inside the DropinOptional — refresh your UI / treat the user as authenticated on your side.
On.goToCheckout(data) (reserved — not currently dispatched in the current build)Dropin asks the host to navigate to checkoutRedirect to your native checkout URL (e.g. window.location.href = '/checkout').

The benefit-selections payload also carries data.source: "modal" | "embed""modal" for the main rewards screen, "embed" for the in-page CTA. Use it to decide whether to redirect to checkout (modal) or stay on the page (embed).

Example wiring:

instance.On.benefitSelectionsChanged((data) => {
  if (data.statusInfo.errorCode !== 0) return; // halt + clear any sc_club_discount
  const total = data.simply.transaction.selectedDiscountsAndActions.totalAmount;
  applyHostDiscount({ code: "sc_club_discount", amount: total });
  saveTransactionCode(data.simply.transaction.number);
  if (data.source !== "embed") window.location.href = "/checkout";
});

instance.On.transactionCodeCreated((code) => {
  saveOrderMetadata({ simplyTransactionCode: code });
});

instance.On.redirectToLogin(() => {
  window.location.href = "/login";
});

// Reserved — not currently dispatched in the current build.
instance.On.addItemToCart((e) => {
  addToHostCart(e.product)
    .then(() => { instance.commands.setCart(getCurrentHostCart()); e.resolve(); });
});

instance.On.membershipProductSelected((e) => {
  addToHostCart(e.product)
    .then(() => { instance.commands.setCart(getCurrentHostCart()); e.resolve(); });
});

5. Optional — embedded Call-to-Action

For inline placements (product page, mini-cart, sidebar) use the membership CTA web component. It renders the join/upgrade card and the embedded benefits selector, all in one element.

Load the module once (separate from the main loader):

<script type="module">
  import "https://dropins.simplyclub.co.il/drop-ins/membership-cta-web-component/index.mjs";
</script>

Then drop the element anywhere:

<simply-club-membership-cta
  style="display:block; max-width:480px; margin:24px auto;">
</simply-club-membership-cta>

The CTA reads its poiId and member state from the main drop-in instance, so dropInLoader.setConfig({ poiId }) must run on the same page.

The CTA dispatches one DOM event you should listen for — the guest-state login click:

<script>
  document.addEventListener('simply-club:login', () => {
    // Open your login modal, or:
    window.location.href = '/login';
  });
</script>

Benefit application from inside the embedded CTA still fires the same instance.On.benefitSelectionsChanged event — there is nothing extra to wire.


6. WordPress recipe

WordPress has no special API for Dropins — it’s plain HTML + script. Pick whichever insertion point you prefer:

  • Header/footer plugin (e.g. Insert Headers and Footers, WPCode) — paste the §2 snippet into the footer section. Survives theme updates.
  • footer.php — paste before </body>. Will be overwritten by theme updates unless you use a child theme.
  • Custom HTML block — for inline placement of <simply-club-membership-cta> inside a page or post. WordPress will strip custom elements from paragraph blocks; the Custom HTML block preserves them verbatim.
  • Custom HTML widget — for sidebar / footer widget areas.

6.1 WooCommerce — full integration patterns

The recipes below are drawn from the production wp-dropins plugin (Simply Club DropIns for WooCommerce) and address every gotcha that comes up in a real WC integration.

Use wp_localize_script to bridge PHP → JS. Configure the dropin from your plugin/theme so secrets and per-user data come from the server:

wp_localize_script('simply-dropins-wc', 'simplyDropinsWcData', [
  'poiId'            => $poi_id,
  'env'              => $env,                                            // "prod" | "test" | "local"
  'ajaxUrl'          => admin_url('admin-ajax.php'),
  'nonce'            => wp_create_nonce('simply_dropins_nonce'),
  'user'             => $current_user_data,                              // { id, firstName, lastName, email, phone } or null
  'loginUrl'         => wc_get_page_permalink('myaccount'),
  'checkoutUrl'      => wc_get_checkout_url(),
  'hasSavedBenefits' => !empty(WC()->session->get('simply_benefits')),
]);

Init:

const data = window.simplyDropinsWcData;
const instance = await dropInLoader.setConfig({
  poiId: data.poiId,
  env: data.env || 'prod',
});

Note — widget language is server-driven. The widget loads its language from the SimplyClub server (Hebrew); there is no client-side language config option to set. Any language_code / languageCode value passed to setConfig is currently ignored.

Sync user + cart:

if (data.user) {
  instance.commands.setUser(data.user);
} else {
  instance.commands.logout();
  instance.commands.hide();
}

Cart-sync with a re-entrancy guard. WooCommerce fires wc_fragments_refreshed and updated_checkout as a side-effect of our own AJAX, which would re-sync the cart and reset the freshly-applied benefits. Listen only to genuine user mutations and guard against your own work:

let isApplyingBenefits = false;

const syncCart = (isCartChange) => {
  jQuery.post(data.ajaxUrl, { action: 'simply_get_cart', nonce: data.nonce }, (res) => {
    if (!res.success) return;
    instance.commands.setCart(res.data);
    if (isCartChange && instance.commands.resetBenefitSelections) {
      instance.commands.resetBenefitSelections();
    }
  });
};

syncCart(false); // initial sync — no reset

// Listen to GENUINE cart mutations only.
// Do NOT listen to wc_fragments_refreshed or updated_checkout — they self-trigger
// from the apply-benefits flow and create a reset loop.
jQuery(document.body).on('added_to_cart removed_from_cart updated_cart_totals', (e) => {
  if (isApplyingBenefits) return;       // our own AJAX → ignore
  syncCart(true);                       // user-initiated → reset selections
});

Apply benefits + WC fees. On benefitSelectionsChanged, POST the selection to your AJAX handler, which stores it in WC()->session and turns it into a negative add_fee() ('sc_club_discount'). Skip the checkout redirect when source === 'embed' so the user stays put:

instance.On.benefitSelectionsChanged((sel) => {
  isApplyingBenefits = true;
  jQuery.post(data.ajaxUrl, {
    action: 'simply_apply_benefits',
    nonce: data.nonce,
    benefits: sel || [],
  }, (res) => {
    isApplyingBenefits = false;
    if (!res.success) return;
    if (sel.source !== 'embed' && data.checkoutUrl) {
      window.location.href = data.checkoutUrl;
    } else {
      jQuery(document.body).trigger('update_checkout');
      jQuery(document.body).trigger('wc_fragment_refresh');
      instance.commands.hide();
    }
  });
});

PHP side (in your AJAX handler):

WC()->session->set('simply_benefits', $benefits);
WC()->cart->calculate_totals();
// elsewhere, on woocommerce_cart_calculate_fees:
$cart->add_fee(__('SimplyClub Rewards', 'simply-dropins-wc'),
               -$discount_amount, true, 'sc_club_discount');

Resume-after-login. Guests who hit a gated screen are sent to /my-account/. Save the current URL before redirecting, then re-open the dropin once they come back:

const LOGIN_RETURN_KEY = 'simplyclub-session-login-redirect';

instance.On.redirectToLogin(() => {
  try { localStorage.setItem(LOGIN_RETURN_KEY, window.location.href); } catch {}
  window.location.href = data.loginUrl;
});

// After init, if a saved return URL exists and we now have a user, re-open
if (data.user && localStorage.getItem(LOGIN_RETURN_KEY)) {
  localStorage.removeItem(LOGIN_RETURN_KEY);
  setTimeout(() => instance.commands.show(), 1000);
}

Force-logout cookie. When the WP user signs out, the dropin’s own session needs to be cleared too. Set a short-lived cookie from PHP on wp_logout, then read & honour it from JS:

const forceLogout = document.cookie.split(';').some(
  c => c.trim().startsWith('simply_force_logout=')
);
if (forceLogout) {
  instance.commands.logout();
  instance.commands.hide();
  // clear the cookie across every plausible domain scope
}
// Also wire common logout selectors:
jQuery(document.body).on('click', 'a[href*="action=logout"], a[href*="customer-logout"]', () => {
  instance.commands.logout();
  instance.commands.hide();
});

Seed the rewards screen on first load. dispatchExampleTransaction() returns an example benefits payload that pre-populates the rewards UI so a returning customer sees something interesting immediately. Only do this once per session, and only if the user hasn’t already chosen something:

if (!data.hasSavedBenefits && instance.commands.dispatchExampleTransaction) {
  instance.commands.dispatchExampleTransaction().then((seed) => {
    if (!seed) return;             // null when no transactionCode yet
    persistBenefits(seed);          // your own AJAX → simply_apply_benefits
  });
}

Custom CTA button. A storefront / page-builder (Elementor, etc.) can drop a styled button that opens the rewards screen directly:

<button id="sc_open_starter_cta">Select Benefits</button>
<script>
  jQuery(document.body).on('click', '#sc_open_starter_cta', (e) => {
    e.preventDefault();
    instance.commands.show('transactionBenefits');
  });
</script>

Real WC cart payload shape. simply_get_cart returns a richer object than the minimal { token, items }. Pass-through is fine — the Dropin reads what it needs:

{
  token: 'cust42_md5OfCartContents',
  items: [
    {
      id: 'wc_line_key_abc',           // line key
      product_id: 123,
      variant_id: 456,
      title: 'Olive Oil 500ml',
      price: 39.9,
      final_price: 35.9,               // after WC discounts
      compare_at_price: 49.9,          // when on sale
      quantity: 2,
      sku: '1004',
      discounts: [{ title: 'SUMMER10', amount: 4 }],
      total_discount: 8,
      discount_allocations: [{ amount: '8.00', discount_application_index: 0 }],
      extended_attributes: { tags: [...], collections: [...], metafields: [] }
    }
  ],
  discount_codes: ['SUMMER10'],
  discount_applications: [
    { title: 'SUMMER10', value: '8.00', value_type: 'fixed_amount', target_type: 'order', ... }
  ],
  totals: { subtotal: 79.8, total: 71.8, discount_total: 8 }
}

Important — fee filtering. When you build discount_applications, skip fees whose ID starts with sc_club_. Those are SimplyClub’s own discounts — reporting them back as external promos would cause SimplyClub to deduct points for amounts the customer didn’t actually pay.

Token formula. A cart token whose value changes on every mutation (e.g. customerId + '_' + md5(items + discount_applications)) lets the Dropin’s backend detect a stale benefit calculation. A static token defeats the staleness check.

6.2 Where to put the CTA in WooCommerce

For the mini-cart the drawer is re-rendered after every AJAX add-to-cart. Use a MutationObserver with a duplicate guard:

<script>
  (function () {
    function insertCTA() {
      const target = document.querySelector('.woocommerce-mini-cart, .widget_shopping_cart_content');
      if (!target) return;
      if (target.querySelector('simply-club-membership-cta')) return; // duplicate guard
      const cta = document.createElement('simply-club-membership-cta');
      cta.style.cssText = 'display:block; width:95%; margin:16px auto;';
      target.prepend(cta);
    }
    new MutationObserver(insertCTA).observe(document.body, { childList: true, subtree: true });
    insertCTA();
  })();
</script>

For WooCommerce checkout and my-account pages, the markup is static — static placement (template part or Custom HTML block) is enough; the observer is unnecessary overhead. The wp-dropins plugin uses woocommerce_before_cart / woocommerce_before_checkout_form action hooks to echo the element server-side.


7. Custom starter button

To use your own button as the launcher (instead of the default Dropin button):

<button id="my-club-btn">Join the Club</button>

<script type="module">
  import { dropInLoader } from "https://dropins.simplyclub.co.il/drop-ins/drop-in-loader/index.mjs";
  await dropInLoader.setConfig({
    poiId: "YOUR_POI_ID",
    starterBtn: { selector: "#my-club-btn" },
  });
</script>

The Dropin attaches a click handler that opens the modal and follows the state machine (guest → OTP, member → dashboard).

You can also open the modal imperatively from any code path:

instance.commands.show();                       // default tab (auto-picks based on auth state)
instance.commands.show('transactionBenefits');  // jump straight to the benefits screen
instance.commands.hide();

Valid show() tab names: "dashboard", "settings", "notifications", "renewal", "howToGetPoints", "transactionBenefits". Any other value falls through to the default view.

Other useful commands:

instance.commands.logout();              // sign the member out of SimplyClub (call when host logs out)
instance.commands.getCart();             // Promise<currentCart>
instance.commands.getSession();          // Promise<sessionInfo>
instance.commands.getTransactionCode();  // Promise<string|null>
instance.commands.getBenefitSelections();// Promise<currentlySelectedBenefits>

7.1 Opening a specific tab from your own elements

To open the Dropin straight to a tab from your own buttons or links — including a reusable data-sc-open pattern for wiring many elements at once (the recipe used in our Shopify integration) — see the dedicated guide: Open a Specific Tab.


8. Troubleshooting

SymptomLikely cause
Nothing appears on the pagepoiId is wrong, or the loader URL was blocked (check the Network tab and console).
<simply-club-membership-cta> renders emptyThe CTA module import is missing or hasn’t run yet. Confirm the <script type="module"> is present and the URL is reachable.
CTA appears multiple times in the mini-cartThe duplicate guard (querySelector('simply-club-membership-cta')) is missing from your MutationObserver.
simply-club:login fires but nothing happensYour listener is missing or your login modal selector is wrong.
Events not firingSubscriptions must be registered after setConfig() resolves. Use await or .then(...).
Benefits applied but cart shows wrong totalsetCart was not called with net prices after host discounts, or resetBenefitSelections() was not called after a post-apply cart change.
OTP screen shows the wrong phoneThe Dropin uses setUser({ phone }) — make sure you sync it on login.

9. What’s next

  • Webhook — your backend must POST order lifecycle events (order_paid, order_cancelled, order_refunded) to the SimplyClub Unified Webhook, including the captured transactionCode. See §6 of the Technical Documentation.
  • Admin Dropin — for post-checkout edits in your backoffice (V2 roadmap). See §7 of the Technical Documentation.