PageSourceSearch

https://roadtrippers.com/wp-content/mu-plugins/roadtrippers-checkout/js/rt-checkout-embed.js?ver=1.0.0

js roadtrippers.com collected 2026-09-24 08:28:28 UTC 9,927 bytes, 316 lines download raw bytes

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.