--- title: 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 `` 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 `` (or anywhere a ` ``` > **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. ```html ``` **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**](/content/login/server-handoff-login) article. --- ## 3. Sync host state into the Dropin The Dropin can only personalize what you tell it about. Call these whenever native state changes: ```js // 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). | Event | When it fires | What to do | |---|---|---| | `On.benefitSelectionsChanged(data)` | User clicks "Apply" inside the rewards screen or embedded CTA | Apply `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 opened | Persist `code` as order metadata. You will send it back in the payment webhook. | | `On.redirectToLogin()` | A guest tries to access a gated Dropin | Redirect 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 cart | Add `e.product` to your native cart → call `setCart(...)` → call `e.resolve()` on success. | | `On.membershipProductSelected(e)` | User picks a membership SKU in the Dropin | Add `e.product` → `setCart(...)` → `e.resolve()`. | | `On.userLoggedIn(member)` | OTP succeeded inside the Dropin | Optional — 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 checkout | Redirect 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: ```js 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): ```html ``` Then drop the element anywhere: ```html ``` 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**: ```html ``` 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 ``. Will be overwritten by theme updates unless you use a child theme. * **Custom HTML block** — for inline placement of `` 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: ```php 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**: ```js 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**: ```js 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: ```js 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: ```js 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): ```php 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: ```js 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: ```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: ```js 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: ```html ``` **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: ```js { 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: ```html ``` 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): ```html ``` 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: ```js 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: ```js instance.commands.logout(); // sign the member out of SimplyClub (call when host logs out) instance.commands.getCart(); // Promise instance.commands.getSession(); // Promise instance.commands.getTransactionCode(); // Promise instance.commands.getBenefitSelections();// Promise ``` ### 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](/content/dropins/open-a-specific-tab)**. --- ## 8. Troubleshooting | Symptom | Likely cause | |---|---| | Nothing appears on the page | `poiId` is wrong, or the loader URL was blocked (check the Network tab and console). | | `` renders empty | The CTA module import is missing or hasn't run yet. Confirm the `