PageSourceSearch

https://www.untappedcities.com/assets/js/insider-portal.js?v=13ZQLLoGHiOaMJAT

js untappedcities.com collected 2026-09-24 08:38:55 UTC 19,722 bytes, 390 lines download raw bytes

1(function () {
2
3  // Trigger hash — append /#/insider-portal to any URL on the site
4  var HASH = '/insider-portal';
5
6  function shouldOpen() {
7    var h = window.location.hash || '';
8    // Matches #/insider-portal and the booking-gateway variant #/insider-portal-book
9    return h.indexOf('#/' + HASH.replace(/^\//, '')) === 0 ||
10           window.location.href.indexOf('/#' + HASH) !== -1;
11  }
12
13  // ── Auto-click Ghost portal tier after upgrade navigation ────────────
14  // This must run on EVERY page load (before any shouldOpen check)
15  // because the upgrade navigates to #/portal/account/plans (full reload)
16  function autoClickGhostTier() {
17    var targetTier = sessionStorage.getItem('ip_upgrade_tier');
18    if (!targetTier) return;
19    sessionStorage.removeItem('ip_upgrade_tier');
20
21    var attempts = 0;
22    var maxAttempts = 60; // 6 seconds
23
24    var poll = setInterval(function () {
25      attempts++;
26      if (attempts > maxAttempts) {
27        clearInterval(poll);
28        return;
29      }
30
31      var portalFrame = document.querySelector('iframe[data-testid="portal-popup-frame"]');
32      if (!portalFrame) {
33        return;
34      }
35
36      var doc;
37      try {
38        doc = portalFrame.contentDocument || (portalFrame.contentWindow && portalFrame.contentWindow.document);
39      } catch(e) {
40        clearInterval(poll);
41        return;
42      }
43
44
45      var allCards = doc.querySelectorAll('.gh-portal-product-card');
46
47      var cards = doc.querySelectorAll('.gh-portal-product-card:not(.disabled):not(.checked)');
48      if (!cards.length) return;
49
50      var matched = false;
51      cards.forEach(function (card) {
52        var nameEl = card.querySelector('.gh-portal-product-name');
53        if (nameEl && nameEl.textContent.trim().toLowerCase() === targetTier.toLowerCase()) {
54          var btn = card.querySelector('button[data-test-button="select-tier"]');
55          if (btn) {
56            btn.click();
57            matched = true;
58          }
59        }
60      });
61
62      if (matched) clearInterval(poll);
63    }, 100);
64  }
65
66  // Run auto-click check immediately on every page load
67  if (sessionStorage.getItem('ip_upgrade_tier')) {
68    autoClickGhostTier();
69  }
70
71  function initPortal() {
72    // Guard — don't double-init
73    if (document.getElementById('ip-overlay')) return;
74
75  // ── Tier config ─────────────────────────────────────────────────────
76  var IP_PRICES = {
77    fan:      { monthly: '$10',  yearly: '$110' },
78    insider:  { monthly: '$20',  yearly: '$220' },
79    explorer: { monthly: '$30',  yearly: '$330' }
80  };
81
82  // Signup URLs (not logged in)
83  var IP_SIGNUP_URLS = {
84    fan:      { monthly: 'https://www.untappedcities.com/#/portal/signup/65b172b5953b8b0008a3f2a4/monthly', yearly: 'https://www.untappedcities.com/#/portal/signup/65b172b5953b8b0008a3f2a4/yearly' },
85    insider:  { monthly: 'https://www.untappedcities.com/#/portal/signup/65b3220dee57060001800397/monthly', yearly: 'https://www.untappedcities.com/#/portal/signup/65b3220dee57060001800397/yearly' },
86    explorer: { monthly: 'https://www.untappedcities.com/#/portal/signup/65b32254ee5706000180039d/monthly', yearly: 'https://www.untappedcities.com/#/portal/signup/65b32254ee5706000180039d/yearly' }
87  };
88
89  // Upgrade URL (logged in) — Ghost's native plan change flow
90  // For logged-in members we test using the same signup URLs — Ghost may route to Stripe upgrade directly
91  var IP_UPGRADE_URL = null; // signals to use IP_SIGNUP_URLS[tier][billing] same as non-members
92
93  var TIER_DATA = [
94    {
95      id: 'fan',
96      name: 'Fan',
97      featured: false,
98      benefits: [
99        'Access to 12,000+ full articles, ad-free',
100        'Commenting rights',
101        'Stream 400+ on-demand videos',
102        'Special offers from our NYC partners'
103      ]
104    },
105    {
106      id: 'insider',
107      name: 'Insider',
108      featured: true,
109      badge: 'Most popular',
110      includes: 'All Fan benefits, plus:',
111      benefits: [
112        'FREE access to our best-selling walking tours (a $42 value per ticket)',
113        'Exclusive members-only in-person experiences',
114        'Members-only tour discounts'
115      ]
116    },
117    {
118      id: 'explorer',
119      name: 'Explorer',
120      featured: false,
121      includes: 'All Insider benefits, plus:',
122      benefits: [
123        'Bring a friend to any in-person member experience'
124      ]
125    }
126  ];
127
128  // ── Styles ──────────────────────────────────────────────────────────
129  var style = document.createElement('style');
130  style.textContent =
131    '#ip-overlay{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.72);z-index:99999;align-items:center;justify-content:center;padding:1rem;box-sizing:border-box;}' +
132    '#ip-overlay.ip-open{display:flex;}' +
133    '#ip-portal{background:var(--color-bg);border-radius:20px;width:100%;max-width:700px;padding:2rem 1.75rem;position:relative;max-height:90vh;overflow-y:auto;font-family:var(--font-body);}' +
134    '#ip-close{position:absolute;top:14px;right:16px;background:none;border:none;font-size:22px;
134cursor:pointer;color:var(--color-text-acc);line-height:1;}' +
135    '#ip-close:hover{color:var(--color-text);}' +
136    '.ip-head{text-align:center;margin-bottom:0;}' +
137    '.ip-headline{font-size:20px;font-weight:700;color:var(--color-text);margin-bottom:6px;font-family:var(--font-headings);}' +
138    '.ip-subhead{font-size:13px;color:var(--color-text-acc);margin-bottom:0;}' +
139    '.ip-subhead a{color:#e1115f;font-weight:600;text-decoration:none;}' +
140    '.ip-billing{display:flex;margin:1.1rem auto 0;max-width:280px;border:1px solid var(--color-border);border-radius:6px;overflow:hidden;}' +
141    '.ip-bill-opt{flex:1;padding:9px 12px;text-align:center;font-size:13px;font-weight:600;cursor:pointer;background:var(--color-bg-acc);color:var(--color-text-acc);border:none;font-family:var(--font-body);transition:background 0.15s,color 0.15s;}' +
142    '.ip-bill-opt.ip-bill-active{background:#e1115f;color:#fff;}' +
143    '.ip-save{font-size:10px;font-weight:400;opacity:0.9;}' +
144    '.ip-tiers{display:grid;gap:14px;margin-top:1.1rem;}' +
145    '.ip-tiers.ip-tiers-3{grid-template-columns:repeat(3,minmax(0,1fr));}' +
146    '.ip-tiers.ip-tiers-2{grid-template-columns:repeat(2,minmax(0,1fr));max-width:480px;margin-left:auto;margin-right:auto;}' +
147    '@media(max-width:580px){.ip-tiers.ip-tiers-3,.ip-tiers.ip-tiers-2{grid-template-columns:1fr;}.ip-tier.ip-featured{order:-1;}}' +
148    '.ip-tier{border:1px solid var(--color-border);border-radius:14px;padding:18px 16px;display:flex;flex-direction:column;position:relative;cursor:pointer;}' +
149    '.ip-tier.ip-featured{border:2px solid #e1115f;}' +
150    '.ip-badge{position:absolute;top:-11px;left:50%;transform:translateX(-50%);background:#e1115f;color:#fff;font-size:10px;font-weight:700;padding:3px 12px;border-radius:20px;white-space:nowrap;}' +
151    '.ip-tier-name{font-size:14px;font-weight:700;color:#e1115f;margin-bottom:6px;font-family:var(--font-headings);}' +
152    '.ip-price{display:flex;align-items:baseline;gap:4px;margin-bottom:2px;}' +
153    '.ip-price-amount{font-size:26px;font-weight:800;color:var(--color-text);line-height:1;font-family:var(--font-headings);}' +
154    '.ip-price-cadence{font-size:13px;font-weight:400;color:var(--color-text-acc);}' +
155    '.ip-includes{font-size:11px;font-weight:600;color:var(--color-text-acc);margin:12px 0 6px;}' +
156    '.ip-benefits{list-style:none;flex:1;padding:0;margin:8px 0 0;}' +
157    '.ip-benefits li{font-size:12px;color:var(--color-text-acc);padding:3px 0;display:flex;align-items:flex-start;gap:8px;line-height:1.5;}' +
158    '.ip-benefits li::before{content:"";flex-shrink:0;margin-top:4px;width:13px;height:9px;background-image:url("data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 14 10\'%3E%3Cpath d=\'M1 5l4 4L13 1\' stroke=\'%23e1115f\' stroke-width=\'1.8\' fill=\'none\' stroke-linecap=\'round\' stroke-linejoin=\'round\'/%3E%3C/svg%3E");background-repeat:no-repeat;background-size:13px;}' +
159    '.ip-choose{width:100%;margin-top:auto;padding:11px;background:#e1115f !important;color:#fff !important;border:none;border-radius:9px;font-size:14px;font-weight:700;cursor:pointer;font-family:var(--font-body);transition:background 0.15s;}' +
160    '.ip-choose:hover{background:#c40e52 !important;}' +
161    '.ip-terms{margin-top:1.1rem;padding-top:1rem;border-top:1px solid var(--color-border);width:100%;}' +
162    '.ip-terms-label{display:flex;align-items:flex-start;gap:10px;cursor:pointer;font-size:12px;color:var(--color-text-acc);line-height:1.5;}' +
163    '.ip-terms-label input[type="checkbox"]{margin-top:2px;width:16px;height:16px;flex-shrink:0;accent-color:#e1115f;cursor:pointer;}' +
164    '.ip-terms-label a{color:#e1115f;font-weight:600;text-decoration:none;}' +
165    '#ip-tc-warning{display:none;font-size:11px;color:#e1115f;margin:6px 0 0 26px;}' +
166    // Already-member state
167    '.ip-already-state{text-align:center;padding:1.5rem 1rem;}' +
168    '.ip-already-icon{font-size:42px;margin-bottom:12px;}' +
169    '.ip-already-title{font-size:20px;font-weight:700;color:var(--color-text);margin-bottom:8px;font-family:var(--font-headings);}' +
170    '.ip-already-msg{font-size:14px;color:var(--color-text-acc);margin-bottom:1.5rem;line-height:1.6;}' +
171    '.ip-already-btn{display:inline-block;padding:12px 28px;background:#e1115f;color:#fff;border-radius:9px;font-size:14px;font-weight:700;text-decoration:none;font-family:var(--font-body);}' +
172    '.ip-already-btn:hover{background:#c40e52;color:#fff;}';
173  document.head.appendChild(style);
174
175  // ── Helpers ──────────────────────────────────────────────────────────
176  var ipCurrentBilling = 'monthly';
177  var ipIsUpgrade = false; // true when member is logged in and needs to upgrade
178  var ipVisibleTiers = ['fan', 'insider', 'explorer'];
179
180  function buildTierHTML(tierIds, ctaLabel) {
181    var count = tierIds.length;
182    var html = '<div class="ip-tiers ip-tiers-' + count + '">';
183    tierIds.forEach(function (id) {
184      var t = TIER_DATA.filter(function (x) { return x.id === id; })[0];
185      html += '<div class="ip-tier' + (t.featured ? ' ip-featured' : '') + '" onclick="ipSelect(\'' + id + '\')">';
186      if (t.badge) html += '<div class="ip-badge">' + t.badge + '</div>';
187      html += '<div class="ip-tier-name">' + t.name + '</div>';
188      html += '<div class="ip-price"><span class="ip-price-amount" id="ip-price-' + id + '">$' + (id === 'fan' ? '10' : id === 'insider' ? '20' : '30') + '</span><span class="ip-price-cadence" id="ip-cadence-' + id + '">/month</span></div>';
189      if (t.includes) html += '<div class="ip-includes">' + t.includes + '</div>';
190      html += '<ul class="ip-benefits">';
191      t.benefits.forEach(function (b) { html += '<li>' + b + '</li>'; });
192      html += '</ul>';
193      html += '<button class="ip-choose">' + ctaLabel + '</button>';
194      html += '</div>';
195    });
196    html += '</div>';
197    return html;
198  }
199
200  function buildSignupModal(tierIds, headline, subhead, ctaLabel) {
201    return '<div id="ip-overlay" class="ip-open">' +
202      '<div id="ip-portal">' +
203        '<button id="ip-close" onclick="ipClose()" aria-label="Close">&times;</button>' +
204        '<div class="ip-head">' +
205          '<div class="ip-headline">' + headline + '</div>' +
206          '<p class="ip-subhead">' + subhead + '</p>' +
207          '<div class="ip-billing">' +
208            '<button class="ip-bill-opt ip-bill-active" id="ip-opt-monthly" onclick="ipBilling(\'monthly\')" type="button">Monthly</button>' +
209            '<button class="ip-bill-opt" id="ip-opt-yearly" onclick="ipBilling(\'yearly\')" type="button">Yearly <span class="ip-save">(1 month free)</span></button>' +
210          '</div>' +
211        '</div>' +
212        buildTierHTML(tierIds, ctaLabel) +
213        '<div class="ip-terms">' +
214          '<label class="ip-terms-label">' +
215            '<input type="checkbox" id="ip-tc" onchange="ipTcChange()">' +
216            '<span>Automatic renewal. Cancel anytime. By joining, you agree to Untapped New York\'s <a href="https://www.untappedcities.com/terms-of-use/" target="_blank">Terms &amp; Conditions</a>.</span>' +
217          '</label>' +
218          '<p id="ip-tc-warning">Please agree to the Terms &amp; Conditions to continue.</p>' +
219        '</div>' +
220      '</div>' +
221    '</div>';
222  }
223
224  function buildAlreadyModal(tierName) {
225    return '<div id="ip-overlay" class="ip-open">' +
226      '<div id="ip-portal">' +
227        '<button id="ip-close" onclick="ipClose()" aria-label="Close">&times;</button>' +
228        '<div class="ip-already-state">' +
229          '<div class="ip-already-icon">✓</div>' +
230          '<div class="ip-already-title">You\'re already ' + (tierName === 'Explorer' ? 'an Explorer' : 'an Insider') + '!</div>' +
231          '<p class="ip-already-msg">You already have access to all Insider member events and benefits.<br>Head to the members area to see what\'s coming up.</p>' +
232          '<a href="https://www.untappedcities.com/insiders/events/" class="ip-already-btn">View Member Events →</a>' +
233        '</div>' +
234      '</div>' +
235    '</div>';
236  }
237
238  // ── Member detection ─────────────────────────────────────────────────
239  // Membership state comes from assets/js/member.js. This used to read
240  // subscriptions[0] with no status check, so a member who had upgraded could
241  // be identified from their old cancelled subscription, and comped members
242  // were always treated as free.
243  function getMember() {
244    if (!window.UNYMember) return Promise.resolve(null);
245    return window.UNYMember.get();
246  }
247
248  function getTierSlug(m) {
249    if (!m || !m.signedIn) return null;
250    if (!m.tier) return 'free';
251    var name = m.tier.toLowerCase();
252    if (name.indexOf('superfan') !== -1) return 'superfan';
253    if (name.indexOf('explorer') !== -1) return 'explorer';
254    if (name.indexOf('insider') !== -1) return 'insider';
255    if (name.indexOf('fan') !== -1) return 'fan';
256    if (name.indexOf('supporter') !== -1) return 'supporter';
257    return 'free';
258  }
259
260  // ── Render based on member state ─────────────────────────────────────
261  getMember().then(function (member) {
262    var tierSlug = getTierSlug(member);
263    var signedIn = !!(member && member.signedIn);
264    var modalHTML;
265    // The booking gateway opens this portal via #/insider-portal-book to steer
266    // prospective members to the tour-eligible tiers only. The normal
267    // #/insider-portal (header, events page) is unaffected.
268    var bookContext = window.location.href.indexOf('insider-portal-book') !== -1;
269
270    if (!signedIn) {
271      // State 1: Not logged in — sign up modal
272      ipIsUpgrade = false;
273      ipVisibleTiers = bookContext ? ['insider', 'explorer'] : ['fan', 'insider', 'explorer'];
274      modalHTML = buildSignupModal(
275        ipVisibleTiers,
276        'Unlock Free Tours as an Insider',
277        'Already a member? <a href="https://www.untappedcities.com/#/portal/signin">Sign in</a>',
278        'Choose'
279      );
280
281    } else if (tierSlug === 'insider' || tierSlug === 'explorer' || tierSlug === 'superfan') {
282      // State 5: Already at Insider or above — "you're all set" modal
283      var displayName = tierSlug.charAt(0).toUpperCase() + tierSlug.slice(1);
284      modalHTML = buildAlreadyModal(displayName);
285
286    } else if (tierSlug === 'fan') {
287      // State 4: Logged in as Fan — show Insider + Explorer upgrade only
288      ipIsUpgrade = true;
289      ipVisibleTiers = ['insider', 'explorer'];
290      modalHTML = buildSignupModal(
291        ipVisibleTiers,
292        'Upgrade to Unlock Free Tours',
293        'Upgrade your Fan membership to get access to exclusive member events and free tours.',
294        'Upgrade'
295      );
296
297    } else {
298      // States 2 & 3: Free subscriber or Supporter — upgrade modal
299      ipIsUpgrade = true;
300      ipVisibleTiers = bookContext ? ['insider', 'explorer'] : ['fan', 'insider', 'explorer'];
301      modalHTML = buildSignupModal(
302        ipVisibleTiers,
303        'Upgrade Your Membership',
304        'Choose a tier to unlock member events, free tours, and more.',
305        'Upgrade'
306      );
307    }
308
309    document.body.insertAdjacentHTML('beforeend', modalHTML);
310
311    // Close on overlay background click
312    document.getElementById('ip-overlay').addEventListener('click', function (e) {
313      if (e.target === this) ipClose();
314    });
315  });
316
317  // ── Controls ─────────────────────────────────────────────────────────
318  window.ipClose = function () {
319    var overlay = document.getElementById('ip-overlay');
320    if (overlay) overlay.classList.remove('ip-open');
321    history.replaceState(null, '', window.location.pathname + window.location.search);
322  };
323
324  window.ipBilling = function (mode) {
325    ipCurrentBilling = mode;
326    var monthly = document.getElementById('ip-opt-monthly');
327    var yearly = document.getElementById('ip-opt-yearly');
328    if (monthly) monthly.classList.toggle('ip-bill-active', mode === 'monthly');
329    if (yearly) yearly.classList.toggle('ip-bill-active', mode === 'yearly');
330    var cadence = mode === 'yearly' ? '/year' : '/month';
331    ipVisibleTiers.forEach(function (tier) {
332      var priceEl = document.getElementById('ip-price-' + tier);
333      var cadenceEl = document.getElementById('ip-cadence-' + tier);
334      if (priceEl) priceEl.textContent = IP_PRICES[tier][mode];
335      if (cadenceEl) cadenceEl.textContent = cadence;
336    });
337  };
338
339  window.ipTcChange = function () {
340    var warning = document.getElementById('ip-tc-warning');
341    if (warning) warning.style.display = 'none';
342  };
343
344  window.ipSelect = function (tier) {
345    var tc = document.getElementById('ip-tc');
346    if (tc && !tc.checked) {
347      var warning = document.getElementById('ip-tc-warning');
348      if (warning) warning.style.display = 'block';
349      return;
350    }
351    var btns = document.querySelectorAll('.ip-choose');
352    btns.forEach(function (b) { b.disabled = true; b.style.opacity = '0.7'; });
353    var idx = ipVisibleTiers.indexOf(tier);
354    if (idx !== -1 && btns[idx]) btns[idx].textContent = 'Redirecting\u2026';
355
356    if (ipIsUpgrade) {
357      var tierName = tier.charAt(0).toUpperCase() + tier.slice(1);
358      sessionStorage.setItem('ip_upgrade_tier', tierName);
359      // Close our modal first
360      var overlay = document.getElementById('ip-overlay');
361      if (overlay) overlay.classList.remove('ip-open');
362      // Set hash to open Ghost's plan screen
363      window.location.hash = '#/portal/account/plans';
364      // Start polling immediately — no page reload, so we call directly
365      autoClickGhostTier();
366    } else {
367      window.location.href = IP_SIGNUP_URLS[tier][ipCurrentBilling];
368    }
369  };
370
371  } // end initPortal()
372
373  // Always listen for hash changes — works when user pastes /#/insider-portal into address bar on existing page
374  window.addEventListener('hashchange', function () {
375    if (shouldOpen() && !document.getElementById('ip-overlay')) {
376      initPortal();
377    }
378  });
379
380  // On page load, run if hash already present
381  if (shouldOpen()) {
382    initPortal();
383  }
384
385  // Check if we should auto-click a Ghost portal tier (runs after upgrade navigation)
386  if (sessionStorage.getItem('ip_upgrade_tier')) {
387    autoClickGhostTier();
388  }
389
390})();

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.