"use strict"; window.fbmPayments && (window.fbmPayments.initForm = async function () { var pay = window.fbmPayments, paymentForm, card, headerImg = '/yokee/images/header-bg.jpg', logoImg = '/yokee/images/app-logo-248-beveled.png', closeImg = '/images/close-button-white.png', anims = {}; async function onPaymentConfirmed(e) { var d = e.detail || {}, action = pay.selectedAction; if (paymentForm) { paymentForm.removeEventListener('submit', onPaymentFormSubmit); } clearCache(); var body = { intentId: d.intentId, setupIntentId: d.setupIntentId, subId: d.subId, attribution: urlParamsAsObject() }; Object.assign(body, browserInfo()); try { var completeInfo = await pay.post('/complete', body); if (completeInfo.voucher.isNew) { var price = completeInfo.invoice.price; pay.trackEvent('purchase', { affiliation: pay.provider, currency: (completeInfo.invoice.currency || '').toUpperCase(), transaction_id: completeInfo.invoice.id, value: price, items: [{ id: action.priceId, name: action.offer, brand: action.brand, variant: action.desc, quantity: 1, price }] }); } if (!paymentForm) showForm({ completeInfo }); else showCompleteView(completeInfo); } catch (err) { pay.dispatchPaymentFailedEvent(err); } } function showCompleteView(completeInfo) { submitButtons().forEach(function(x) { x.remove() }); var mainElements = paymentForm.querySelectorAll('.main > :not(.complete)'); mainElements.forEach(function(x) { if (!x.classList.contains('complete')) x.remove(); }) paymentForm.querySelectorAll('.complete').forEach(function(e) { e.classList.toggle('hidden'); }); paymentForm.querySelectorAll('.activate').forEach(function(e) { e.href = completeInfo.voucher.url }); if (completeInfo.customer && completeInfo.customer.email) { var msg; if (isAppCompatible()) { msg = 'Want to activate later?
The activation link was just sent to {{email}}. You can share or activate it at any time. Please note it can be activated only once.'.replace( '{{email}}', completeInfo.customer.email ); } else { msg = 'An email has been sent to {{email}}. Open it from your mobile device to download and activate Yokee.'.replace( '{{email}}', completeInfo.customer.email ); } setTimeout(function() { paymentForm.querySelectorAll('.notice').forEach(function(e) { e.innerHTML = msg; }); }, 0); } paymentForm.querySelectorAll('.contact_link').forEach(function(e) { var url = 'mailto:support@yokee.tv?subject=Purchase+Number+{{code}}'.replace( '{{code}}', completeInfo.voucher.code ); e.href = url; }); paymentForm.querySelectorAll('.purchase_confirmation').forEach(function(e) { var msg = 'Confirmation: {{code}}'.replace( '{{code}}', completeInfo.voucher.code ); e.innerHTML = msg; }); var animElems = paymentForm.querySelectorAll('.anim'); for (var e of animElems) anims[e.id].play(); } function onPaymentMethodVerified(e) { var action = pay.selectedAction; pay.trackEvent('checkout_progress', { currency: (action.price.currency || '').toUpperCase(), value: action.price.amount, checkout_option: pay.provider, checkout_step: pay.CHECKOUT_STEP_METHOD_VERIFIED, items: [{ id: action.priceId, name: action.offer, brand: action.brand, variant: action.desc, quantity: 1, price: action.price.amount }] }); createSubscription(e.detail); } function onPaymentRequiresAction(e) { var d = e.detail || {}, action = pay.selectedAction; pay.trackEvent('checkout_progress', { currency: (action.price.currency || '').toUpperCase(), value: action.price.amount, checkout_option: pay.provider, checkout_step: pay.CHECKOUT_STEP_ACTION_REQUIRED, items: [{ id: action.priceId, name: action.offer, brand: action.brand, variant: action.desc, quantity: 1, price: action.price.amount }] }); if (d.pendingSetupIntent && d.pendingSetupIntent.id) { confirmPaymentSetup({ pendingSetupIntent: d.pendingSetupIntent, subId: d.subId }) } else { confirmPayment({ paymentMethodId: d.paymentMethodId, paymentIntent: d.paymentIntent }); } } function onPaymentFailed(e) { var d = e.detail, err = d.error, action = pay.selectedAction; if (err.error) err = err.error; var msg; if (err.code === undefined) { msg = 'Payment failed: {{message}}'.replace( '{{message}}', err.message ); } else { msg = 'Payment failed: {{message}} ({{code}})'.replace( '{{code}}', err.code ).replace( '{{message}}', err.message ); } // https://developers.google.com/gtagjs/reference/event#exception pay.trackEvent('exception', { description: msg, fatal: true }); if (paymentForm) { var errorElement = document.querySelector('#payment-error'); errorElement.textContent = msg; toggleSubmitLoader(); } else { pay.resetModal(); setTimeout(function () { alert(msg); }, 1); } } async function createSubscription({ paymentMethodId, complete }) { try { var result = await pay.post('/sub', { paymentMethodId, priceId: pay.selectedAction.priceId }); await processCreateSubscriptionResult({ paymentMethodId, result }); if (complete) complete('success'); return Promise.resolve(); } catch (err) { if (complete) complete('fail'); return pay.dispatchPaymentFailedEvent(err); } } async function confirmPaymentSetup({ pendingSetupIntent, subId }) { var psi = pendingSetupIntent; try { var res = await pay.stripe.confirmCardSetup(psi.client_secret); // start code flow to handle updating the payment details // Display error message in your UI. // The card was declined (i.e. insufficient funds, card has expired, etc) if (!res) throw new Error('missing setup response'); if (res.error) throw res.error; // error-like object var si = res.setupIntent; if (!si) throw new Error('missing setup info'); if (si.status !== 'succeeded') throw new Error(`invalid setup status ${si.status}`); if (!si.id) throw new Error(`missing setup id`); return pay.dispatchPaymentConfirmedEvent({ setupIntentId: si.id, subId }); } catch (err) { return pay.dispatchPaymentFailedEvent(err); } } async function confirmPayment({ paymentMethodId, paymentIntent }) { var pmId = paymentMethodId; var pi = paymentIntent; try { var res = await pay.stripe.confirmCardPayment(pi.client_secret, { payment_method: pmId }) // start code flow to handle updating the payment details // Display error message in your UI. // The card was declined (i.e. insufficient funds, card has expired, etc) if (!res) throw new Error('missing payment response'); if (res.error) throw res.error; // error-like object var pi = res.paymentIntent; if (!pi) throw new Error('missing payment info'); if (pi.status !== 'succeeded') throw new Error(`invalid payment status ${pi.status}`); if (!pi.id) throw new Error(`missing payment id`); return pay.dispatchPaymentConfirmedEvent({ intentId: pi.id }); } catch (err) { return pay.dispatchPaymentFailedEvent(err); } } async function createPaymentMethod({ billingDetails, invoiceId, customerId }) { try { // invoiceId, customerId exist IFF we retry var result = await pay.stripe.createPaymentMethod({ type: 'card', card: card, billing_details: billingDetails, }); if (result.error) return pay.dispatchPaymentFailedEvent(result); if (invoiceId && customerId) { // Update the payment method and retry invoice payment return retryInvoiceWithNewPaymentMethod({ paymentMethodId: result.paymentMethod.id, customerId, invoiceId }); } else { return pay.dispatchPaymentMethodVerifiedEvent({ paymentMethodId: result.paymentMethod.id }); } } catch(err) { return pay.dispatchPaymentFailedEvent(err); } } async function retryInvoiceWithNewPaymentMethod({ customerId, paymentMethodId, invoiceId }) { var result = await pay.post('/retry', { customerId, paymentMethodId, invoiceId }); return processRetryInvoiceResult({ paymentMethodId, result }); } async function processCreateSubscriptionResult({ paymentMethodId, result }) { if (result.error) throw result; var sub = result.subscription || {}; var pi = sub.payment_intent || {}; if (processActiveSubscription(sub, pi)) return; // const subStatusToProcess = new Set(['active', 'trialing']); // const intentStatusToProcess = new Set(['succeeded', '']); // if (!subStatusToProcess.has(sub.status)) return false; // if (!intentStatusToProcess.has(pi.status)) var psi = sub.pending_setup_intent || {}; if (pi.status === 'requires_confirmation') { return confirmPayment({ paymentMethodId, paymentIntent: pi }); } else if (pi.status === 'requires_action' || psi.status === 'requires_confirmation') { // additional authentication required by financial institution? (3DS, 2FA) return pay.dispatchPaymentRequiresActionEvent({ paymentMethodId, paymentIntent: pi, pendingSetupIntent: psi, subId: sub.id }); } else if (pi.status === 'requires_payment_method') { // Card attached to customer but charging the customer failed? return processRequiresPaymentMethod({ invoiceId: sub.latest_invoice, customerId: sub.customer.id, paymentIntent: pi }); } throw new Error(`invalid inactive status ${pi.status}`); } function processRetryInvoiceResult({paymentMethodId, result }) { if (result.error) throw result; var sub = result.subscription || {}; var pi = sub.payment_intent || {}; if (processActiveSubscription(sub, pi)) return; if (['requires_action', 'requires_payment_method'].indexOf(pi.status) >= 0) { // additional authentication required or // we need to re-confirm the previously-failed payment: return pay.dispatchPaymentRequiresActionEvent({ paymentMethodId, paymentIntent: pi }); } throw new Error(`invalid inactive status ${pi.status}`); } function processActiveSubscription(sub, pi) { if (sub.status !== 'active') return false; if (pi.status !== 'succeeded') throw new Error(`invalid active status ${pi.status}`); pay.dispatchPaymentConfirmedEvent({ intentId: pi.id }); return true; } function processRequiresPaymentMethod({ invoiceId, customerId, paymentIntent }) { // Using localStorage to store the state of the retry: latest invoice ID and status localStorage.setItem('latestInvoiceId', invoiceId); localStorage.setItem('customerId', customerId); localStorage.setItem( 'lastPaymentIntentStatus', paymentIntent.status ); throw new Error('Your card was declined'); } function onPaymentFormSubmit(ev) { ev.preventDefault(); clearOutcome(); toggleSubmitLoader(); var name = document.querySelector("#payment-name").value; var email = document.querySelector("#payment-email").value; var countryCode = document.querySelector("#payment-country").value; try { validateUserInput(name, email, countryCode); } catch (err) { return pay.dispatchPaymentFailedEvent(err); } var billingDetails = { name: name, email: email, address: { country: countryCode } }; var lastPaymentIntentStatus = localStorage.getItem('lastPaymentIntentStatus'); if (lastPaymentIntentStatus === 'requires_payment_method') { // If a previous payment failed, reuse lastest invoice and customer // with a new payment method: var invoiceId = localStorage.getItem('latestInvoiceId'); var customerId = localStorage.getItem('customerId'); // create new payment method & retry payment on invoice with new payment method createPaymentMethod({ billingDetails, priceId: pay.selectedAction.priceId, card, customerId, invoiceId }); } else { // create new payment method & create subscription createPaymentMethod({ billingDetails, priceId: pay.selectedAction.priceId, card }); } } function validateUserInput(name, email, countryCode) { if (!name || name.trim().length == 0) throw new Error('Missing name'); if (!email || email.trim().length == 0) throw new Error('Missing email'); if (!countryCode || countryCode.trim().length == 0) throw new Error('Missing country'); } function toggleSubmitLoader() { var btnElems = submitButtons(); btnElems.forEach(function(btnElem) { var textElem = btnElem.querySelector("span"); var loaderElem = btnElem.querySelector(".loader"); textElem.classList.toggle('hidden'); loaderElem.classList.toggle('hidden'); if (btnElem.getAttribute('disabled')) btnElem.removeAttribute('disabled'); else btnElem.setAttribute('disabled', 'disabled'); }); } function submitButtons() { return paymentForm.querySelectorAll("[type='submit']"); } function initCardElement() { var elements = pay.stripe.elements(); card = elements.create('card', { iconStyle: 'solid', style: { base: { iconColor: '#8898AA', color: '#586A82', lineHeight: '36px', fontWeight: 300, fontFamily: '"Helvetica Neue", Helvetica, sans-serif', fontSize: '19px', '::placeholder': { color: '#8898AA', }, }, invalid: { iconColor: '#e85746', color: '#e85746', } }, classes: { base: 'field', focus: 'is-focused', }, }); card.mount("#payment-card"); } function showForm({ completeInfo }) { buildForm(); setTimeout(function () { paymentForm.style.visibility = 'visible'; document.getElementById("payment-email").focus(); if (completeInfo) showCompleteView(completeInfo); }, 20); // fonts need some time to load, to avoid flicker } function buildForm() { var modal = pay.modal(); modal.innerHTML = `
`; paymentForm = document.getElementById('payment-form'); paymentForm.style.visibility = 'hidden'; paymentForm.querySelector('.top').style.backgroundImage = `url("${headerImg}")`; paymentForm.querySelector('.logo').src = logoImg; paymentForm.querySelector('.close').style.backgroundImage = `url("${closeImg}")`; // remove elements that don't belong to this device: paymentForm.querySelectorAll(isAppCompatible() ? '.incompatible' : '.compatible') .forEach(function(e) { e.remove() }); // preload anims as early as possible: if (isAppCompatible()) { loadAnim('complete-animation-non-landscape', 'complete'); loadAnim('complete-animation-landscape', 'complete-bordered'); } else { loadAnim('complete-animation-email-non-landscape', 'complete-email'); loadAnim('complete-animation-email-landscape', 'complete-email'); } initCardElement(); var closeButton = document.getElementById('payment-form-close'); if (closeButton) closeButton.onclick = closeForm; submitButtons().forEach(function(b) { b.appendChild(pay.createLoader('hidden')); }); var inputs = document.querySelectorAll('.field'); Array.prototype.forEach.call(inputs, function (input) { input.addEventListener('focus', function () { input.classList.add('is-focused'); }); input.addEventListener('blur', function () { input.classList.remove('is-focused'); }); function onChange() { if (input.value.length === 0) { input.classList.add('is-empty'); } else { input.classList.remove('is-empty'); } } input.addEventListener('keyup', onChange); input.addEventListener('change', onChange); }); card.on('change', function (event) { clearOutcome(); }); paymentForm.addEventListener('submit', onPaymentFormSubmit); if (pay.selectedAction) { document.getElementById('payment-brand').innerHTML = pay.selectedAction.brand; document.getElementById('payment-price').innerHTML = pay.selectedAction.priceName; document.getElementById('payment-offer').innerHTML = pay.selectedAction.offer; } } function closeForm() { pay.resetModal(); card = undefined; paymentForm = undefined; } // Gets available attribution URL params for a download link function browserInfo() { const res = { isMobile: !!isAppCompatible(), referrer: document.referrer, }; var v = document.getElementsByTagName('title')[0]; if (v) res.title = v.text; return res; } function isAppCompatible() { return navigator.userAgent.match(/Android/i) || navigator.userAgent.match(/iPhone|iPad|iPod/i); } function clearOutcome() { var errorElement = document.querySelector('#payment-error'); errorElement.textContent = ''; } function clearCache() { localStorage.clear(); } function urlParamsAsObject() { var p = new URLSearchParams(window.location.search); var res = {}; for(var [key, value] of p.entries()) res[key] = value; var toRemove = ['env', 'domain']; toRemove.forEach(function(k) { delete res[k]; }); return res; } function loadImage(url) { return new Promise(function(resolve, reject) { var imageUrl = new URL(url, pay.launcherUrl); var img = new Image(); img.onload = resolve; img.onerror = reject; img.src = imageUrl.toString(); }); } function initPaymentListeners() { document.addEventListener('paymentFailed', onPaymentFailed); document.addEventListener('paymentMethodVerified', onPaymentMethodVerified); document.addEventListener('paymentRequiresAction', onPaymentRequiresAction); document.addEventListener('paymentConfirmed', onPaymentConfirmed); } function loadAnim(id, file) { anims[id] = lottie.loadAnimation({ container: document.getElementById(id), path: `/yokee/images/${file}.lottie`, loop: false, autoplay: false }); return anims[id]; } Object.assign(pay, { closeForm, showForm }); await Promise.all([ pay.initScript('https://cdnjs.cloudflare.com/ajax/libs/lottie-web/5.7.8/lottie_light.min.js'), loadImage(headerImg), loadImage(logoImg), fetch(new URL('fonts/CoreSansA45Regular.woff', pay.launcherUrl), { mode: 'no-cors' }), fetch(new URL('fonts/CoreSansA55Medium.woff', pay.launcherUrl), { mode: 'no-cors' }), ]); clearCache(); initPaymentListeners(); return Promise.resolve(); });