--- title: Technical Documentation --- ## 1. Introduction & Definitions **General Purpose** The primary purpose of this plugin is to act as a standardized, lightweight bridge between third-party commerce platforms (e.g., CRM, ERP, POS, e-commerce: Shopify, WooCommerce, etc.) and the SimplyClub loyalty and marketing automation ecosystem. By utilizing a pre-built connection layer (called Dropins), the plugin minimizes custom frontend development while delivering a native-feeling loyalty experience. It handles user synchronization, dynamic cart benefit application, and secure order lifecycle reporting via webhooks. **Note for product requirements:** This `Dropins.md` must be used in conjunction with the `PRD.md` file. Any technical specification generated from this document must synthesize the product requirements outlined here with the specific technical implementation details, events, and commands provided in the Dropins documentation. **Definitions & Terminology** * **SimplyClub / Simply Club / Service / Platform:** All refer to the core SimplyClub backend loyalty and marketing automation solution. * **SimplyClub Dropins / Dropins / Loyalty Dropins:** All refer to the connection layer that facilitates the integration. This encompasses the visual UI/GUI components (each individually referred to as a "dropin"), as well as the underlying SDK events, functions, and webhook architecture. * **POI (Point of Interaction) / Touch Point / Host / Host System / Hosting System / System:** All refer to the third-party platforms (e.g., E-commerce websites, ERPs, POS, or CRM systems) that are integrating with the SimplyClub solution via the Dropins. --- ## 2. Dropins Installation & Core API **Prerequisite:** Obtain your `poiId` from SimplyClub support — it identifies your store/location and is required for every Dropin to initialize. > **Shopify users:** install the official **SimplyClub Shopify app** instead — it wires up `setUser`, `setCart`, webhooks, and `transactionCode` capture automatically. The manual snippet below targets WordPress, WooCommerce, custom storefronts, and any other host system. Inject the loader before the `` tag. ```html ``` **`setConfig` options:** | Option | Type | Default | Notes | |---|---|---|---| | `poiId` | string | — *(required)* | Identifies your store/location. Get it from SimplyClub support. | | `env` | `"prod"` \| `"test"` \| `"local"` | `"prod"` | Switches API base URL. Use `"test"` for staging, `"local"` only when running the Frontend repo locally. | | `starterBtn.selector` | CSS selector | — | Use your own element as the launcher instead of the default Dropin button. | | `user` | `{ id, firstName, lastName, email, phone }` | — | Pre-seed user state at init (alternative to calling `setUser` later). | > **Language note:** the widget's UI language is **server-driven** — it is resolved from the POI's settings in the server response, not from a `setConfig` parameter. A `language_code` option is **not** consumed by the loader. ### 2.1 API Commands (Host -> Dropins) The Hosting System must call these publishers when its native state changes: * `instance.commands.show(tab?)`: Open the SimplyClub visual dropin programmatically (the dropin layer picks the right view based on user status). Optional `tab` argument jumps straight to a dashboard tab. Valid values: `"dashboard"`, `"settings"`, `"notifications"`, `"renewal"`, `"howToGetPoints"`, `"transactionBenefits"`. * `instance.commands.hide()`: Close the visual dropin. * `instance.commands.setUser({ id, firstName, lastName, email, phone })`: Sync login state. * `instance.commands.setUser(null)`: Sync logout state. * `instance.commands.logout()`: Programmatic sign-out — clears the Dropin's authenticated session (calls the SimplyClub logout API and clears `current-member`). Use this when the host's own logout fires. * `instance.commands.setCart({ token, items: [{ id, sku, title, price, quantity, ... }] })`: Sync cart mutations. **Prices must reflect native hosting system discounts.** The Dropin accepts the host's native cart shape and reads the fields it needs — most Shopify/Woo cart objects can be passed through verbatim. Fields read by the Dropin: * `id` *(required)* — line / variant ID (string or number) * `sku` *(required)* — product SKU; falls back to `id` if missing * `title` *(preferred)* or `name` — display name; `title` wins when both exist (matches Shopify cart shape) * `price` *(required)* — net unit price after host-side discounts (number) * `quantity` *(required)* — units in the line (number) * `variant_id`, `image`, `url`, `description`, `extended_attributes` *(optional)* — passed through for richer UI / rules * `token` *(optional, on the cart root — not on items)* — host cart token used by the SimplyClub transaction endpoint. **Field must be named `token`** (the dev demo's `cartToken` is silently ignored). * `instance.commands.resetBenefitSelections()`: Force recalculation/clear benefits if the cart is altered post-benefit application. **Read-only / utility command helpers** (all return Promises): * `instance.commands.getCart()` — current cart in the signal store. * `instance.commands.getSession()` — current SimplyClub session info. * `instance.commands.getTransactionCode()` — current SimplyClub `transactionCode` if a transaction is open. * `instance.commands.getBenefitSelections()` — currently selected benefits. * `instance.commands.dispatchExampleTransaction(promos?)` — returns `Promise` with a "seed" benefit-selection payload that can be persisted into the host cart on first load (used by the WordPress plugin to pre-populate the rewards screen). Resolves to `null` when no transaction code is open yet. The returned payload has `source: "modal"`. ### 2.2 API Events (Dropins -> Host) The Hosting System must subscribe and react to these events: * `instance.On.benefitSelectionsChanged((data) => { ... })`: Extract payload and apply host-side checkout discounts. See §5. The payload includes a `source: "modal" | "embed"` field — `"modal"` for the main rewards screen, `"embed"` for the in-page CTA. Hosts that want to redirect to checkout after applying benefits in the modal can use this field to skip the redirect for embed clicks. * `instance.On.transactionCodeCreated((code) => { ... })`: Store `code` as order metadata for the webhook payload. * `instance.On.redirectToLogin(() => { ... })`: Execute `window.location.href = '/login'` (or the native login URL). * `instance.On.addItemToCart((e) => { ... })`: Fires when the Dropin needs the host to add a non-membership product (e.g. a free gift redemption). Payload is `{ product, resolve, reject }`. Add `e.product` to the host cart, call `setCart()` to re-sync, then call `e.resolve()` on success or `e.reject(reason)` on failure so the Dropin can show an error. * `instance.On.membershipProductSelected((e) => { ... })`: Fires when the user selects a membership product inside the Dropin. Same `{ product, resolve, reject }` shape as `addItemToCart` — add `e.product` natively → call `setCart()` → call `e.resolve()` (or `e.reject(reason)` on failure). * `instance.On.userLoggedIn((member) => { ... })`: Fires after a successful OTP login inside the Dropin. Payload is the current member object (sourced from the `current-member` signal). Useful for refreshing the host UI or pulling personalized data. * `instance.On.goToCheckout((data) => { ... })`: Fires when the Dropin asks the host to navigate to checkout (e.g. the member taps a checkout CTA inside the dashboard). Redirect to your native checkout URL. --- ## 3. Visual Widgets (Dropins) & API Correlation The Dropins layer consists of several pre-built GUI components. The Hosting System does not need to build these interfaces; it only needs to trigger them or react to them using the Pub/Sub API. ### 3.1 Starter Button The primary gateway for club interactions. * **UI Behavior:** * **Logged Out:** Displays an entry point for the Join/Registration flow. * **Logged In:** Transforms to display the member's personal club data summary. * **Correlated API:** * Initialized via the `starterBtn.selector` config property. * The state (Logged In vs. Logged Out) is controlled strictly by the host calling `instance.commands.setUser(user)` or `setUser(null)`. ### 3.2 Mobile OTP & Verification Automated UI flows for secure authentication and mobile number validation. * **UI Behavior:** Identifies if a user is new or returning and handles the standard OTP (One-Time Password) SMS loop. * **Correlated API:** * If a guest tries to access a gated Dropin, it triggers `instance.On.redirectToLogin`. * Upon successful SimplyClub verification, it triggers `instance.On.userLoggedIn`. ### 3.3 Member Personal Club Zone A dedicated modal view for authenticated members which shows the member's club data, for example: reward list, club number, membership expiary date etc. * **UI Behavior:** Displays the user's reward list, point balance, and active tier. Crucially, it shows a real-time total of their current E-commerce cart and calculates what they can afford. * **Correlated API:** * Opened programmatically via `instance.commands.show()`. * The real-time cart total inside this UI is kept accurate by the host continuously calling `instance.commands.setCart(cart)` in the background. ### 3.4 Reward Selection Screen / Transation Benefit The interface for choosing specific transaction benefits during checkout. * **UI Behavior:** Lists available rewards (Cashback, Discounts, Free Gifts) that are mathematically compatible with the current cart. * **Correlated API:** Clicking "Apply" inside this UI locks the benefits on the server to prevent double-dipping. * Clicking "Apply" is the exact trigger that fires the `instance.On.benefitSelectionsChanged(callback)` event, passing the JSON discount data back to the Hosting System for application. * Selecting a free physical product triggers `instance.On.addItemToCart`. * Opened programmatically via `instance.commands.show('transactionBenefits')`. ### 3.5 Embedded Call to action - Register / Reward Selection This is an embedded element that can be integrated in the POI frontend UI, for example: * product pages * checkout * mini cart It has few states: * Member is not logged in to the dropins: it will show an option to login/register * Member is looged in to the dropins: it will show an option to calculate the current transaction benefits and to select the available ones for the current transaction How to apply in code: The CTA is a separate module — it must be imported once per page **in addition to** the main drop-in loader. Loading the module is what registers the `` custom element. ```html ``` The element reads `poiId` and member state from the main drop-in instance via the shared signal layer, so the main `dropInLoader.setConfig({ poiId })` must run on the same page. * **Correlated API:** * The CTA renders three internal states automatically: guest, logged-in with empty cart, logged-in with items. * In the guest state, clicking the CTA dispatches a **DOM CustomEvent** `simply-club:login` on the element (bubbles, composed). Listen on `document` to trigger your own login flow: ```html ``` * In the logged-in state, clicking "Choose benefits" reveals the embedded benefits selector. Clicking "Apply" inside it fires the same `instance.On.benefitSelectionsChanged(callback)` event as §3.4, passing the JSON discount data back to the Hosting System for application. * Selecting a membership product inside the embedded benefits view triggers `instance.On.membershipProductSelected`. --- ## 4. User Registration/Login Flows The Dropin supports three login channels. They coexist on the same POI — the widget picks one per page-load based on which signals are present. The session issued is byte-identical across all three; downstream consumers (`/my-self`, `/user-benefits`, `/logout`, secure-storage iframe) cannot tell them apart. | Channel | When to use | Shopper sees | Requires | |---|---|---|---| | **4.1 Storefront-asserted** | Host already authenticated the user and you want OTP confirmation on top | Phone + OTP screen | `setUser({ id })` synced from host | | **4.2 Independent mode (fast)** | Guest-checkout stores, microsites — no host login | Phone + OTP screen | `poi.settings.fastLoginEnabled === true` | | **4.3 Server-handoff** | Host backend already knows the user and wants zero shopper interaction | Nothing — the authenticated UI mounts directly | `poi.settings.handoffEnabled === true` + a valid handoff token in `setConfig` | The widget evaluates channels in **handoff → fast → legacy** order. A successful handoff token short-circuits everything below it; a missing/invalid handoff token silently falls through to fast-login (if enabled) or legacy. ### 4.1 Storefront-asserted identity (legacy) The classic flow — the host already has the user logged in on its own side. 1. **Host Login:** User authenticates on the Hosting System. 2. **Sync & Show:** Host system notifies Dropins of the active user via `setUser(user)` and calls `show()` to continue the flow. 3. **Validation:** User completes OTP mobile validation inside the Dropins UI. *(Note: Unauthenticated users interacting with Dropin features will trigger the `redirectToLogin` event.)* ### 4.2 Independent mode — phone + OTP A parallel channel for storefronts that don't identify the shopper themselves (guest checkout, microsites, kiosks). The widget asks for phone, sends an OTP, looks up the SimplyClub member by phone, and auto-creates the member if no match exists. Enabled per-POI via `poi.settings.fastLoginEnabled === true`. The session shape is identical to §4.1. Routes: `POST /api/v1/users/fast/get-otp/:poiId/:phone`, `POST /api/v1/users/fast/validate-otp/:poiId`. Full spec — the two-call OTP flow, new-vs-returning member handling, error codes, and session parity — is in the dedicated **[Fast Login](/content/login/fast-login)** article. ### 4.3 Server-handoff — merchant-signed token The shopper sees **no login UI at all**. A host backend that has already authenticated the shopper mints an HMAC-SHA256-signed token and passes it into the widget via `setConfig({ handoffToken })`; the widget POSTs the token to `/api/v1/users/handoff/login/:poiId` and on success the authenticated UI mounts directly. Full spec — token format, payload constraints, minting examples in Node/PHP/Python, verify-endpoint contract with the complete error-code table, admin keys CRUD, and the security model — is in the dedicated **[Login From Backend](/content/login/server-handoff-login)** article. ### 4.4 Reacting to a login on your backend (outbound webhook) Independent of which channel the shopper used, the Simply server can emit a signed **outbound webhook** to your backend on every successful login, carrying the shopper's verified identity plus a single-use SSO ticket. This is not a login channel for the widget — it is a server-to-server notification you can use to start a native storefront session. See the dedicated **[Login Through Webhook](/content/login/outbound-login-event)** article. --- ## 5. Handling Selected Benefits (`benefitSelectionsChanged`) When a user approves benefits, the `benefitSelectionsChanged` event passes a JSON payload detailing the applied discounts. ### 5.1 The Benefit Selection Payload (`selectedBenefits` JSON) The Dropins will supply the following JSON structure to the callback: ```json { "simply": { "transaction": { "number": "7770928", "docType": "sale_tran", "status": "in_progress", "date": "2025-07-13T08:06:25Z", "poi": { "platform": "shopify", "externalTransactionNumber": "333666999", "externalCustomerId": "7602153062442" }, "customer": { "internalCustomerId": 1200621, "type": "club_member", "membershipCardNumber": "2886100108", "membershipExpirationDate": "2025-07-13T08:06:25Z" }, "selectedDiscountsAndActions": { "totalSelectedDiscountAmount": 10.0, "pointsDiscounts": [ { "type": "discount", "promoId": 3168, "name": "Anniversary Reward Redemption", "description": "", "unitDiscountAmount": 5.0, "quantity": 2, "totalDiscountAmount": 10.0, "promotedItems": [] } ], "generalDiscounts": [ { "type": "cashback", "promoId": 10144, "name": "10% Off Pickles", "description": "", "unitDiscountAmount": 12, "quantity": 1, "totalDiscountAmount": 12, "promotedItems": [], "discountPercent": 0 } ] } } }, "statusInfo": { "errorCode": 0, "errorMessage": "" }, "source": "modal" } ``` ### 5.2 Implementation Guide: V1 (Total Order Discount) 1. **State Cleanup (CRITICAL):** Remove any existing native discount named `sc_club_discount` and clear any previous "Pretty Print" data from the native cart/session. 2. **Validate:** Check `statusInfo.errorCode === 0`. If > 0, halt execution, ensure the cart remains clear of SC discounts, and handle the error message. 3. **Apply Total:** Apply `simply.transaction.selectedDiscountsAndActions.totalSelectedDiscountAmount` as a single native discount at the order level. The identifier/code **must** be set to `sc_club_discount`. 4. **Pretty Print:** Iterate through the `pointsDiscounts` and `generalDiscounts` arrays. Extract the `name` properties and generate a readable string (e.g., *"SimplyClub Rewards: Anniversary Reward Redemption, 10% Off Pickles"*). Display this in the UI and save it to order metadata. 5. **Transaction Linking:** Save `simply.transaction.number` as the active `transactionCode` metadata to be sent in the final payment webhook. ### 5.3 Implementation Guide: V2 (Line-Item Allocation - Preview) For integrations requiring accurate per-item tax calculation: 1. Loop through `pointsDiscounts` and `generalDiscounts`. 2. Map the `name`, `unitDiscountAmount`, and `quantity` natively to individual line items within the host system's cart, rather than relying on the single aggregate `totalSelectedDiscountAmount`. --- ## 6. Backend Unified Webhook After a customer pays, refunds, or cancels an order on the host, the host **backend** must POST the lifecycle event to the SimplyClub order webhook. This is what closes the SimplyClub transaction, deducts/awards points, and writes back the audit trail. Without it the points/cashback are never finalized. ### 6.1 Endpoint ``` POST {BASE_URL}/api/v1/transaction/order-webhook Content-Type: application/json ``` | Environment | `BASE_URL` | |---|---| | Production | `https://dropins.simplyclub.co.il` | | Staging | `https://simply-drop-ins-2.onrender.com` | No bearer/API-key header is required on this route — the request is authenticated by `sc_transaction_code` (which is unguessable and bound to a single open session) plus the resolved `poiId`. Use HTTPS only. ### 6.2 When to send | Host event | `topic` value | Required toggle | |---|---|---| | Order paid / completed | `orders/paid` | enabled by default | | Order refunded (full or partial) | `refunds/create` | **disabled** by default — ask SimplyClub support to flip `webhookTopicToggles["refunds/create"]` on your POI | Other topics return HTTP 404 `Unhandled webhook topic`. ### 6.3 Request payload — required fields The webhook accepts the host's native order payload with **four required top-level fields added**. For `orders/paid`, `customer_id` is also required. | Field | Type | Notes | |---|---|---| | `topic` | string | `"orders/paid"` or `"refunds/create"`. | | `domain` | string | Host store domain — used for logging and admin links. | | `sc_transaction_code` | string | The `transactionCode` you captured at checkout via `On.transactionCodeCreated`. Resolves the SimplyClub session + POI. | | `order_id` | string \| number | Native order ID on the host. | | `customer_id` | string \| number | Native customer ID. Required for `orders/paid`. | | `order_number` | string \| number *(optional)* | Human-readable order number — surfaced in admin UI and SimplyClub additional info. | The rest of the body follows the **Shopify order shape** because that is what the downstream processor reads — most platforms can map their native cart cleanly to it: | Field | Type | Notes | |---|---|---| | `items` *or* `line_items` | array | Required for `orders/paid`. Each line: `{ id, price, quantity, title, sku, variant_id, discount_allocations[], extended_attributes? }`. `items` is checked first; `line_items` is the Shopify fallback. | | `discount_applications` | array | Shopify-shape array of applied discount headers. The processor matches `discount_application.title` against the SimplyClub discount label (`הנחת מועדון`) to separate SimplyClub discounts from host promo codes. | | `total_price` | string | Stored on the session for refund reconciliation. | | `refund` fields | object | For `refunds/create` — includes `order_id`, `refund_line_items[]`, `order_adjustments[]`, `note`. | ### 6.4 Example — `orders/paid` body ```json { "topic": "orders/paid", "domain": "demo-store.myshopify.com", "sc_transaction_code": "7770928", "order_id": "6647180362050", "customer_id": "7602153062442", "order_number": "1042", "total_price": "139.90", "items": [ { "id": 44550337626248, "sku": "1004", "title": "Heart Fitted Crop Top", "variant_id": 44550337626248, "price": 39.9, "quantity": 2, "discount_allocations": [ { "amount": "10.00", "discount_application_index": 0 } ] } ], "discount_applications": [ { "title": "הנחת מועדון", "value": "10.00", "value_type": "fixed_amount" } ] } ``` ### 6.5 Response shape Every response (success **and** business error) follows: ```ts { success: boolean, data: { ... }, error: any | null } ``` | HTTP | `success` | When | |---|---|---| | `200` | `true` | Transaction successfully closed in SimplyClub. `data` includes `transactionResponse`, `sessionId`, `transactionCode`, `customer: { clubId, cardNumber }`, and `updates` (tags + metafields the host should write back to the order). | | `200` | `false` | Reached SimplyClub but the call failed (validation, member not found, transaction error). `data` includes `errorCode`, `errorMessage`, `processName`, and `updates` containing an `sc_error_message` metafield + `paid_error` tag. **Do not retry** — fix the underlying issue. | | `200` | `true` | Topic toggle is off — `data: { skipped: true }`. Treat as a no-op. | | `400` | `false` | Missing required fields (`topic`, `domain`, `sc_transaction_code`, `order_id`, or `customer_id`), or POI not found. | | `404` | `false` | Unhandled `topic`. | | `500` | `false` | Uncaught server error. **Safe to retry** with the same payload. | **Idempotency:** if the session is already in a terminal state (`paid`, `refunded`, `cancelled`, `expired`, `closed`), the webhook logs a warning but does not re-process — re-delivering the same payload is safe. **Sessions are bound to one `sc_transaction_code`:** never reuse a transaction code across orders. Capture a fresh one per checkout via `On.transactionCodeCreated`. --- ## 7. Admin UI Integration & The Admin Dropin (V2 Roadmap) ### 7.1 Basic Admin Mapping (MVP) * **Metadata flags:** Save `is_simplyclub_order: true` and `simplyclub_sync_status: pending` when an order is created with a `transactionCode`. * **Webhook Response:** Parse the webhook response log. If `status: "success"`, update sync status to `approved`. If `status: "error"`, update to `failed`. Display the full Info Log in the order view. * **Manual Resync:** Expose a "Resync SC" UI button on failed orders to manually reconstruct and resend the webhook payload. ### 7.2 The Admin Dropin (J5 / Auth & Capture Recalculation) For post-checkout backoffice edits, the host will embed a specialized Admin Dropin rather than using server-to-server APIs. **Initialization (Admin Context):** ```html
``` **Admin Pub/Sub Workflow:** 1. **`adminInstance.commands.loadOrder({ originalItems, appliedDiscounts })`:** Host loads the original order state into the widget. 2. **Merchant edits order:** Merchant alters the uncaptured order natively in the host admin. 3. **`adminInstance.commands.updateOrderItems(newItems)`:** Host passes updated items array to the Admin Dropin. The Dropin calculates the delta and displays suggested adjustments. 4. **`adminInstance.On.benefitsRecalculated((data) => { ... })`:** Fired when the merchant confirms recalculation. Host receives the updated V2 allocation payload, overwrites the existing `sc_club_discount`, and captures the final payment.