1/** 2 * Roadtrippers embedded checkout bridge. 3 * 4 * Turns each `.rt-checkout-embed` mount into a cross-origin iframe pointed at the 5 * shared /checkout app and runs the documented postMessage contract: 6 * iframe -> parent: checkout:ready | checkout:resize | checkout:loading 7 * checkout:error | checkout:success 8 * parent -> iframe: checkout:init { options } 9 * 10 * Analytics (Trial Started / Purchase Succeeded / location events) fire INSIDE 11 * the iframe, so this bridge only handles layout + the WordPress success UI. 12 */ 13(function () { 14 'use strict'; 15 16 var ORIGIN = (window.rtCheckoutConfig && window.rtCheckoutConfig.origin) || 'https://maps.roadtrippers.com'; 17 var TIERS = ['basic', 'pro', 'premium']; 18 19 function buildSrc(tier) { 20 var src = ORIGIN + '/checkout?host=wordpress'; 21 if (tier && TIERS.indexOf(tier) !== -1) { 22 src += '&tier=' + encodeURIComponent(tier); 23 } 24 return src; 25 } 26 27 // Tier selection mirrors the legacy component: it follows the URL hash 28 // (#basic / #pro / #premium). Both in-page tier links and the popup-opener 29 // buttons that launch the modal set this hash (the latter via main.js), so a 30 // single hashchange listener covers every entry point. 31 function tierFromHash() { 32 var h = (window.location.hash || '').replace(/^#/, '').split(/[?&]/)[0]; 33 return TIERS.indexOf(h) !== -1 ? h : null; 34 } 35 36 // Non-pinned iframes that should re-point when the tier hash changes. 37 var tieredFrames = []; 38 39 function parseOptions(mount) { 40 var raw = mount.getAttribute('data-rt-checkout'); 41 if (!raw) { 42 return {}; 43 } 44 try { 45 return JSON.parse(raw) || {}; 46 } catch (e) { 47 return {}; 48 } 49 } 50 51 function setSuccessCookie() { 52 // Reuse the site's cookie helper so the flag matches the legacy checkout 53 // path exactly (expiry/path/domain), keeping getCookie() reads consistent. 54 if (typeof window.setCookie === 'function') { 55 window.setCookie('rt_purchase_success', true, false); 56 return; 57 } 58 // Fallback mirroring that helper: 365-day expiry, path=/, no domain. 59 var date = new Date(); 60 date.setTime(date.getTime() + 365 * 24 * 60 * 60 * 1000); 61 document.cookie = 'rt_purchase_success=true;expires=' + date.toUTCString() + ';path=/'; 62 } 63 64 // Container/success pairs the embed can drive: standard purchase and upgrade. 65 var SUCCESS_PAIRS = [ 66 { form: '.sign-up-plus-form-purchase', success: '.sign-up-plus-form-success' }, 67 { form: '.upgrade-plus-form', success: '.upgrade-form-success' } 68 ]; 69 70 /** 71 * Replicate the theme's post-purchase behaviour: swap the form (purchase or 72 * upgrade) for its success panel, flag the purchase, and reload on plus-deals. 73 */ 74 function handleSuccess(mount, detail) { 75 var scope = mount.closest('.sign-up-inner-wrapper') || document; 76 77 // Prefer the pair whose form actually contains this mount. 78 var pair = null; 79 for (var i = 0; i < SUCCESS_PAIRS.length; i++) { 80 if (mount.closest(SUCCESS_PAIRS[i].form)) { 81 pair = SUCCESS_PAIRS[i]; 82 break; 83 } 84 } 85 if (!pair) { 86 pair = SUCCESS_PAIRS[0]; 87 } 88 89 var form = mount.closest(pair.form) || scope.querySelector(pair.form) || document.querySelector(pair.form); 90 var success = scope.querySelector(pair.success) || document.querySelector(pair.success); 91 if (form) { 92 form.style.display = 'none'; 93 } 94 if (success) { 95 success.style.display = 'block'; 96 } 97 98 setSuccessCookie(); 99 100 // Let the rest of the site react (e.g. analytics handled in-iframe already). 101 document.dispatchEvent(new CustomEvent('rtCheckoutSuccess', { detail: detail || {} })); 102 103 // The plus-deals page re-renders member state on reload. 104 if (document.getElementById('prmcd-plus-deals')) { 105 window.location.reload(); 106 } 107 } 108 109 // The checkout only self-serves upgrades for Stripe-billed subscriptions. 110 var STRIPE_SOURCES = ['stripe', 'trv']; 111 112 function subscriptionSource() { 113 var g = window.rtGlobalObj; 114 var data = g && g.user && g.user.data; 115 if (data && data.subscription_plan && data.subscription_plan.pro_source) { 116 return data.subscription_plan.pro_source; 117 } 118 if (g && g.tracking && g.tracking.properties) { 119 return g.tracking.properties.subscription_source || null; 120 } 121 return null; 122 } 123 124 /** 125 * Non-Stripe subscriptions (e.g. billed via Apple) can't be upgraded through 126 * the embedded checkout, so swap the upgrade embed for a manual-upgrade 127 * message. Only acts when the source is known and not Stripe-billable. 128 */ 129 function handleUpgradeFallback() { 130 var mounts = document.querySelectorAll('.upgrade-plus-form .rt-checkout-embed'); 131 if (!mounts.length) { 132 return; 133 } 134 var source = subscriptionSource(); 135 if (!source || STRIPE_SOURCES.indexOf(source) !== -1) { 136 return; 137 } 138 Array.prototype.forEach.call(mounts, function (mount) { 139 var form = mount.closest('.upgrade-plus-form'); 140 if (!form) { 141 return; 142 } 143 form.style.display = 'none'; 144 var scope = mount.closest('.sign-up-inner-wrapper') || document; 145 var fallback = scope.querySelector('.upgrade-manual-fallback'); 146 if (fallback) { 147 fallback.style.display = 'block'; 148 } 149 }); 150 } 151 152 var modalOpenHandled = false;
153 154 /** 155 * Restore the legacy deep-link behaviour: landing on a page with a tier hash 156 * (#basic / #pro / #premium) opens the enclosing purchase modal, and the 157 * IntersectionObserver then builds the iframe on that tier. Runs on userReady 158 * so the membership type is known. Limited to logged-in non-subscribers 159 * ('free'), the only state that renders the tier checkout. 160 */ 161 function openPurchaseModalForHash() { 162 if (modalOpenHandled || tierFromHash() === null) { 163 return; 164 } 165 166 var type = (window.rtGlobalObj && window.rtGlobalObj.user) ? window.rtGlobalObj.user.type : null; 167 if (type !== 'free') { 168 return; 169 } 170 171 var mount = document.querySelector('.popup .rt-checkout-embed'); 172 var popup = mount && mount.closest('.popup'); 173 if (!popup || popup.classList.contains('popup_visible')) { 174 return; 175 } 176 177 modalOpenHandled = true; 178 document.documentElement.classList.add('overflowed'); 179 document.body.classList.add('overflowed'); 180 popup.classList.add('popup_visible'); 181 var overlay = document.getElementById('overlay'); 182 if (overlay) { 183 overlay.classList.add('overlay_visible'); 184 } 185 } 186 187 function createBridge(mount) { 188 var options = parseOptions(mount); 189 190 var frame = document.createElement('iframe'); 191 frame.className = 'rt-checkout-iframe'; 192 frame.setAttribute('allow', 'payment *'); 193 frame.setAttribute('title', 'Roadtrippers checkout'); 194 frame.style.cssText = 'width:100%;border:0;display:block;'; 195 // A pinned priceId fixes the tier; otherwise pick up the current hash so 196 // the iframe builds on the right tier even when launched from a modal. 197 var appliedTier = options.priceId ? null : tierFromHash(); 198 frame.src = buildSrc(appliedTier); 199 mount.appendChild(frame); 200 201 function postInit() { 202 var payload = {}; 203 for (var k in options) { 204 if (Object.prototype.hasOwnProperty.call(options, k)) { 205 payload[k] = options[k]; 206 } 207 } 208 payload.pageUrl = window.location.href; 209 frame.contentWindow.postMessage( 210 { type: 'checkout:init', options: payload }, 211 ORIGIN 212 ); 213 } 214 215 function onMessage(event) { 216 if (event.origin !== ORIGIN || event.source !== frame.contentWindow) { 217 return; 218 } 219 var data = event.data || {}; 220 switch (data.type) { 221 case 'checkout:ready': 222 postInit(); 223 break; 224 case 'checkout:resize': 225 if (typeof data.height === 'number') { 226 frame.style.height = data.height + 'px'; 227 } 228 break; 229 case 'checkout:loading': 230 mount.classList.add('is-loading'); 231 break; 232 case 'checkout:error': 233 mount.classList.remove('is-loading'); 234 break; 235 case 'checkout:success': 236 mount.classList.remove('is-loading'); 237 handleSuccess(mount, data.detail); 238 break; 239 } 240 } 241 242 window.addEventListener('message', onMessage); 243 244 // Register for tier-hash changes (unless a priceId pins the tier). The app 245 // reads ?tier from the query string; interval selection stays in-frame. 246 if (!options.priceId) { 247 tieredFrames.push({ 248 reloadTier: function (tier) { 249 if (tier !== appliedTier) { 250 appliedTier = tier; 251 frame.src = buildSrc(tier); 252 } 253 } 254 }); 255 } 256 } 257 258 function init(mount) { 259 if (mount.getAttribute('data-rt-checkout-init') === '1') { 260 return; 261 } 262 mount.setAttribute('data-rt-checkout-init', '1'); 263 createBridge(mount); 264 } 265 266 function boot() { 267 var mounts = document.querySelectorAll('.rt-checkout-embed'); 268 if (!mounts.length) { 269 return; 270 } 271 272 // Re-point already-built iframes when the tier hash changes (tier cards, 273 // modal openers, etc.). Non-tier hash changes are ignored. 274 window.addEventListener('hashchange', function () { 275 var tier = tierFromHash(); 276 if (tier === null) { 277 return; 278 } 279 tieredFrames.forEach(function (b) { 280 b.reloadTier(tier); 281 }); 282 }); 283 284 // Deep-link: a tier hash present on load opens the purchase modal. Gated on 285 // userReady so the membership type is resolved before deciding. 286 document.addEventListener('userReady', openPurchaseModalForHash); 287 288 // Swap the upgrade embed for a manual message for non-Stripe subscriptions. 289 document.addEventListener('userReady', handleUpgradeFallback); 290 291 // Defer iframe creation until a mount is actually visible. This avoids 292 // loading the (auth-gated) checkout for hidden user-state branches and 293 // for modules inside not-yet-opened modals. 294 if ('IntersectionObserver' in window) { 295 var observer = new IntersectionObserver(function (entries) {
296 entries.forEach(function (entry) { 297 if (entry.isIntersecting) { 298 init(entry.target); 299 observer.unobserve(entry.target); 300 } 301 }); 302 }); 303 Array.prototype.forEach.call(mounts, function (mount) { 304 observer.observe(mount); 305 }); 306 } else { 307 Array.prototype.forEach.call(mounts, init); 308 } 309 } 310 311 if (document.readyState === 'loading') { 312 document.addEventListener('DOMContentLoaded', boot); 313 } else { 314 boot(); 315 } 316})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.