PageSourceSearch

https://assetmarketcap.com/static/js/iap.js?v=3

js assetmarketcap.com collected 2026-09-24 13:24:26 UTC 11,841 bytes, 290 lines download raw bytes

1/* Native in-app purchases for the Capacitor mobile apps.
2 *
3 * Loaded on /pro and /account. Does NOTHING in a normal browser - it only
4 * activates when the page runs inside the Capacitor WebView (window.Capacitor
5 * is injected by the native shell, along with the cordova-plugin-purchase
6 * JS, because assetmarketcap.com is the app's server.url).
7 *
8 * What it does in the app:
9 *  - adds html.is-native-app (CSS hides Stripe/web-only billing UI - Apple
10 *    guideline 3.1.1 forbids links out to external payment)
11 *  - registers the 4 subscription products and shows localized store prices
12 *  - intercepts the Stripe checkout <form> submits and runs the native
13 *    App Store / Play Store purchase sheet instead
14 *  - after purchase/restore, POSTs the receipt to /iap/validate which
15 *    verifies it directly with Apple/Google and activates Pro on the account
16 *
17 * Product ids (must match the store consoles + IAP_PRODUCTS in iap.py).
18 * The stores use DIFFERENT ids: Apple's were created as Pro1month/Pro1year/
19 * ProPlus1month/ProPlus1year (immutable once created); Google Play requires
20 * ids to start with a lowercase letter/digit, so Play uses amc_*.
21 */
22(function () {
23  'use strict';
24
25  var isNative = !!(window.Capacitor && window.Capacitor.isNativePlatform &&
26                    window.Capacitor.isNativePlatform());
27  if (!isNative) return;
28
29  document.documentElement.classList.add('is-native-app');
30
31  // Hide web-only billing UI inside the app (external purchase links are an
32  // App Store rejection; Play has the same rule for subscriptions).
33  var css = [
34    'html.is-native-app form[action="/create-customer-portal-session"],',
35    'html.is-native-app form[action="/create-pro-plus-customer-portal-session"],',
36    'html.is-native-app form[action="/create-api-customer-portal-session"],',
37    'html.is-native-app form[action="/create-api-checkout-session"],',
38    'html.is-native-app form[action="/cancel-subscription"],',
39    'html.is-native-app form[action="/cancel-pro-plus-subscription"],',
40    'html.is-native-app form[action="/cancel-api-subscription"],',
41    'html.is-native-app .x-final-trust,',
42    'html.is-native-app [data-web-billing-only] { display: none !important; }',
43    'html.is-native-app [data-native-billing-only] { display: block !important; }',
44    'html.is-native-app .iap-restore { display: inline-block !important; }'
45  ].join('\n');
46  var styleEl = document.createElement('style');
47  styleEl.textContent = css;
48  document.head.appendChild(styleEl);
49
50  var IDS_APPLE = {
51    'pro:monthly':      'Pro1month',
52    'pro:annual':       'Pro1year',
53    'pro_plus:monthly': 'ProPlus1month',
54    'pro_plus:annual':  'ProPlus1year'
55  };
56  var IDS_GOOGLE = {
57    'pro:monthly':      'amc_pro_monthly',
58    'pro:annual':       'amc_pro_annual',
59    'pro_plus:monthly': 'amc_proplus_monthly',
60    'pro_plus:annual':  'amc_proplus_annual'
61  };
62  var PRODUCT_IDS = null;
62 // set in init() once the platform is known
63  // Stripe checkout endpoints -> tier, so the same interception works on
64  // /pro plan cards AND the /account subscribe/upgrade forms.
65  var FORM_TIER = {
66    '/create-checkout-session': 'pro',
67    '/create-pro-plus-checkout-session': 'pro_plus'
68  };
69
70  var store = null;
71  var iapPlatform = null;
72  var busy = false;
73
74  // Block the Stripe checkout forms FIRST, before the store is ready. If the
75  // purchase plugin is missing or slow, the native app must fail closed (no
76  // purchase) rather than fall through to external payment, which is an App
77  // Store 3.1.1 rejection. These bind on DOM ready, not on deviceready.
78  if (document.readyState === 'loading') {
79    document.addEventListener('DOMContentLoaded', bindUi, false);
80  } else {
81    bindUi();
82  }
83  function bindUi() {
84    interceptCheckoutForms();
85    bindRestoreLinks();
86  }
87
88  document.addEventListener('deviceready', init, false);
89  // Safety net: Capacitor fires deviceready, but if this script loads after
90  // the event already fired, init directly.
91  setTimeout(function () {
92    if (!store && window.CdvPurchase) init();
93  }, 4000);
94
95  function init() {
96    if (store || !window.CdvPurchase) {
97      if (!window.CdvPurchase) console.warn('[iap] CdvPurchase not available');
98      return;
99    }
100    store = CdvPurchase.store;
101    iapPlatform = (Capacitor.getPlatform() === 'ios')
102      ? CdvPurchase.Platform.APPLE_APPSTORE
103      : CdvPurchase.Platform.GOOGLE_PLAY;
104    PRODUCT_IDS = (Capacitor.getPlatform() === 'ios') ? IDS_APPLE : IDS_GOOGLE;
105
106    var products = Object.keys(PRODUCT_IDS).map(function (k) {
107      return {
108        id: PRODUCT_IDS[k],
109        type: CdvPurchase.ProductType.PAID_SUBSCRIPTION,
110        platform: iapPlatform
111      };
112    });
113    store.register(products);
114
115    store.when()
116      .approved(onApproved)
117      .productUpdated(refreshDisplayedPrices);
118
119    store.error(function (err) {
120      console.warn('[iap] store error', err && err.code, err && err.message);
121      busy = false;
122      resetButtons();
123    });
124
125    store.initialize([iapPlatform]).then(function () {
126      refreshDisplayedPrices();
127    });
128
129    // Checkout forms are already intercepted by bindUi(); just un-hide the
130    // restore link now that the store exists.
131    bindRestoreLinks();
132  }
133
134  // ── Purchase flow ─────────────────────────────────────────────
135
136  function interceptCheckoutForms() {
137    document.addEventListener('submit', function (ev) {
138      var form = ev.target;
139      var action = (form.getAttribute && form.getAttribute('action')) || '';
140      var tier = FORM_TIER[action];
141      if (!tier) return;                    // not a subscription form
142      ev.preventDefault();
143      ev.stopImmediatePropagation();        // block the web "Loading..." handler
144      if (busy) return;
145      if (!store) {
146        alert('The App Store is still connecting. Please try again in a moment.');
147        return;
148      }
149
150      var intervalInput = form.querySelector('input[name=interval]');
151      var interval = (intervalInput && intervalInput.value) === 'annual' ? 'annual' : 'monthly';
152      order(tier, interval, form.querySelector('button[type=submit]'));
153    }, true); // capture phase: beat pro.html's own submit listener
154  }
155
156  function order(tier, interval, btn) {
157    var productId = PRODUCT_IDS[tier + ':' + interval];
158    var product = store.get(productId, iapPlatform);
159    var offer = product && product.getOffer();
160    if (!offer) {
161      alert('Store products are still loading. Please try again in a moment.');
162      return;
163    }
164    busy = true;
165    if (btn) { btn.disabled = true; btn.dataset.prevText = btn.textContent; btn.textContent = 'Opening store...'; }
166    offer.order().then(function (err) {
167      // resolves when the store sheet closes; err is set if it failed/cancelled
168      busy = false;
169      resetButtons();
170      if (err) console.warn('[iap] order result', err.code, err.message);
171    });
172  }
173
174  function resetButtons() {
175    document.querySelectorAll('button[data-prev-text]').forEach(function (b) {
176      b.disabled = false;
177      b.textContent = b.dataset.prevText;
178      delete b.dataset.prevText;
179    });
180  }
181
182  // ── Server-side verification ──────────────────────────────────
183
184  function onApproved(tx) {
185    var payload = null;
186    if (Capacitor.getPlatform() === 'ios') {
187      var receipt = null;
188      try {
189        receipt = tx.parentReceipt && tx.parentReceipt.nativeData &&
190                  tx.parentReceipt.nativeData.appStoreReceipt;
191        if (!receipt && store.localReceipts) {
192          store.localReceipts.forEach(function (r) {
193            if (!receipt && r.nativeData && r.nativeData.appStoreReceipt) {
194              receipt = r.nativeData.appStoreReceipt;
195            }
196          });
197        }
198      } catch (e) { /* fall through */ }
199      if (!receipt) { console.warn('[iap] no app receipt on approved tx'); return; }
200      payload = { platform: 'ios', receipt: receipt };
201    } else {
202      var np = tx.nativePurchase || {};
203      if (!np.purchaseToken) { console.warn('[iap] no purchaseToken on approved tx'); return; }
204      payload = {
205        platform: 'android',
206        purchaseToken: np.purchaseToken,
207        productId: (tx.products && tx.products[0] && tx.products[0].id) || np.productId
208      };
209    }
210
211    fetch('/iap/validate', {
212      method: 'POST',
213      credentials: 'same-origin',
214      headers: { 'Content-Type': 'application/json' },
215      body: JSON.stringify(payload)
216    })
217    .then(function (r) { return r.json().then(function (j) { return { status: r.status, body: j }; }); })
218    .then(function (res) {
219      var j = res.body || {};
220      if (j.ok) {
221        tx.finish();                        // acknowledges on Android too
222        if (j.active) window.location.href = '/account?iap=activated';
223      } else if (j.error === 'purchase_linked_elsewhere') {
224        tx.finish();                        // don't retry forever
225        alert(j.message || 'This subscription is linked to another account.');
226      } else {
227        // Server/verification hiccup: leave the tx unfinished so the plugin
228        // re-fires approved on next app launch and we retry.
229        console.warn('[iap] validate failed', res.status, j.error, j.message);
230      }
231    })
232    .catch(function (e) { console.warn('[iap] validate network error', e); });
233  }
234
235  // ── Restore + localized prices ────────────────────────────────
236
237  function bindRestoreLinks() {
238    document.querySelectorAll('.iap-restore').forEach(function (el) {
239      el.hidden = false;
240      if (el.dataset.iapBound) return;      // bindUi() and init() both call this
241      el.dataset.iapBound = '1';
242      el.addEventListener('click', function (ev) {
243        ev.preventDefault();
244        if (!store) return;
245        el.textContent = 'Restoring...';
246        store.restorePurchases().then(function () {
247          el.textContent = 'Restore purchases';
248        });
249      });
250    });
251  }
252
253  function refreshDisplayedPrices() {
254    // Rewrite the /pro plan cards with the store's localized prices so the
255    // label always matches what the store sheet will charge.
256    var cards = document.querySelectorAll('.x-plan[data-plan]');
257    if (!cards.length) return;
258    cards.forEach(function (card) {
259      var tier = card.dataset.plan === 'pro-plus' ? 'pro_plus' : 'pro';
260      var mo = priceOf(PRODUCT_IDS[tier + ':monthly']);
261      var yr = priceOf(PRODUCT_IDS[tier + ':annual']);
262      if (mo) {
263        card.dataset.monthlyAmount = mo;
264        card.dataset.ctaMonthly = 'Get ' + (tier === 'pro' ? 'Pro' : 'Pro Plus') + ' - ' + mo + '/mo';
265      }
266      if (yr) {
267        card.dataset.annualAmount = yr;
268        card.dataset.ctaAnnual = 'Get ' + (tier === 'pro' ? 'Pro' : 'Pro Plus') + ' - ' + yr + '/yr';
269      }
270      if (mo || yr) {
271        // Localized price string already contains the currency symbol.
272        var cur = card.querySelector('.x-plan-cur');
273        if (cur) cur.textContent = '';
274        var on = document.querySelector('#xBilling button.is-on');
275        var interval = (on && on.dataset.interval) === 'annual' ? 'annual' : 'monthly';
276        var amt = card.querySelector('[data-price-amount]');
277        var ctaTxt = card.querySelector('[data-cta-text]');
278        if (amt) amt.textContent = card.dataset[interval + 'Amount'];
279        if (ctaTxt) ctaTxt.textContent = card.dataset['cta' + (interval === 'annual' ? 'Annual' : 'Monthly')];
280      }
281    });
282  }
283
284  function priceOf(productId) {
285    var p = store && store.get(productId, iapPlatform);
286    var offer = p && p.getOffer();
287    var phases = offer && offer.pricingPhases;
288    return (phases && phases.length && phases[phases.length - 1].price) || null;
289  }
290})();

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.