1/** 2 * Opens Stripe Embedded Checkout in a modal so plan purchases never leave the 3 * site. In live mode every failure path falls back to the button's own Payment 4 * Link href, so a broken script degrades to the previous behaviour rather than 5 * to nothing. In test mode there is no fallback by design - see fallback(). 6 */ 7( function () { 8 'use strict'; 9 10 var config = window.sollosStripe || {}; 11 if ( ! config.publishableKey || ! config.endpoint ) { 12 return; 13 } 14 15 var strings = config.strings || {}; 16 17 // wp_localize_script stringifies booleans: true arrives as '1', false as ''. 18 var testMode = !! config.testMode && '0' !== String( config.testMode ); 19 20 var stripeJsPromise = null; 21 var modal = null; 22 var checkout = null; 23 var trigger = null; 24 var pending = false; 25 26 /** 27 * Fetch Stripe.js on demand. Loading it up front would put a third-party 28 * request on every pricing page view. 29 * 30 * @return {Promise} Resolves with the Stripe constructor. 31 */ 32 function loadStripeJs() { 33 if ( stripeJsPromise ) { 34 return stripeJsPromise; 35 } 36 37 stripeJsPromise = new Promise( function ( resolve, reject ) { 38 if ( window.Stripe ) { 39 resolve( window.Stripe ); 40 return; 41 } 42 43 var existing = document.querySelector( 'script[data-sollos-stripe-js]' ); 44 if ( existing ) { 45 existing.addEventListener( 'load', function () { 46 window.Stripe ? resolve( window.Stripe ) : reject( new Error( 'Stripe.js unavailable' ) ); 47 } ); 48 existing.addEventListener( 'error', function () { 49 reject( new Error( 'Stripe.js failed to load' ) ); 50 } ); 51 return; 52 } 53 54 var script = document.createElement( 'script' ); 55 script.src = 'https://js.stripe.com/v3/'; 56 script.async = true; 57 script.setAttribute( 'data-sollos-stripe-js', '1' ); 58 script.onload = function () { 59 window.Stripe ? resolve( window.Stripe ) : reject( new Error( 'Stripe.js unavailable' ) ); 60 }; 61 script.onerror = function () { 62 reject( new Error( 'Stripe.js failed to load' ) ); 63 }; 64 document.head.appendChild( script ); 65 } ); 66 67 return stripeJsPromise; 68 } 69 70 function buildModal() { 71 var overlay = document.createElement( 'div' ); 72 overlay.className = 'sollos-checkout-modal'; 73 overlay.setAttribute( 'role', 'dialog' ); 74 overlay.setAttribute( 'aria-modal', 'true' ); 75 overlay.setAttribute( 'aria-label', strings.title || 'Secure checkout' ); 76 overlay.hidden = true; 77 78 var backdrop = document.createElement( 'div' ); 79 backdrop.className = 'sollos-checkout-modal__backdrop'; 80 backdrop.setAttribute( 'data-sollos-close', '' ); 81 82 var dialog = document.createElement( 'div' ); 83 dialog.className = 'sollos-checkout-modal__dialog'; 84 85 var close = document.createElement( 'button' ); 86 close.type = 'button'; 87 close.className = 'sollos-checkout-modal__close'; 88 close.setAttribute( 'data-sollos-close', '' ); 89 close.setAttribute( 'aria-label', strings.close || 'Close checkout' ); 90 close.innerHTML = '×'; 91 92 var status = document.createElement( 'p' ); 93 status.className = 'sollos-checkout-modal__status'; 94 status.setAttribute( 'role', 'status' ); 95 96 var mount = document.createElement( 'div' ); 97 mount.className = 'sollos-checkout-modal__mount'; 98 mount.id = 'sollos-checkout-mount'; 99 100 dialog.appendChild( close ); 101 dialog.appendChild( status ); 102 dialog.appendChild( mount ); 103 overlay.appendChild( backdrop ); 104 overlay.appendChild( dialog ); 105 document.body.appendChild( overlay ); 106 107 overlay.addEventListener( 'click', function ( event ) { 108 if ( event.target instanceof Element && event.target.hasAttribute( 'data-sollos-close' ) ) { 109 closeModal(); 110 } 111 } ); 112 113 return overlay; 114 } 115 116 function setStatus( message ) { 117 if ( ! modal ) { 118 return; 119 } 120 121 var status = modal.querySelector( '.sollos-checkout-modal__status' ); 122 if ( ! status ) { 123 return; 124 } 125 126 status.textContent = message || ''; 127 status.hidden = ! message; 128 } 129 130 function onKeydown( event ) { 131 if ( 'Escape' === event.key || 'Esc' === event.key ) { 132 closeModal(); 133 } 134 } 135 136 function openModal() { 137 if ( ! modal ) { 138 modal = buildModal(); 139 } 140 141 modal.hidden = false; 142 document.body.classList.add( 'sollos-checkout-open' ); 143 setStatus( strings.loading || 'Loading secure checkout...' ); 144 document.addEventListener( 'keydown', onKeydown ); 145 146 var close = modal.querySelector( '.sollos-checkout-modal__close' ); 147 if ( close ) { 148 close.focus(); 149 } 150 } 151 152 function closeModal() { 153 if ( ! modal || modal.hidden ) { 154 return; 155 } 156 157 modal.hidden = true; 158 pending = false;
159 document.body.classList.remove( 'sollos-checkout-open' ); 160 document.removeEventListener( 'keydown', onKeydown ); 161 162 if ( checkout ) { 163 try { 164 checkout.destroy(); 165 } catch ( error ) { 166 // Already torn down; nothing to recover. 167 } 168 checkout = null; 169 } 170 171 if ( trigger ) { 172 trigger.focus(); 173 trigger = null; 174 } 175 } 176 177 /** 178 * Give up on the embedded flow and use the hosted Payment Link instead. 179 * 180 * In test mode the server replaces the href with '#', so there is nothing 181 * to fall back to and the customer is never bounced to a live payment page 182 * from a test-keyed site. The error is reported in place instead. 183 * 184 * @param {string} url Payment Link URL from the button's href. 185 * @param {Error} error What went wrong. 186 */ 187 function fallback( url, error ) { 188 if ( window.console && window.console.warn ) { 189 window.console.warn( '[sollos] embedded checkout unavailable:', error ); 190 } 191 192 if ( url && '#' !== url && ! testMode ) { 193 setStatus( strings.error || 'Checkout could not load.' ); 194 window.location.href = url; 195 return; 196 } 197 198 setStatus( ( testMode ? strings.errorDev : strings.error ) || 'Checkout could not load.' ); 199 } 200 201 /** 202 * @param {string} plan Plan slug. 203 * @param {string} fallbackUrl Payment Link URL to use if anything fails. 204 * @param {Element} source The clicked button, for focus restore. 205 */ 206 function startCheckout( plan, fallbackUrl, source ) { 207 if ( pending ) { 208 return; 209 } 210 211 pending = true; 212 trigger = source || null; 213 openModal(); 214 215 var session = fetch( config.endpoint, { 216 method: 'POST', 217 headers: { 'Content-Type': 'application/json' }, 218 credentials: 'same-origin', 219 body: JSON.stringify( { plan: plan } ) 220 } ).then( function ( response ) { 221 return response.json().then( function ( data ) { 222 if ( ! response.ok || ! data || ! data.clientSecret ) { 223 throw new Error( ( data && data.message ) || 'Could not start checkout.' ); 224 } 225 return data; 226 } ); 227 } ); 228 229 Promise.all( [ loadStripeJs(), session ] ).then( function ( results ) { 230 var Stripe = results[ 0 ]; 231 var data = results[ 1 ]; 232 var stripe = Stripe( config.publishableKey ); 233 234 // Stripe renamed the API and its mount function together, so the 235 // session's ui_mode decides which one to call. 236 var create = ( 'embedded_page' === data.uiMode && 'function' === typeof stripe.createEmbeddedCheckoutPage ) 237 ? stripe.createEmbeddedCheckoutPage.bind( stripe ) 238 : stripe.initEmbeddedCheckout.bind( stripe ); 239 240 return create( { clientSecret: data.clientSecret } ); 241 } ).then( function ( instance ) { 242 pending = false; 243 244 // The customer closed the modal while Stripe was still loading. 245 if ( ! modal || modal.hidden ) { 246 try { 247 instance.destroy(); 248 } catch ( error ) { 249 // Nothing to clean up. 250 } 251 return; 252 } 253 254 checkout = instance; 255 setStatus( '' ); 256 instance.mount( '#sollos-checkout-mount' ); 257 } ).catch( function ( error ) { 258 pending = false; 259 fallback( fallbackUrl, error ); 260 } ); 261 } 262 263 document.addEventListener( 'click', function ( event ) { 264 if ( 0 !== event.button || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey ) { 265 return; 266 } 267 268 if ( ! ( event.target instanceof Element ) ) { 269 return; 270 } 271 272 var button = event.target.closest( '[data-sollos-plan]' ); 273 if ( ! button ) { 274 return; 275 } 276 277 var plan = button.getAttribute( 'data-sollos-plan' ); 278 if ( ! plan ) { 279 return; 280 } 281 282 event.preventDefault(); 283 startCheckout( plan, button.getAttribute( 'href' ) || '', button ); 284 } ); 285}() );
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.