SUMMER SALE
Exclusive 50% OFF Discount
SECURE CHECKOUT
Contact us:info@balmbare.com
⚠️ LIMITED STOCK! Your cart is reserved for 09:49
STEP 1: PICK YOUR BUNDLE DISCOUNT
ProductPrice
Take consistently for 9 to 12 weeks for best results.
Can I buy one time?

We don’t offer one time orders. Our ingredients are rare and expensive, so we make very little on each order. Subscriptions are what keep the price this low.

Best results show after the second month, so we recommend taking it consistently.

Cancel anytime, no hassle
  • ✓We email you before every renewal, so there are never any surprises.
  • ✓Cancel in 3 clicks from your account, or email us.
  • ✓Not happy with it? Email us and we will refund you.
Order now... only 11 items left in stock!
Next restock on soon
🚚
WANT YOUR ORDER TO ARRIVE FASTER? Add Priority Shipping to your order for just $4.99. Our UPS carrier delivers to United States addresses in just 2-3 working days!
Extra More Than Collagen bag
STOCK UP AND SAVE 70% Add 1 more bag of More Than Collagen to this order, one time only, for just $14.70. That is 70% off the usual $49. Keep your joints supported without a gap, never run out mid routine, and lock in your biggest saving on an extra bag. This price is only here at checkout.
Pain Free Joints eBook cover
WALK FREELY AGAIN, ON YOUR OWN Add the Pain Free Joints eBook to your order, one time only, for just $1.99. This is about walking freely again. Walk the dog, walk the whole block, keep up on the trip, without your knees stopping you or having to wave everyone on ahead. You get a personalized diagnosis quiz plus the exact plan to get there. Real recipes, precise supplement doses, and a little known therapy called PEMF.
Read a free preview
Understanding joint pain Joint pain is never only physical. This chapter shows how it quietly takes your walking first, why the joints you stop moving are the ones that hurt the most, and how it spreads when it is left alone.
The quiz that names your pain A short questionnaire pinpoints which type of joint pain you actually have and how it works, because the path back to walking freely is different for each type. Does your pain get worse with activity and ease with rest? Did it come on slowly over months or years? Do you feel a grating sensation or hear cracking when you move the joint? You come away knowing your type, so the plan that follows fits you.
And then the way out The following chapters give you the practical changes and the exact steps to get back on your feet and do it yourself. Walk the dog, walk to the mailbox, keep up on the trip, and stop waving people on ahead. This is the life you want back, not just a quieter ache. You are not alone, and it is closer than you think.
STEP 2: CUSTOMER INFORMATION
STEP 3: SHIPPING INFORMATION
🇺🇸 +1
🇺🇸 SHIPPING FROM UNITED STATES
Your Order Is Expected to Arrive By
BILLING INFORMATION
Select the address that matches your card or payment method.
ORDER SUMMARY
🛒 Hide order summary ⌃
$104.95
1
More Than Collagen 3 Month Supply
$150
$34.95/jar
+ FREE Shipping
Save $45
Total$104.95
STEP 4: PAYMENT METHOD
All transactions are secure and encrypted
VISA
AMERICAN
EXPRESS
DISCOVER
Credit/Debit Card
VISA
AMERICAN
EXPRESS
DISCOVER
By completing my order, I agree to the Terms of Sale and Privacy Policy
● Still in Stock
Delivered to your city by September 3
VISA
AMERICAN
EXPRESS
DISCOVER
SSL SECURED PAYMENT
🛍️
Less than 1% of customers use Balmve's 60 Day Money Back Guarantee

Have a question? We are here to help!

These statements have not been evaluated by the FDA. This product is not intended to diagnose, treat, cure, or prevent any disease. Individual results vary.

What our customers are saying

★★★★★ 50,000+ Reviews
). * Owns recovery CONSUMPTION on the page without editing the front-end's checkout_mtc source (frozen seam): * 1) fetch wrapper attaches recoverToken to /api/checkout/submit (card + wallet paths). * 2) ?recover= -> GET /api/checkout/resume -> prefill contact/shipping, restore bundle, * apply the server-authoritative % discount to the shown total (display == charge, math mirrors server.js:3662). * 3) email-entry beacon -> POST /api/checkout/started so pure abandons are recoverable (server-gated by RECOVERY_EMAILS). * All DOM hooks are read-only against the page's ids (email/fname/.../osTotal/osTogglePrice, input[name=bundle], #priority), * so a front-end regen that keeps those ids keeps this working. */ (function () { 'use strict'; var TOKEN = null, PCT = 0, VARS = []; var EVCARD = null; // latest browser-encrypted card from the Evervault secure box: {number, cvc, month, year, complete} var EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/; function money(n) { return '$' + (Number(n) || 0).toFixed(2); } function parseMoney(s) { var m = String(s == null ? '' : s).replace(/[^0-9.]/g, ''); return m ? parseFloat(m) : null; } function val(id) { var e = document.getElementById(id); return e ? String(e.value || '').trim() : ''; } function fire(el) { try { el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {} } function setVal(id, v) { var el = document.getElementById(id); if (el && v != null && String(v) !== '') { el.value = v; fire(el); } } // Mobile anti-zoom: iOS Safari auto-zooms (and won't zoom back out) when you focus an input smaller than 16px. // The checkout fields are 14px, so on phones it feels stuck zoomed-in while entering address/card. Bump inputs to // 16px on small screens only (desktop unchanged). Injected here so it survives a front-end regen of checkout_mtc. try { var _z = document.createElement('style'); _z.textContent = '@media (max-width:767px){input,select,textarea{font-size:16px !important}}'; (document.head || document.documentElement).appendChild(_z); } catch (e) {} // Back after paying (Rafi 2026-10-15): the thank-you page notes the order in this tab (sessionStorage bb_paid). Back from it // showed this checkout still filled in and Pay charged again (11 repeat orders since Sep 1). A Back or back-cache return within // 30 minutes goes to that thank-you page instead; a fresh visit to the checkout works as before. function paidBack(ev) { try { var nav = performance.getEntriesByType ? performance.getEntriesByType('navigation')[0] : null; if (!((ev && ev.persisted) || (nav && nav.type === 'back_forward'))) return; var paid = JSON.parse(sessionStorage.getItem('bb_paid') || 'null'); if (paid && /^\/checkout\/thank-you\?/.test(String(paid.url)) && Date.now() - paid.at < 30 * 60000) location.replace(paid.url); } catch (e) {} } window.addEventListener('pageshow', paidBack); /* 1) attach recoverToken to the submit call, no page edits needed */ var _fetch = window.fetch; window.fetch = function (input, init) { try { var url = (typeof input === 'string') ? input : (input && input.url) || ''; if (init && typeof init.body === 'string' && /\/api\/checkout\/submit(\?|$)/.test(url)) { var b = JSON.parse(init.body), changed = false; if (b && typeof b === 'object') { if (TOKEN && !b.recoverToken) { b.recoverToken = TOKEN; changed = true; } // Evervault secure box: swap the card object for the browser-encrypted (ev:) values right before submit, // so the page's own validation/handler never has to carry (and can't mangle) the ciphertext. Card path only. if (EVCARD && EVCARD.complete && b.card) { b.card = { number: EVCARD.number, expMonth: EVCARD.month, expYear: EVCARD.year, cvv: EVCARD.cvc }; changed = true; } // separate billing: when "same as shipping" is UNticked, send the entered billing address (drives AVS + cardholder name) try { var bs = document.getElementById('billsame'); if (bs && !bs.checked && !b.billing) { var bv = function (id) { var e = document.getElementById(id); return e ? String(e.value || '').trim() : ''; }; // name = the customer's name (shipping); address = the separately-entered billing address (drives AVS + card name) var bill = { firstName: bv('fname'), lastName: bv('lname'), address1: bv('baddr1'), city: bv('bcity'), province: bv('bstate'), zip: bv('bzip'), country: (bv('bcountry') === 'Canada' ? 'CA' : 'US') }; if (bill.address1) { b.billing = bill; changed = true; } } } catch (e) {} if (changed) init = Object.assign({}, init, { body: JSON.stringify(b) }); } } } catch (e) {} return _fetch.call(this, input, init); }; function checkedId() { var r = document.querySelector('input[name=bundle]:checked'); return r ? r.value : null; } // VARS is the More Than Collagen list. A page selling another product (/c/probiotics) exposes its own list as // window.__ALLVARS, so its pick is priced as itself, not as the first collagen bundle. function findVar(id) { for (var i = 0; i < VARS.length; i++) { if (VARS[i].id === id) return VARS[i]; } var own = []; try { own = window.__ALLVARS || []; } catch (e) {} for (var j = 0; j < own.length; j++) { if (String(own[j].id) === String(id)) return own[j]; } return VARS[0] || null; } function priorityOn() { var p = document.getElementById('priority'); return !!(p && p.checked); } // NOTE: must attach the same offer gifts as the card path (checkout_mtc.html builds its cart URL with &gift=OFFER_GIFTS). // Without this, EV wallet (Apple/Google Pay) orders were created missing the free gifts. window.OFFER_GIFTS is set by // the checkout page; absent on non-offer pages → no gift param (unchanged behavior there). function cartUrlFor(v) { if (!v) return null; var g = ''; try { g = window.OFFER_GIFTS || ''; } catch (e) {} return '/api/checkout/cart?v=' + v.id + ':1' + (v.sellingPlan ? (':sp' + v.sellingPlan) : '') + (v.every ? ('&every=' + v.every) : '') + (v.weeks ? ('&weeks=' + v.weeks) : '') + foFor(v) + (priorityOn() ? '&priority=1' : '') + (g ? ('&gift=' + g) : ''); } // one time switch offer: the checkout page sets window.__CX_FO/__CX_FO_VID while the switched tier is selected, so the // EV wallet + abandon beacon carts are priced at the same switch price the card path charges (server reads &fo=). function foFor(v) { try { return (window.__CX_FO && window.__CX_FO_VID === v.id) ? ('&fo=' + encodeURIComponent(window.__CX_FO)) : ''; } catch (e) { return ''; } } function shippingFields() { return { firstName: val('fname'), lastName: val('lname'), address1: val('addr1'), city: val('city'), province: val('state'), zip: val('zip'), phone: val('phone') }; } // Custom-checkout funnel: log a 'view' once per session with the split group, so the funnel dashboard has the // "reached checkout" count (landing -> /c -> submit -> approved). try { if (!sessionStorage.getItem('cxview')) { sessionStorage.setItem('cxview', '1'); var _abm = /(?:^|;\s*)bbab=([^;]+)/.exec(document.cookie || ''); var _cxab = _abm ? decodeURIComponent(_abm[1]) : ''; var _sid = (window.crypto && crypto.randomUUID) ? crypto.randomUUID() : String(Date.now()); fetch('/api/checkout/event', { method: 'POST', headers: { 'Content-Type': 'application/json' }, keepalive: true, body: JSON.stringify({ sid: _sid, step: 'cx-checkout', action: 'view', ab: _cxab }) }).catch(function () {}); } } catch (e) {} // Split-test "tried to checkout": fire once per session when they click a PAYMENT button on /c (Complete Order / // Apple Pay / Google Pay), stamped with their split group, so the split dashboard shows payment-attempt depth. try { var _ptm = /(?:^|;\s*)bbab=([^;]+)/.exec(document.cookie || ''); var _ptab = _ptm ? decodeURIComponent(_ptm[1]) : ''; var _ptg = /^([a-z0-9-]+)\.(\d+)$/.exec(_ptab); if (_ptg) { var _ptSlug = _ptg[1], _ptGrp = parseInt(_ptg[2], 10); var _pvid = ''; try { _pvid = localStorage.getItem('bbab_cxvid') || ''; } catch (e) {} if (!_pvid) { _pvid = ((window.crypto && crypto.randomUUID) ? crypto.randomUUID() : String(Date.now()) + Math.random()); try { localStorage.setItem('bbab_cxvid', _pvid); } catch (e) {} } document.addEventListener('click', function (ev) { try { var el = ev.target; for (var n = 0; n < 6 && el; n++) { var id = el.id || ''; if (id === 'pay' || id === 'applePayButton' || id === 'googlePayButton') { if (sessionStorage.getItem('bbpt') !== '1') { try { sessionStorage.setItem('bbpt', '1'); } catch (e) {} var pl = JSON.stringify({ slug: _ptSlug, grp: _ptGrp, vid: _pvid, pt: 1 }); if (!(navigator.sendBeacon && navigator.sendBeacon('/api/ab/hit', pl))) fetch('/api/ab/hit', { method: 'POST', body: pl, keepalive: true, mode: 'no-cors' }); } break; } el = el.parentNode; } } catch (e) {} }, true); } } catch (e) {} // Separate billing address: the page has the "billing same as shipping" checkbox (#billsame, checked by default) // but no billing inputs. Inject fields IDENTICAL to the shipping form — same labels/placeholders/structure AND the // same standard address autocomplete tokens the browser uses — so browser-native address autofill works exactly like // shipping. (The page has no Google Places; shipping autofills via the browser, so we match that.) function bbSetupBilling() { try { var bs = document.getElementById('billsame'); if (!bs) return false; if (document.getElementById('baddr1')) return true; // page already ships its own billing form (e.g. /c) — do NOT inject a duplicate if (document.getElementById('bbBillWrap')) return true; var wrap = document.createElement('div'); wrap.id = 'bbBillWrap'; wrap.style.cssText = 'display:none'; wrap.innerHTML = '
' + '
' + '
' + '
' + '
'; var cr = (bs.closest && bs.closest('.check-row')) || bs.parentNode; if (!cr || !cr.parentNode) return false; cr.parentNode.insertBefore(wrap, cr.nextSibling); function tog() { wrap.style.display = bs.checked ? 'none' : 'block'; } bs.addEventListener('change', tog); tog(); return true; } catch (e) { return false; } } if (!bbSetupBilling()) { var _bt = 0, _bi = setInterval(function () { if (bbSetupBilling() || ++_bt > 40) clearInterval(_bi); }, 150); } fetch('/api/checkout/mtc-variants').then(function (r) { return r.json(); }).then(function (d) { VARS = (d && d.variants) || []; boot(); }).catch(function () { boot(); }); function boot() { var tok = null; try { tok = new URL(location.href).searchParams.get('recover'); } catch (e) {} if (tok) resume(tok); beacon(); } /* 2) RESUME: pull saved cart + current offer, prefill, restore bundle, apply discount to the shown total */ function resume(tok) { fetch('/api/checkout/resume?recover=' + encodeURIComponent(tok)).then(function (r) { return r.json(); }).then(function (d) { if (!d || !d.ok) return; // expired / invalid -> leave the page at full price // Recovery links all open /c (the collagen page). A cart saved on another product's page reopens on that page. var home = pageOf(d.ref); if (home && location.pathname.replace(/\/$/, '') !== home) { location.replace(home + location.search); return; } TOKEN = d.redeemed ? null : (d.recoverToken || tok); PCT = d.redeemed ? 0 : (Number(d.offerPct) || 0); // a used up link: prefill only, regular price, no token prefill(d); restoreBundle(d.ref); // A recovery % discount makes the server charge less than the Collect.js wallet sheet -> flag it so the // front-end hides those express buttons (amount would mismatch). The EV wallet path prices the discount itself. if (PCT > 0) { try { window.__RECOVERY_PCT = PCT; if (window.__cxRerender) window.__cxRerender(); } catch (e) {} applyOffer(PCT); try { window.__RECOVERY_DISCOUNT = true; if (window.__walletRecheck) window.__walletRecheck(); } catch (e) {} } }).catch(function () {}); } var PAGE_OF = { '52260743381277': '/c/probiotics', '52260743414045': '/c/probiotics', '52543600296221': '/c/probiotics', '50228848427293': '/c/serum', '50228848460061': '/c/serum', '50228848492829': '/c/serum', '50228819820829': '/c/oil', '50228819853597': '/c/oil', '50228819886365': '/c/oil' }; function pageOf(ref) { try { var spec = JSON.parse(atob(ref)); var it = spec && spec.items && spec.items[0]; return (it && PAGE_OF[String(it.variantId).replace(/\D/g, '')]) || null; } catch (e) { return null; } } function prefill(d) { if (d.email) setVal('email', d.email); var s = d.shipping || {}; setVal('fname', s.firstName); setVal('lname', s.lastName); setVal('addr1', s.address1); setVal('city', s.city); setVal('state', s.province || s.state); setVal('zip', s.zip); setVal('phone', s.phone); } function restoreBundle(ref) { var want = null; try { var spec = JSON.parse(atob(ref)); var it = spec && spec.items && spec.items[0]; want = it ? String(it.variantId).replace(/\D/g, '') : null; } catch (e) {} if (!want) return; var tries = 0, iv = setInterval(function () { var radios = document.querySelectorAll('input[name=bundle]'); if (radios.length) { for (var i = 0; i < radios.length; i++) { if (String(radios[i].value).replace(/\D/g, '') === want) { if (!radios[i].checked) { radios[i].checked = true; fire(radios[i]); } break; } } clearInterval(iv); } else if (++tries > 40) clearInterval(iv); }, 150); } function applyOffer(pct) { var osTotal = document.getElementById('osTotal'); if (!osTotal) return; var osToggle = document.getElementById('osTogglePrice'); var mo = new MutationObserver(sync); function observe() { mo.observe(osTotal, { childList: true, characterData: true, subtree: true }); } function sync() { var full = parseMoney(osTotal.textContent); if (full == null) return; var off = Math.round(full * pct) / 100, pay = Math.round((full - off) * 100) / 100; mo.disconnect(); // our own write must not re-trigger the observer (no compounding) osTotal.textContent = money(pay); if (osToggle) osToggle.textContent = money(pay); discRow(off, pct); observe(); } sync(); observe(); } function discRow(off, pct) { var tot = document.getElementById('osTotal'); if (!tot) return; var totRow = (tot.closest && tot.closest('.os-row')) || tot.parentNode; var row = document.getElementById('osRecoverRow'); if (!row) { row = document.createElement('div'); row.id = 'osRecoverRow'; row.style.cssText = 'display:flex;justify-content:space-between;align-items:center;color:#1e5c3a;font-weight:700;margin:4px 0'; if (totRow && totRow.parentNode) totRow.parentNode.insertBefore(row, totRow); } row.innerHTML = 'Return offer (' + pct + '% off)-' + money(off) + ''; } /* attribution of the current session (from URL + the .balmve.com bridge cookies) -> stored with the abandonment so the recovered order (which returns straight to /c via the email link, losing all of it) inherits the journey + ad match. */ function recCk(n) { var m = new RegExp('(?:^|;\\s*)' + n + '=([^;]+)').exec(document.cookie || ''); return m ? decodeURIComponent(m[1]) : ''; } function recAttrib() { var out = {}, utm = {}; try { new URLSearchParams(location.search).forEach(function (v, k) { if (k.indexOf('utm_') === 0) utm[k] = v; }); } catch (e) {} if (!Object.keys(utm).length) { var uj = recCk('bv_utm'); if (uj) { try { utm = JSON.parse(uj); } catch (e) {} } } if (Object.keys(utm).length) out.utm = utm; var fbclid = ''; try { fbclid = new URLSearchParams(location.search).get('fbclid') || ''; } catch (e) {} if (!fbclid) fbclid = recCk('bv_fbclid'); if (fbclid) out.fbclid = fbclid; var sid = ''; try { sid = new URLSearchParams(location.search).get('bv_sid') || ''; } catch (e) {} if (!sid) sid = recCk('bv_sid'); if (sid) out.eyes_sid = sid; var vid = ''; try { vid = new URLSearchParams(location.search).get('bv_vid') || ''; } catch (e) {} if (!vid) vid = recCk('bv_vid'); if (vid) out.eyes_vid = vid; var fbp = recCk('_fbp'); if (fbp) out.fbp = fbp; // Meta browser id (for CAPI match) var eref = recCk('bv_ref'); if (eref) out.eyes_ref = eref; // last external referrer (stamped as eyes_ref) // Google Ads click ids, same sources as the page's gclidData(): url, bv_ cookie/localStorage, then Google's _gcl_aw ['gclid', 'gbraid', 'wbraid'].forEach(function (k) { var v = ''; try { v = new URLSearchParams(location.search).get(k) || ''; } catch (e) {} if (!v) v = recCk('bv_' + k); if (!v) { try { v = localStorage.getItem('bv_' + k) || ''; } catch (e) {} } if (!v && k === 'gclid') { var g = recCk('_gcl_aw'); if (g) { var pr = g.split('.'); if (pr.length >= 3) v = pr.slice(2).join('.'); } } if (v) out[k] = v; }); var fbc = recCk('_fbc'); if (fbc) out.fbc = fbc; else if (out.fbclid) { var _ft = recCk('bv_fbts'); out.fbc = 'fb.1.' + ((_ft && /^[0-9]+$/.test(_ft)) ? _ft : Date.now()) + '.' + out.fbclid; } return out; } /* 3) BEACON: capture email + current cart on entry so pure abandons are recoverable (no-op until RECOVERY_EMAILS=true) */ function beacon() { var em = document.getElementById('email'); if (!em) return; var last = '', busy = false; function ping() { var email = val('email'); if (!EMAIL_RE.test(email) || email === last || busy) return; var v = findVar(checkedId()), cu = cartUrlFor(v); if (!cu) return; busy = true; fetch(cu).then(function (r) { return r.json(); }).then(function (cart) { if (!cart || !cart.ref) { busy = false; return; } last = email; return fetch('/api/checkout/started', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: email, ref: cart.ref, shipping: shippingFields(), attrib: recAttrib() }) }); }).then(function () { busy = false; }).catch(function () { busy = false; }); } em.addEventListener('blur', ping); em.addEventListener('change', ping); document.addEventListener('change', function (e) { if (e.target && (e.target.name === 'bundle' || e.target.id === 'priority')) { last = ''; ping(); } }); } // ---- Evervault secure card box (SAQ A) -------------------------------------------------------------------- // Replaces the plain card inputs (#ccnum/#ccexp/#cvc) with Evervault's hosted fields, so the real card never // touches our page or server (it arrives as ev: ciphertext; the server relays it to the processor, which is where // it's decrypted). Active only when the server injects the flag ('1'==='1' = full launch) OR the URL has // ?ev=1 (a single controlled test order). ANY SDK/mount failure reveals the original inputs and does nothing else // — the card still charges (plaintext goes direct; the fetch swap above no-ops), so a box problem never blocks pay. (function evCardSetup() { var CFG = { on: ('1' === '1') || /[?&]ev=1(?:&|$)/.test(location.search), team: 'team_626c8de911ac', app: 'app_f4e91bdb5e1f' }; if (!CFG.on || !CFG.team || !CFG.app || CFG.team.indexOf('{{') === 0) return; // off or unreplaced/unconfigured -> plain inputs function whenReady(fn) { if (document.readyState !== 'loading') fn(); else document.addEventListener('DOMContentLoaded', fn); } whenReady(function () { var numEl = document.getElementById('ccnum'); if (!numEl) return; var fields = numEl.closest('.pay-fields'); if (!fields) return; var mount = document.createElement('div'); mount.id = 'ev-card'; mount.style.padding = '16px'; // matches .pay-fields inset so the box isn't flush to the main panel border fields.parentNode.insertBefore(mount, fields); fields.style.display = 'none'; var revealed = false; function reveal() { if (revealed) return; revealed = true; try { mount.parentNode && mount.parentNode.removeChild(mount); } catch (e) {} fields.style.display = ''; } var t = setTimeout(reveal, 8000); // SDK/box never became ready -> fall back to the normal inputs function setRaw(id, v) { var el = document.getElementById(id); if (el) el.value = v; } var s = document.createElement('script'); s.src = 'https://js.evervault.com/v2'; s.onload = function () { try { var ev = new Evervault(CFG.team, CFG.app); // Theme matches the checkout's native inputs (checkout_mtc.html: .field input / .field label) with roomier // padding inside the boxes. ui.card takes the theme as { styles: {...} } DIRECTLY (Evervault docs). The old // code wrapped it again in ev.ui.themes.clean({ styles }); clean() then saw `styles` as an unknown selector // and dropped the .field input padding, so the card text sat flush to the left edge. Pass { styles } raw. var styles = { label: { fontWeight: 700, fontSize: 13.5, marginBottom: 6, color: '#333' }, '.field input': { padding: '14px 14px', border: '1px solid #cfcfcf', borderRadius: 7, fontSize: 16, color: '#333', background: '#fff' }, '.field': { marginBottom: 12 }, '.field:focus-within input': { borderColor: '#2f8f4e' }, '.error': { fontSize: 12.5, marginTop: 4 } }; var card = ev.ui.card({ fields: ['number', 'expiry', 'cvc'], theme: { styles: styles } }); card.on('ready', function () { clearTimeout(t); revealed = true; }); // mounted OK -> cancel fallback (box stays) card.on('change', function (d) { try { var c = (d && d.card) || {}; if (d && d.isComplete && c.number && c.cvc) { var mo = (c.expiry && c.expiry.month) || '', yr = (c.expiry && c.expiry.year) || ''; EVCARD = { number: c.number, cvc: c.cvc, month: String(mo), year: String(yr), complete: true }; // feed the page's pre-submit validation (number>=12 chars, exp>=3 digits, cvc>=3 digits). ev:number // has no spaces so it passes; the REAL ev: values are swapped into the request by the fetch wrapper. setRaw('ccnum', c.number); setRaw('ccexp', String(mo) + String(yr).slice(-2)); setRaw('cvc', '000'); } else { EVCARD = null; setRaw('ccnum', ''); setRaw('ccexp', ''); setRaw('cvc', ''); } } catch (e) {} }); card.mount('#ev-card'); } catch (e) { reveal(); } }; s.onerror = reveal; document.head.appendChild(s); }); })(); // ---- Evervault wallets (Apple Pay + Google Pay) — replaces the NMI Collect.js wallet buttons ---------------- // On EV_WALLETS (flag) OR ?evw=1: hide the NMI wallet buttons and mount Evervault Apple/Google Pay. The WALLET // provides the email + shipping address (that's the point of a wallet — no form typing), which we extract and send // with the charge to /api/checkout/wallet-pay (NMI·Kurv via the Evervault relay). Form fields are only a fallback. // Amount + cart ref come from ONE cart fetch per mount, so the sheet amount always equals what we charge; buttons // re-mount when the bundle/priority changes. (function evWalletSetup() { var on = ('1' === '1') || /[?&]evw=1(?:&|$)/.test(location.search); var TEAM = 'team_626c8de911ac', APP = 'app_f4e91bdb5e1f', MERCHANT = 'merchant_66b4ece7f115'; if (!on || !TEAM || !APP || !MERCHANT || TEAM.indexOf('{{') === 0 || MERCHANT.indexOf('{{') === 0) return; function whenReady(fn) { if (document.readyState !== 'loading') fn(); else document.addEventListener('DOMContentLoaded', fn); } function loadSDK(cb) { if (window.Evervault) return cb(); var s = document.createElement('script'); s.src = 'https://js.evervault.com/v2'; s.onload = cb; s.onerror = function () {}; document.head.appendChild(s); } // Collect.js re-shows its wallet buttons async, so a one-time hide loses — kill them with CSS !important instead. try { var st = document.createElement('style'); st.textContent = '#applePayButton,#googlePayButton{display:none!important}'; (document.head || document.documentElement).appendChild(st); } catch (e) {} // Normalize wallet-provided contact → our shipping shape. Apple: shipping/billingContact {givenName, familyName, // emailAddress, phoneNumber, address:{addressLines[],locality,administrativeArea,postalCode,countryCode}}. // Google: shipping/billingAddress {name, address1, locality, administrativeArea, postalCode, countryCode, phoneNumber}. function extractContact(pm) { var o = { email: '', firstName: '', lastName: '', address1: '', city: '', province: '', zip: '', country: 'US', phone: '' }; var ac = pm.shippingContact || pm.billingContact; if (ac) { o.firstName = ac.givenName || ''; o.lastName = ac.familyName || ''; o.email = ac.emailAddress || ''; o.phone = ac.phoneNumber || ''; var a = ac.address || ac; o.address1 = (a.addressLines && a.addressLines[0]) || a.address1 || ''; o.city = a.locality || a.city || ''; o.province = a.administrativeArea || a.state || ''; o.zip = a.postalCode || a.zip || ''; o.country = String(a.countryCode || a.country || 'US').toUpperCase().slice(0, 2); } var ga = pm.shippingAddress || pm.billingAddress; if (ga && !o.address1) { var nm = String(ga.name || '').trim().split(/\s+/); o.firstName = o.firstName || nm[0] || ''; o.lastName = o.lastName || nm.slice(1).join(' ') || ''; o.address1 = ga.address1 || (ga.addressLines && ga.addressLines[0]) || ''; o.city = ga.locality || ga.city || ''; o.province = ga.administrativeArea || ''; o.zip = ga.postalCode || ''; o.country = String(ga.countryCode || 'US').toUpperCase().slice(0, 2); o.phone = ga.phoneNumber || o.phone; } if (!o.email) o.email = pm.email || pm.emailAddress || ''; return o; } function formShip() { var s = shippingFields(); var c = document.getElementById('country'); s.country = (c && /canada/i.test(c.value || '')) ? 'CA' : 'US'; return s; } whenReady(function () { var wrap = document.getElementById('expWrap'); var host = document.createElement('div'); host.id = 'ev-wallets'; host.style.cssText = 'display:flex;flex-direction:column;gap:10px;margin:10px 0'; var anchor = document.getElementById('expHead') || wrap; if (anchor && anchor.parentNode) { anchor.parentNode.insertBefore(host, anchor.nextSibling); if (anchor.id === 'expHead') anchor.style.display = 'flex'; } else document.body.appendChild(host); function emsg(t) { var m = document.getElementById('ev-wallet-msg'); if (m) m.textContent = t || ''; } function cartNow(cb) { var v = findVar(checkedId()); var u = cartUrlFor(v); if (!u) return cb(null); fetch(u).then(function (r) { return r.json(); }).then(function (c) { cb(c && c.ref && c.total > 0 ? c : null); }).catch(function () { cb(null); }); } loadSDK(function () { var ev; try { ev = new Evervault(TEAM, APP); } catch (e) { return; } function pay(paymentMethod, walletName, curRef, fail) { var wc = extractContact(paymentMethod); var email = EMAIL_RE.test(wc.email) ? wc.email : val('email'); var ship = (wc.address1 && wc.city && wc.zip) ? { firstName: wc.firstName, lastName: wc.lastName, address1: wc.address1, city: wc.city, province: wc.province, zip: wc.zip, country: wc.country, phone: wc.phone } : formShip(); if (!EMAIL_RE.test(email) || !ship.address1 || !ship.city || !ship.zip) { try { fetch('/api/checkout/ev-wallet-inspect', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ wallet: walletName, paymentMethod: paymentMethod }) }); } catch (e) {} // capture the shape so we can fix the mapping emsg('We could not read your email/address from the wallet. Fill them in above and tap again.'); if (fail) fail({ message: 'Need email + address' }); return; } emsg(''); fetch('/api/checkout/wallet-pay', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(Object.assign({ wallet: walletName, ref: curRef, email: email, shipping: ship, paymentMethod: paymentMethod }, recAttrib())) }) .then(function (r) { return r.json(); }).then(function (rr) { if (rr && rr.ok) { location.href = rr.redirect || ('/checkout/thank-you?order=' + encodeURIComponent(rr.order || '')); } else { emsg((rr && rr.error) || 'Wallet payment failed.'); if (fail) fail({ message: (rr && rr.error) || 'declined' }); } }).catch(function () { emsg('Network error, please try again.'); if (fail) fail({ message: 'network' }); }); } var tries = 0, reT; function mount() { cartNow(function (c) { host.innerHTML = '
'; if (!c) { if (tries++ < 5) setTimeout(mount, 700); return; } tries = 0; var amt = Math.round(c.total * 100), ref = c.ref; // itemized breakdown for the wallet sheet (products incl. $0 gifts, then shipping). Falls back to a single // "Order" line if the parts don't sum exactly to the total (Google/Apple Pay reject a mismatched total). var li = (c.items || []).map(function (it) { return { label: (it.title || 'Item') + ((it.qty || 1) > 1 ? ' ×' + it.qty : ''), amount: Math.round((it.price || 0) * (it.qty || 1) * 100) }; }); if (c.shipping > 0) li.push({ label: 'Shipping', amount: Math.round(c.shipping * 100) }); var liSum = 0; for (var _i = 0; _i < li.length; _i++) liSum += li[_i].amount; if (!li.length || liSum !== amt) li = [{ label: 'Order', amount: amt }]; function txn() { return ev.transactions.create({ amount: amt, currency: 'USD', country: 'US', merchantId: MERCHANT, lineItems: li }); } // request email + name + address from the wallet so the customer doesn't type anything (correct Evervault // option names: Apple = requestPayerDetails + requestBillingAddress; Google = emailRequired + billingAddress) try { var apple = ev.ui.applePay(txn(), { requestPayerDetails: ['name', 'email', 'phone'], requestShipping: true, requestBillingAddress: true, style: 'black', type: 'buy', borderRadius: 8, size: { width: '100%', height: 50 }, process: function (pm, o) { return pay(pm, 'apple', ref, o && o.fail); } }); if (apple.on) apple.on('error', function () {}); apple.mount('#ev-applepay'); } catch (e) {} try { var google = ev.ui.googlePay(txn(), { allowedAuthMethods: ['PAN_ONLY', 'CRYPTOGRAM_3DS'], allowedCardNetworks: ['VISA', 'MASTERCARD', 'AMEX', 'DISCOVER'], emailRequired: true, billingAddress: true, color: 'black', type: 'buy', borderRadius: 8, size: { width: '100%', height: 50 }, process: function (pm, o) { return pay(pm, 'google', ref, o && o.fail); } }); if (google.on) google.on('error', function () {}); google.mount('#ev-gpay'); } catch (e) {} }); } mount(); document.addEventListener('change', function (e) { if (e.target && (e.target.name === 'bundle' || e.target.id === 'priority')) { clearTimeout(reT); reT = setTimeout(mount, 350); } }); }); }); })(); })();