Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.orderprotection.com/llms.txt

Use this file to discover all available pages before exploring further.

The Order Protection widget integrates with Ordergroove and your Shopify store, enhancing the subscription order experience for users with all relevant information in one place.The widget addition to the cart simplifies the claim-filing process for users reducing friction and eliminating the need for manual navigation to the Order Protection website.

Installation

1

Generate an API Token

Access the Ordergroove admin page: Ordergroove Admin
  • Go to Developers menu and click on API Keys
  • Click on Create button
  • Enter a Name and Email
    • Name: Order Protection
    • Email: onboarding@orderprotection.com
    • Permissions: Read and Write (required — Read Only will cause the Send Now flow to fail with a 401 when Order Protection tries to sync its line item onto the order)
    • Allow bulk queries?: Yes
  • Click on Save button
  • Copy the API Key for Step 3
API Token
2

Create a Webhook

Since Ordergroove does not have a way to automate the creation of webhooks, we will need to create them manually.
  • Go to Developers menu and click on Webhooks
  • Click on Create button
  • Enter a Name and Email
    • Webhook Name: Order Protection
    • Webhook URL: https://subscriptions.production.orderprotection.com/api/v1/webhooks/ordergroove/[PLATFORM_DOMAIN]/webhook
      • Replace [PLATFORM_DOMAIN] with your platform domain. e.g. for Shopify it would be example.myshopify.com
  • For events, tick only subscription.create
    • Note: there is a similar event called subscriber.create, please note to tick the correct one Webhook
  • No actions on Advance Settings
  • Click on Save button
  • Copy the Verification Key for Step 3
Webhook
3

Integrate Ordergroove within the Order Protection Platform

Within the Order Protection app, do the following:
  • Navigate to Integrations in the left navigation.
  • Find the Ordergroove block under the Available tab.
  • Take the API token you created in Step 1 and the Webhook Verification Key you created in Step 2 and add it to the corresponding fields and save.
  • Once saved, click on the Active tab and the API token and Webhook Verification Key fields within the Ordergroove block will have a hidden value. This means that your token and webhook successfully uploaded. ordergroove api token in op app
4

Install Scripts in Ordergroove's Subscription Manager

  • Go to Subscriptions menu and click on Subscription Manager
  • Click on Advanced toggle button
  • On the side menu, click on View
  • Click on ADD NEW FILE, and name it orderprotection.liquid with content:
    orderprotection.liquid
    {# required non-null: order, subscriptions, current_order_items #}
    <div style="padding-left: 12px; padding-right: 12px;">
      <order-protection instance-id="{{ order.public_id }}"></order-protection>
    </div>
    {{ 'bootstrapOrderProtection({ order, subscriptions, current_order_items })' | js }}
    
  • Go to /views/order/main.liquid, enter new line after {% include 'order-summary/main' %} and add:
    order/main.liquid
    `{% include 'orderprotection' %}`
    
    Like below: Paste code
  • Go to /views/order-level-actions/send-order-now.liquid and change the button:
    order-level-actions/send-order-now.liquid
    <button class="og-button" type="submit" name="send_now">{{ 'modal_send_now_save' | t }}</button>
    
    to:
    order-level-actions/send-order-now.liquid
    <button class="og-button" type="button" @click="{{ 'orderProtectionSendNow' | js }}" data-order-id="{{ order.public_id }}">
      {{ 'modal_send_now_save' | t }}
    </button>
    
  • Lastly, go to /scripts/script.js and add the following code to the bottom of the file:
    script.js
    /*
     * BEGIN ORDER PROTECTION ORDERGROOVE MANAGER
     *
     * Loads the OP subscriptions script and initialises one widget per OG order.
     *
     * Public API (called from the Liquid template):
     *   bootstrapOrderProtection(params)   – called once per OG order row
     *   orderProtectionSendNow(event)      – wired to the "Send Now" dialog button
     */
    
    /* ─── Config ─────────────────────────────────────────────────────────────── */
    
    const OP_DEBUG = false;
    const OP_LOG_PREFIX = '[OP]';
    
    /* ─── Utilities ──────────────────────────────────────────────────────────── */
    
    function opLog(...args) {
      if (OP_DEBUG) console.log(OP_LOG_PREFIX, ...args);
    }
    
    function opLoadAsset({ tag, attrs, parent = document.head }) {
      return new Promise((resolve, reject) => {
        const el = Object.assign(document.createElement(tag), attrs, {
          onload: () => resolve(el),
          onerror: reject,
        });
        parent.appendChild(el);
      });
    }
    
    function opQuerySelector(orderId, selector) {
      return document.querySelector(`.og-content-wrapper[data-order-id="${orderId}"] ${selector}`);
    }
    
    function opCreateEl(tag, props = {}, children = []) {
      const el = document.createElement(tag);
      Object.assign(el, props);
      el.append(...children);
      return el;
    }
    
    /* ─── Asset loading ──────────────────────────────────────────────────────── */
    
    // Per-store URL derived from the Shopify subdomain — no per-merchant editing.
    function opResolveScriptUrl() {
      const shop = window.Shopify && window.Shopify.shop;
      const slug = shop ? shop.replace(/\.myshopify\.com$/i, '') : '';
      if (!slug) {
        console.error('[OP] Cannot determine shop slug from window.Shopify.shop — subscriptions widget will not load.');
        return null;
      }
      return `https://cdn.orderprotection.com/subscriptions-widget/${slug}/widget.js`;
    }
    
    const OP_SCRIPT_URL = opResolveScriptUrl();
    
    if (OP_SCRIPT_URL) {
      opLoadAsset({ tag: 'script', attrs: { src: OP_SCRIPT_URL }, parent: document.body })
        .then(() => opLog('script loaded'))
        .catch((err) => opLog('script load error', err));
    }
    
    /* ─── UI update (called on every toggle) ─────────────────────────────────── */
    
    function opOnToggled(orderId, enabled) {
      const instance = window.orderProtection?.[orderId];
      if (!instance) return opLog('onToggled: no instance for', orderId);
    
      const { format, symbol } = instance.adapter.createCurrencyFormatter();
      const subtotal = instance.cart.subtotal;
      const total = enabled ? subtotal + instance.price : subtotal;
    
      // Price heading
      const priceHeading = opQuerySelector(orderId, '.og-summary-heading.og-price-heading');
      if (priceHeading) priceHeading.textContent = format(total);
      else opLog('priceHeading not found:', orderId);
    
      // Order total
      const totalEl = opQuerySelector(orderId, '.og-total');
      if (totalEl) {
        totalEl.childNodes.forEach((node) => {
          if (node.textContent?.includes(symbol)) node.textContent = format(total);
        });
      } else {
        opLog('totalEl not found:', orderId);
      }
    
      // Sub-total (excludes OP line item)
      const subtotalEl = opQuerySelector(orderId, 'dd[data-testid="order-sub-total"]');
      const subTotalWithoutOp = instance.adapter.itemsSubtotal;
    
      if (subtotalEl && subTotalWithoutOp) subtotalEl.textContent = format(subTotalWithoutOp);
      else opLog('subtotalEl not found or zero:', orderId);
    
      // Cost breakdown row
      const breakdownList = opQuerySelector(orderId, 'div.og-cost-breakdown > dl');
      if (breakdownList) {
        const existing = breakdownList.querySelector('.op-cost-breakdown-item');
        if (enabled && !existing) {
          breakdownList.appendChild(
            opCreateEl('div', { className: 'op-cost-breakdown-item' }, [
              opCreateEl('dt', { textContent: 'Shipping Protection' }),
              opCreateEl('dd', { className: 'og-text-align-right', textContent: format(instance.price) }),
            ])
          );
        }
        if (!enabled) {
          breakdownList.querySelectorAll('.op-cost-breakdown-item').forEach((el) => el.remove());
        }
      } else {
        opLog('breakdownList not found:', orderId);
      }
    }
    
    /* ─── Widget initialisation ──────────────────────────────────────────────── */
    
    /**
     * Called from the Liquid template once per OG order row.
     * Pushes params to the queue immediately (before the script finishes loading),
     * so the subscriptions widget drains and creates the instance as soon as it's ready.
     */
    function bootstrapOrderProtection(params) {
      const { order } = params;
    
      opLog('bootstrapOrderProtection', order.public_id);
    
      // Use the queue pattern: push now, entry.ts creates the instance on load.
      window.orderProtection = window.orderProtection || [];
    
      // If the script is already loaded, window.orderProtection is the live registry object.
      if (typeof window.orderProtection.create === 'function') {
        initWidget(params);
        return;
      }
    
      // Script not yet loaded — push to queue; entry.ts will call create() when ready.
      // We still need to wire up the toggle handler after the instance is created,
      // so we poll once per frame until the instance appears in the registry.
      window.orderProtection.push({
        integration: 'ordergroove',
        instanceId: order.public_id,
        preexistingData: params,
      });
    
      waitForInstance(order.public_id).then((instance) => {
        if (!instance) return opLog('timed out waiting for instance', order.public_id);
        wireInstance(instance, order.public_id);
      });
    }
    
    /** Called when the script is already loaded (queue already drained). */
    function initWidget(params) {
      const { order } = params;
      const instanceId = order.public_id;
    
      if (window.orderProtection[instanceId]) {
        return opLog('already initialised:', instanceId);
      }
    
      opLog('initWidget', instanceId);
    
      const instance = window.orderProtection.create({
        integration: 'ordergroove',
        instanceId,
        preexistingData: params,
        debug: OP_DEBUG,
      });
    
      wireInstance(instance, instanceId);
    }
    
    /** Attach the toggle UI-update listeners for this instance. */
    function wireInstance(instance, instanceId) {
      // ── Store-specific overrides ──────────────────────────────────────────────
      // Uncomment and customize if your store needs different behavior:
      //
      // instance.adapter.sendNow = async () => { /* custom send-now logic */ };
      // Object.defineProperty(instance.adapter, 'itemsSubtotal', { get: () => 0 });
      // ─────────────────────────────────────────────────────────────────────────
    
      instance.on('user-preference/loaded', (data) => opOnToggled(instanceId, data.preference));
      instance.on('protection/toggled', (data) => opOnToggled(instanceId, data.checked));
    }
    
    /**
     * Poll until window.orderProtection[instanceId] exists (entry.ts populated it)
     * or until timeout. Returns the instance or null.
     */
    function waitForInstance(instanceId, timeoutMs = 5000) {
      return new Promise((resolve) => {
        const start = Date.now();
        const check = () => {
          const instance = window.orderProtection?.[instanceId];
          if (instance) return resolve(instance);
          if (Date.now() - start > timeoutMs) return resolve(null);
          requestAnimationFrame(check);
        };
        check();
      });
    }
    
    /* ─── Send Now ───────────────────────────────────────────────────────────── */
    
    /**
     * Called from the Liquid template when the customer clicks the "Send Now" button
     * inside the OG confirmation dialog. This replaces the default OG send-now handler
     * so that the OP line item is synced into the order before it is dispatched.
     */
    async function orderProtectionSendNow(e) {
      opLog('orderProtectionSendNow');
    
      const btn = e?.currentTarget;
      if (!btn) return opLog('missing btn');
    
      const dialog = btn.closest('dialog');
      const orderId = btn.dataset.orderId || dialog?.querySelector('input[name="order"]')?.value;
      const instance = orderId ? window.orderProtection?.[orderId] : null;
    
      if (!dialog || !orderId || !instance) {
        opLog('missing params — dialog:', !!dialog, 'orderId:', orderId, 'instance:', !!instance);
        return;
      }
    
      const btnText = btn.textContent;
      btn.disabled = true;
      btn.textContent = 'Loading...';
    
      try {
        // 1. Sync the OP line item into the OG order (pass the live toggle state so
        //    the server adds or removes OP to match what the customer is looking at).
        await instance.adapter.syncOpItem(instance.enabled);
    
        // 2. Send the order now via the OG REST API
        await instance.adapter.sendNow();
    
        // 3. Close dialog + refresh OG page state
        dialog.close();
        await window.og.smi.refresh_page_state();
      } catch (err) {
        console.error('[OP] send_now error:', err);
      } finally {
        btn.disabled = false;
        btn.textContent = btnText;
      }
    }
    
    /*
     * END ORDER PROTECTION ORDERGROOVE MANAGER
     */
    
Once set up, Order Protection will be added to all cart instances within Shopify subscription orders using Ordergroove. Customers can choose to opt-in or opt out of Order Protection for their subscription orders.
Customers will be able to file/edit claims per your normal store settings once an order confirmation email has been sent.