GemPages is a Shopify page builder that lets you design custom product and landing pages to optimize conversions.
Because GemPages product pages are custom-built, the RecurrinGO subscription widget is added with a small code snippet placed in a custom code block, rather than through the usual selector setup. You paste the snippet in the GemPages builder and set a few values in its config section.
⚠️ Note: Depending on how your page is built, this integration may need some manual adjustment. If the widget doesn't appear or behaves unexpectedly, our support team can help — reach out via live chat or at [email protected]
How it works
The snippet:
renders RecurrinGO's native subscription widget on the page — the exact same widget (frequencies, discounts, save badges, prices, translations) that RecurrinGO shows on regular theme product pages. Nothing is hand-built, so it always matches your RecurrinGO rule;
reads the product, variant and price automatically from the theme;
reuses the page's existing "Add to cart" button — when a subscription frequency is selected, clicking it adds the item as a subscription; a one-time selection uses the page's normal add-to-cart.
Because the widget comes from the app, there is no manual frequency / selling plan configuration — it's driven entirely by your RecurrinGO rule (store, collection or product scoped).
Prerequisites: Before you start
The widget appears only when all of these are true:
RecurrinGO is installed on your store.
The RecurrinGO app embed is enabled in your live theme (Shopify admin → Online Store → Themes → Customize → App embeds → enable RecurrinGO). This is what loads RecurrinGO on GemPages pages.
There is an active RecurrinGO rule that covers the product on the page.
If any of these is missing, the page stays a normal product page (fail-soft) and a diagnostic message is logged to the browser console.
The full code to paste is in Step 2 below.
Installation
Step 1 — Add a Custom HTML element
In the GemPages editor, drag a Custom HTML / Liquid (Code) element onto the page where the subscription options should appear — typically next to the variant selector or just above the Add to cart button.
Step 2 — Paste the snippet
Copy the entire block below and paste it into the Custom HTML element.
<div id="ros-gp-widget" hidden>
<div id="ros-gp-widget-slot"></div>
<!-- Fallback button — only shown if no existing ATC button is reused. -->
<button type="button" id="ros-gp-atc" style="display:none;margin-top:10px">Add to cart</button>
<p id="ros-gp-status" role="status" style="min-height:1em;margin:6px 0"></p>
</div>
<script>
(function () {
'use strict';
// ============================ CONFIG ============================
var CONFIG = {
// Leave null to auto-detect from window.ShopifyAnalytics.meta. Set explicitly
// only to override (e.g. a fixed variant on a landing page).
variantId: null,
productId: null,
// CSS selector of the page's existing "Add to cart" button to reuse.
// Leave null to default to the app-configured selector (api.getAtcSelector()).
atcSelector: null,
quantity: 1,
// What to do after a successful subscribe add so the theme cart reflects it.
// The public API never reloads the page — the integrator decides.
// 'reload' | 'cart' (go to /cart) | 'none' (leave it to your own cart refresh)
afterAdd: 'cart',
// How long to wait for the RecurrinGO bundle before giving up (ms).
readyTimeoutMs: 8000
};
// ================================================================
// ---- Resolve product data from the theme meta ---------------------------
// Returns {productId, variantId, price} — price in CENTS (Shopify meta units),
// or null if the current product/variant can't be determined.
function resolveProduct() {
var meta = (window.ShopifyAnalytics && window.ShopifyAnalytics.meta) || {};
var product = meta.product || {};
var variants = product.variants || [];
var productId = CONFIG.productId || product.id || null;
var variantId = CONFIG.variantId
|| meta.selectedVariantId
|| (variants[0] && variants[0].id)
|| null;
if (!variantId) { return null; }
var variant = variants.find(function (v) { return v.id === +variantId; }) || variants[0] || {};
var price = typeof variant.price === 'number' ? variant.price : null;
return { productId: productId, variantId: +variantId, price: price };
}
// ---- API readiness (event first, then poll, then time out to null) -------
// Returns Promise<api|null>. null == RecurrinGO not installed / embed off /
// bundle not loaded on this page — always feature-detect, never assume.
function whenRosApiReady(timeoutMs) {
function pick() {
var s = window.Spurit || {};
var key = Object.keys(s).find(function (k) { return s[k] && s[k].publicMethods; });
return key ? s[key].publicMethods : null;
}
return new Promise(function (resolve) {
var existing = pick();
if (existing) { resolve(existing); return; }
var done = false;
function finish(api) { if (!done) { done = true; resolve(api); } }
document.addEventListener('spurit:ros:public-methods-ready', function (e) {
var name = e && e.detail && e.detail.appName;
var api = name && window.Spurit[name] && window.Spurit[name].publicMethods;
finish(api || pick());
}, { once: true });
// Fallback poll in case the event fired before we subscribed.
var waited = 0, step = 200;
var iv = setInterval(function () {
var api = pick();
if (api) { clearInterval(iv); finish(api); return; }
waited += step;
if (waited >= timeoutMs) { clearInterval(iv); finish(null); }
}, step);
});
}
// ---- Cart helpers --------------------------------------------------------
function afterAdd() {
if (CONFIG.afterAdd === 'reload') { window.location.reload(); }
else if (CONFIG.afterAdd === 'cart') { window.location.href = '/cart'; }
// 'none' → integrator refreshes their own cart drawer here.
}
// Add the variant as a subscription (single /cart/add.js via the public API).
function subscribe(api, product, sellingPlanId) {
return api.addToCart({
variantId: product.variantId,
productId: product.productId,
sellingPlanId: sellingPlanId,
quantity: CONFIG.quantity
});
}
// ---- ATC wiring ----------------------------------------------------------
// Reuse the page's existing ATC button: intercept its click in the capture
// phase and, ONLY when a subscription frequency is selected, add as a
// subscription instead of the page's native add. One-time selection is left to
// the page's own button. Returns true if an existing button was wired.
function wireExistingAtc(api, product, handle, atcSelector) {
if (!atcSelector || !document.querySelector(atcSelector)) { return false; }
document.addEventListener('click', function (e) {
var button = e.target.closest && e.target.closest(atcSelector);
if (!button) { return; }
var sellingPlanId = handle.getSellingPlanId(); // null => one-time selected
if (!sellingPlanId) { return; } // let the page add the one-time item natively
// Stop the page's own add-to-cart, run ours instead.
e.preventDefault();
e.stopImmediatePropagation();
subscribe(api, product, sellingPlanId).then(afterAdd).catch(function (err) {
console.warn('[ros-gp] subscribe failed:', err && err.message ? err.message : err);
});
}, true); // capture: runs before the button's own handlers
return true;
}
// Fallback: render our own button when no existing ATC button is reused.
function wireOwnAtc(api, product, handle) {
var atcBtn = document.getElementById('ros-gp-atc');
var status = document.getElementById('ros-gp-status');
atcBtn.style.display = 'block';
atcBtn.addEventListener('click', async function () {
atcBtn.disabled = true;
status.textContent = 'Adding…';
try {
var sellingPlanId = handle.getSellingPlanId();
if (sellingPlanId) {
await subscribe(api, product, sellingPlanId);
} else {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({ id: product.variantId, quantity: CONFIG.quantity })
}).then(function (r) { if (!r.ok) { throw new Error('add.js failed: ' + r.status); } });
}
status.textContent = 'Added to cart.';
afterAdd();
} catch (err) {
status.textContent = 'Error: ' + (err && err.message ? err.message : err);
atcBtn.disabled = false;
}
});
}
// ---- Widget wiring -------------------------------------------------------
function initWidget(api, product) {
var root = document.getElementById('ros-gp-widget');
var slot = document.getElementById('ros-gp-widget-slot');
if (!root || !slot) { return Promise.resolve(false); }
return api.renderWidget({
element: slot,
variantId: product.variantId,
productId: product.productId,
price: product.price
}).then(function (handle) {
if (!handle) {
// No active rule covers this product — leave the page as a plain product.
return false;
}
var atcSelector = CONFIG.atcSelector
|| (typeof api.getAtcSelector === 'function' ? api.getAtcSelector() : null);
if (!wireExistingAtc(api, product, handle, atcSelector)) {
wireOwnAtc(api, product, handle);
}
root.hidden = false;
return true;
});
}
// ---- Boot ----------------------------------------------------------------
whenRosApiReady(CONFIG.readyTimeoutMs).then(function (api) {
if (!api) {
// RecurrinGO not available on this page — fail soft, keep the page usable.
console.warn('[ros-gp] RecurrinGO public API not available (app not installed, embed off, or unsupported page).');
return;
}
if (typeof api.renderWidget !== 'function') {
// Older app build without the native-widget renderer — fail soft.
console.warn('[ros-gp] RecurrinGO renderWidget is unavailable — update the app to a build that exposes it.');
return;
}
var product = resolveProduct();
if (!product) {
console.warn('[ros-gp] Could not resolve product/variant from the theme meta — set CONFIG.variantId/productId manually.');
return;
}
initWidget(api, product).catch(function (err) {
console.warn('[ros-gp] Failed to render RecurrinGO widget:', err);
});
});
})();
</script>
Step 3 — (Optional) adjust the configuration
The snippet works with zero configuration on a standard product page. Edit the CONFIG block at the top of the snippet only if you need to override a default:
Field | Default | When to change it |
| auto (theme meta) | Pin a specific variant, e.g. a single-product landing page. |
| auto (theme meta) | Needed only if auto-detection fails; required to match collection/product-scoped rules. |
| app-configured selector | The page's Add to cart button differs from the one set in RecurrinGO settings, or you want a specific button. |
|
| Add more than one unit per click. |
|
|
|
|
| Slow stores where RecurrinGO loads late. |
Step 4 — Publish and test
Save and publish the GemPages page, open it on the storefront, and:
confirm the subscription widget appears;
switch between one-time and a subscription frequency and check the prices;
add to cart with a subscription selected and confirm the cart line shows the subscription.
Finding product / variant IDs (only if you pin them)
Auto-detection covers the normal case; set the IDs manually only for landing pages or when detection fails.
Product ID — open the product in Shopify admin; it's the number at the end of the URL:
admin.shopify.com/store/<store>/products/1234567890.Variant ID — on the storefront product page, pick the variant and read
?variant=9876543210from the URL, or use the product JSON at/products/<handle>.js.
Troubleshooting
Open the browser console (F12) on the storefront page and look for [ros-gp] messages:
Console message | Cause | Fix |
| App not installed, embed off, or bundle didn't load on this page | Enable the RecurrinGO app embed (Prerequisite 2). |
| The store is on an older app build | Update RecurrinGO to a build that exposes |
| Not a product-context page (e.g. a generic landing page) | Set |
Widget renders but nothing happens on Add to cart | The page's button isn't matched by | Set |
No widget, no error | No active rule covers this product | Create/enable a RecurrinGO rule for the product, collection, or store. |
Notes & limitations
Prices come from the theme meta (
ShopifyAnalytics.meta). If a page doesn't expose them, the widget still renders with its frequencies and badges; only the price figures are blank.Custom (customer-chosen) frequency rules are not supported by this embedded widget — use a rule with fixed frequencies.
Reusing the page's Add to cart button relies on intercepting its click. This works for standard button/form add-to-cart. If a particular GemPages layout triggers add-to-cart another way, set
CONFIG.atcSelectoror rely on the fallback button the snippet renders.This integration may need some manual adjustment depending on the page structure. If you get stuck, contact RecurrinGO support via live chat or email.
Need help?
If you have any questions or need help with the setup, please contact our support team.
📩 Contact Support: Reach out to us via live chat or send an email to [email protected]