@automattic/newspack-blocks 4.12.3 → 4.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/carousel/view.asset.php +1 -1
  3. package/dist/carousel/view.js +3 -3
  4. package/dist/editor.asset.php +1 -1
  5. package/dist/editor.js +3 -3
  6. package/dist/modal.asset.php +1 -1
  7. package/dist/modal.js +1 -1
  8. package/dist/modalCheckout-rtl.css +1 -1
  9. package/dist/modalCheckout.asset.php +1 -1
  10. package/dist/modalCheckout.css +1 -1
  11. package/dist/modalCheckout.js +1 -1
  12. package/includes/class-modal-checkout.php +32 -106
  13. package/includes/class-newspack-blocks.php +4 -0
  14. package/includes/modal-checkout/class-checkout-data.php +287 -0
  15. package/includes/tracking/class-data-events.php +5 -140
  16. package/newspack-blocks.php +3 -2
  17. package/package.json +5 -5
  18. package/src/blocks/checkout-button/view.php +8 -42
  19. package/src/blocks/donate/frontend/class-newspack-blocks-donate-renderer-base.php +2 -2
  20. package/src/blocks/donate/frontend/class-newspack-blocks-donate-renderer-frequency-based.php +4 -28
  21. package/src/blocks/donate/frontend/class-newspack-blocks-donate-renderer-tiers-based.php +5 -17
  22. package/src/modal-checkout/analytics/ga4/checkout-attempt.js +3 -2
  23. package/src/modal-checkout/analytics/ga4/checkout-success.js +3 -3
  24. package/src/modal-checkout/analytics/ga4/dismissed.js +3 -3
  25. package/src/modal-checkout/analytics/ga4/loaded.js +3 -3
  26. package/src/modal-checkout/analytics/ga4/pagination.js +3 -2
  27. package/src/modal-checkout/analytics/ga4/utils/index.js +31 -17
  28. package/src/modal-checkout/analytics/index.js +1 -1
  29. package/src/modal-checkout/checkout.scss +2 -1
  30. package/src/modal-checkout/index.js +1 -1
  31. package/src/modal-checkout/modal.js +232 -253
  32. package/src/modal-checkout/templates/thankyou.php +3 -6
  33. package/src/modal-checkout/utils.js +126 -0
  34. package/vendor/autoload.php +1 -1
  35. package/vendor/composer/autoload_real.php +4 -4
  36. package/vendor/composer/autoload_static.php +2 -2
  37. package/vendor/composer/installed.php +2 -2
@@ -10,8 +10,14 @@ import * as a11y from './accessibility.js';
10
10
  * Internal dependencies
11
11
  */
12
12
  import { manageDismissed, manageOpened } from './analytics';
13
- import { getProductDetails } from './analytics/ga4/utils';
14
- import { createHiddenInput, domReady } from './utils';
13
+ import {
14
+ domReady,
15
+ iframeReady,
16
+ createHiddenInput,
17
+ triggerFormSubmit,
18
+ getCheckoutData,
19
+ getFormattedAmount,
20
+ } from './utils';
15
21
 
16
22
  const CLASS_PREFIX = newspackBlocksModal.newspack_class_prefix;
17
23
  const IFRAME_NAME = 'newspack_modal_checkout_iframe';
@@ -20,11 +26,12 @@ const MODAL_CHECKOUT_ID = 'newspack_modal_checkout';
20
26
  const MODAL_CLASS_PREFIX = `${ CLASS_PREFIX }__modal`;
21
27
  const VARIATON_MODAL_CLASS_PREFIX = 'newspack-blocks__modal-variation';
22
28
 
23
- // Track the checkout state for analytics.
24
- let analyticsData = {};
25
29
  // Track the checkout intent to avoid multiple analytics events.
26
30
  let inCheckoutIntent = false;
27
31
 
32
+ // Checkout title.
33
+ let checkoutTitle = newspackBlocksModal.labels.checkout_modal_title;
34
+
28
35
  // Close the modal.
29
36
  const closeModal = el => {
30
37
  if ( el.overlayId && window.newspackReaderActivation?.overlays ) {
@@ -44,6 +51,11 @@ window.onpageshow = event => {
44
51
  }
45
52
  }
46
53
 
54
+ // Register the "checkout closed" event.
55
+ const checkoutClosedEvent = new CustomEvent( 'checkout-closed' );
56
+
57
+ window.newspackRAS = window.newspackRAS || [];
58
+
47
59
  domReady( () => {
48
60
  const modalCheckout = document.querySelector( `#${ MODAL_CHECKOUT_ID }` );
49
61
  if ( ! modalCheckout ) {
@@ -61,44 +73,6 @@ domReady( () => {
61
73
  iframe.style.height = initialHeight;
62
74
  iframe.style.visibility = 'hidden';
63
75
 
64
- function iframeReady( cb ) {
65
- if ( iframe._readyTimer ) {
66
- clearTimeout( iframe._readyTimer );
67
- }
68
- let fired = false;
69
-
70
- function ready() {
71
- if ( ! fired ) {
72
- fired = true;
73
- clearTimeout( iframe._readyTimer );
74
- cb.call( this );
75
- }
76
- }
77
- function readyState() {
78
- if ( this.readyState === "complete" ) {
79
- ready.call( this );
80
- }
81
- }
82
- function checkLoaded() {
83
- if ( iframe._ready ) {
84
- clearTimeout( iframe._readyTimer );
85
- return;
86
- }
87
- const doc = iframe.contentDocument || iframe.contentWindow?.document;
88
- if ( doc && doc.URL.indexOf('about:') !== 0 ) {
89
- if ( doc?.readyState === 'complete' ) {
90
- ready.call( doc );
91
- } else {
92
- doc.addEventListener( 'DOMContentLoaded', ready );
93
- doc.addEventListener( 'readystatechange', readyState );
94
- }
95
- } else {
96
- iframe._readyTimer = setTimeout( checkLoaded, 10 );
97
- }
98
- }
99
- checkLoaded();
100
- }
101
-
102
76
  /**
103
77
  * Handle iframe load state.
104
78
  */
@@ -116,6 +90,9 @@ domReady( () => {
116
90
  }
117
91
  }
118
92
  const container = iframe?.contentDocument?.querySelector( `#${ IFRAME_CONTAINER_ID }` );
93
+ if ( ! container ) {
94
+ return;
95
+ }
119
96
  const setModalReady = () => {
120
97
  iframeResizeObserver.observe( container );
121
98
  if ( spinner.style.display !== 'none' ) {
@@ -126,41 +103,43 @@ domReady( () => {
126
103
  }
127
104
  iframe._ready = true;
128
105
  }
129
- if ( container ) {
130
- if ( container.checkoutComplete ) {
131
- // Dispatch a checkout_completed event to RAS.
132
- const params = getProductDetails( MODAL_CHECKOUT_ID );
133
- window.newspackRAS = window.newspackRAS || [];
134
- window.newspackRAS.push( function( ras ) {
135
- ras.dispatchActivity( 'checkout_completed', params );
136
- } );
106
+ const productDetails = container.querySelector( '#modal-checkout-product-details' );
107
+ const checkoutData = getCheckoutData( productDetails );
108
+ if ( container.checkoutComplete ) {
109
+ // Dispatch a `checkout_completed` activity to RAS.
110
+ window.newspackRAS.push( [ 'checkout_completed', checkoutData ] );
137
111
 
138
- // Update the newsletters signup modal if it exists.
139
- if ( window?.newspackReaderActivation?.refreshNewslettersSignupModal && window?.newspackReaderActivation?.getReader()?.email ) {
140
- window.newspackReaderActivation.refreshNewslettersSignupModal( window.newspackReaderActivation.getReader().email );
141
- }
112
+ // Update the newsletters signup modal if it exists.
113
+ if ( window?.newspackReaderActivation?.refreshNewslettersSignupModal && window?.newspackReaderActivation?.getReader()?.email ) {
114
+ window.newspackReaderActivation.refreshNewslettersSignupModal( window.newspackReaderActivation.getReader().email );
115
+ }
142
116
 
143
- // Update the modal title and width to reflect successful transaction.
144
- setModalSize( 'small' );
145
- setModalTitle( newspackBlocksModal.labels.thankyou_modal_title );
146
- setModalReady();
147
- a11y.trapFocus( modalCheckout.querySelector( `.${ MODAL_CLASS_PREFIX }` ) );
148
- } else {
149
- // Revert modal title and width default value.
150
- setModalSize();
151
- setModalTitle( newspackBlocksModal.labels.checkout_modal_title );
152
- if ( iframe.contentWindow?.newspackBlocksModalCheckout?.checkout_nonce ) {
153
- // Store the checkout nonce for later use.
154
- // We store the nonce from the iframe content window to ensure the nonce was generated for a logged in session
155
- modalCheckout.checkout_nonce = iframe.contentWindow.newspackBlocksModalCheckout.checkout_nonce;
156
- }
117
+ // Update the modal title and width to reflect successful transaction.
118
+ setModalSize( 'small' );
119
+ setModalTitle( newspackBlocksModal.labels.thankyou_modal_title );
120
+ setModalReady();
121
+ a11y.trapFocus( modalCheckout.querySelector( `.${ MODAL_CLASS_PREFIX }` ) );
122
+ } else {
123
+ // Make sure the order summary renders the correct text.
124
+ const summaryTextNode = productDetails?.querySelector( 'strong' );
125
+ if ( summaryTextNode ) {
126
+ summaryTextNode.textContent = checkoutData.price_summary;
157
127
  }
158
- if ( container.checkoutReady ) {
159
- setModalReady();
160
- } else {
161
- container.addEventListener( 'checkout-ready', setModalReady );
128
+
129
+ // Revert modal title and width default value.
130
+ setModalSize();
131
+ setModalTitle( checkoutTitle );
132
+ if ( iframe.contentWindow?.newspackBlocksModalCheckout?.checkout_nonce ) {
133
+ // Store the checkout nonce for later use.
134
+ // We store the nonce from the iframe content window to ensure the nonce was generated for a logged in session
135
+ modalCheckout.checkout_nonce = iframe.contentWindow.newspackBlocksModalCheckout.checkout_nonce;
162
136
  }
163
137
  }
138
+ if ( container.checkoutReady ) {
139
+ setModalReady();
140
+ } else {
141
+ container.addEventListener( 'checkout-ready', setModalReady );
142
+ }
164
143
  }
165
144
 
166
145
  iframe.addEventListener( 'load', handleIframeReady );
@@ -172,11 +151,13 @@ domReady( () => {
172
151
  * the session for a newly registered reader fails to carry the cart over to
173
152
  * the checkout.
174
153
  *
154
+ * @param {Object} checkoutData The checkout data.
155
+ *
175
156
  * @return {Promise} The promise that resolves with the checkout URL.
176
157
  */
177
- const generateCart = ( formData ) => {
158
+ const generateCart = ( checkoutData ) => {
178
159
  return new Promise( ( resolve, reject ) => {
179
- const urlParams = new URLSearchParams( formData );
160
+ const urlParams = new URLSearchParams( checkoutData );
180
161
  urlParams.append( 'action', 'modal_checkout_request' );
181
162
  fetch( newspackBlocksModal.ajax_url + '?' + urlParams.toString() )
182
163
  .then( res => {
@@ -196,7 +177,7 @@ domReady( () => {
196
177
  /**
197
178
  * Empty cart via ajax.
198
179
  */
199
- const emptyCart = () => {
180
+ const emptyCart = async () => {
200
181
  const body = new FormData();
201
182
  if ( ! newspackBlocksModal.has_unsupported_payment_gateway ) {
202
183
  body.append( 'modal_checkout', '1' );
@@ -204,14 +185,18 @@ domReady( () => {
204
185
  body.append( 'action', 'abandon_modal_checkout' );
205
186
  body.append( '_wpnonce', modalCheckout.checkout_nonce );
206
187
  modalCheckout.checkout_nonce = null;
207
- fetch(
208
- newspackBlocksModal.ajax_url,
209
- {
210
- method: 'POST',
211
- body,
212
- }
213
- );
214
- }
188
+ try {
189
+ await fetch(
190
+ newspackBlocksModal.ajax_url,
191
+ {
192
+ method: 'POST',
193
+ body,
194
+ }
195
+ );
196
+ } catch ( error ) {
197
+ console.warn( 'Unable to empty cart:', error ); // eslint-disable-line no-console
198
+ }
199
+ };
215
200
 
216
201
  /**
217
202
  * Whether reader should be prompted with registration.
@@ -236,19 +221,45 @@ domReady( () => {
236
221
  }
237
222
  const form = ev.target;
238
223
  form.classList.add( 'modal-processing' );
239
- const productData = form.dataset.product;
240
- if ( productData ) {
241
- const data = JSON.parse( productData );
242
- Object.keys( data ).forEach( key => {
224
+
225
+ const checkoutData = getCheckoutData( form );
226
+
227
+ const isDonateBlock = checkoutData.newspack_donate;
228
+ if ( isDonateBlock ) {
229
+ const frequency = checkoutData.donation_frequency;
230
+ const donationTiers = [
231
+ ...form.querySelectorAll(
232
+ `.donation-tier__${ frequency }, .donation-frequency__${ frequency }`
233
+ )
234
+ ];
235
+ const donationTierIndex = checkoutData.donation_tier_index;
236
+ let donationContainer, customAmount;
237
+ if ( donationTierIndex ) {
238
+ donationContainer = donationTiers[ donationTierIndex ];
239
+ customAmount = checkoutData[ `donation_value_${ frequency }` ];
240
+ } else {
241
+ donationContainer = donationTiers[ 0 ];
242
+ customAmount = checkoutData[ `donation_value_${ frequency }_untiered` ];
243
+ }
244
+ const donationData = getCheckoutData( donationContainer );
245
+ for( const key in donationData ) {
246
+ checkoutData[ key ] = donationData[ key ];
247
+ }
248
+ checkoutData.amount = customAmount;
249
+ checkoutData.price_summary = checkoutData.summary_template.replace( '{{PRICE}}', getFormattedAmount( checkoutData.amount, checkoutData.currency ) );
250
+ }
251
+
252
+ if ( checkoutData ) {
253
+ Object.keys( checkoutData ).forEach( key => {
243
254
  const existingInputs = form.querySelectorAll( 'input[name="' + key + '"]' );
244
255
  if ( 0 === existingInputs.length ) {
245
- form.appendChild( createHiddenInput( key, data[ key ] ) );
256
+ form.appendChild( createHiddenInput( key, checkoutData[ key ] ) );
246
257
  }
247
258
  } );
248
259
  }
249
- const formData = new FormData( form );
260
+
250
261
  // If we're not going from variation picker to checkout, set the modal trigger:
251
- if ( ! formData.get( 'variation_id' ) ) {
262
+ if ( ! checkoutData.variation_id ) {
252
263
  modalTrigger = ev.submitter;
253
264
  }
254
265
  // Clear any open variation modal.
@@ -261,9 +272,9 @@ domReady( () => {
261
272
  } );
262
273
 
263
274
  // Trigger variation modal if variation is not selected.
264
- if ( formData.get( 'is_variable' ) && ! formData.get( 'variation_id' ) ) {
275
+ if ( checkoutData.is_variable && ! checkoutData.variation_id ) {
265
276
  const variationModal = [ ...variationModals ].find(
266
- modal => modal.dataset.productId === formData.get( 'product_id' )
277
+ modal => modal.dataset.productId === checkoutData.product_id
267
278
  );
268
279
  if ( variationModal ) {
269
280
  variationModal
@@ -277,12 +288,12 @@ domReady( () => {
277
288
  ].forEach( afterSuccessParam => {
278
289
  const existingInputs = singleVariationForm.querySelectorAll( 'input[name="' + afterSuccessParam + '"]' );
279
290
  if ( 0 === existingInputs.length ) {
280
- singleVariationForm.appendChild( createHiddenInput( afterSuccessParam, formData.get( afterSuccessParam ) ) );
291
+ singleVariationForm.appendChild( createHiddenInput( afterSuccessParam, checkoutData[ afterSuccessParam ] ) );
281
292
  }
282
293
  } );
283
294
 
284
295
  // Append the product data hidden inputs.
285
- const variationData = singleVariationForm.dataset.product;
296
+ const variationData = singleVariationForm.dataset.checkout;
286
297
  if ( variationData ) {
287
298
  const data = JSON.parse( variationData );
288
299
  Object.keys( data ).forEach( key => {
@@ -300,34 +311,30 @@ domReady( () => {
300
311
  openModal( variationModal );
301
312
  a11y.trapFocus( variationModal, false );
302
313
 
303
- // Set up some GA4 information.
304
- const formAnalyticsData = form.getAttribute( 'data-product' );
305
- analyticsData = formAnalyticsData ? JSON.parse( formAnalyticsData ) : {};
306
-
307
314
  // For the variation modal we will not set `inCheckoutIntent = true` and
308
315
  // let the `opened` event get triggered once the user selects a
309
316
  // variation so we track the selection.
310
317
  if ( ! inCheckoutIntent ) {
311
- manageOpened( analyticsData );
318
+ manageOpened( checkoutData );
312
319
  }
313
320
 
314
321
  // Append product data info to the modal itself, so we can grab it for manageDismissed:
315
322
  document
316
323
  .getElementById( 'newspack_modal_checkout' )
317
- .setAttribute( 'data-order-details', JSON.stringify( analyticsData ) );
324
+ .setAttribute( 'data-checkout', JSON.stringify( checkoutData ) );
318
325
  return;
319
326
  }
320
327
  }
321
328
 
322
329
  // Populate cart and redirect to checkout if there is an unsupported payment gateway.
323
330
  if ( ! isModalCheckout && ! shouldPromptRegistration() ) {
324
- generateCart( formData ).then( url => {
331
+ generateCart( checkoutData ).then( url => {
325
332
  // Remove modal checkout query string and trailing question mark (if any).
326
333
  window.location.href = url;
327
334
  } );
328
335
  // Add some animation to the Checkout Button while the non-modal checkout is loading.
329
336
  // For now, don't do it when any popup opens, just when we go right to the checkout page.
330
- if ( ! ( formData.get( 'is_variable' ) && ! formData.get( 'variation_id' ) ) ) {
337
+ if ( ! ( checkoutData.is_variable && ! checkoutData.variation_id ) ) {
331
338
  const buttons = form.querySelectorAll( 'button[type=submit]:focus' );
332
339
  buttons.forEach( button => {
333
340
  button.classList.add( 'non-modal-checkout-loading' );
@@ -338,141 +345,21 @@ domReady( () => {
338
345
  return;
339
346
  }
340
347
  form.classList.remove( 'modal-processing' );
341
- const isDonateBlock = formData.get( 'newspack_donate' );
342
- const isCheckoutButtonBlock = formData.get( 'newspack_checkout' );
343
- // Set up some GA4 information.
344
- if ( isCheckoutButtonBlock ) { // this fires on the second in-modal variations screen, too
345
- const formAnalyticsData = form.getAttribute( 'data-product' );
346
- analyticsData = formAnalyticsData ? JSON.parse( formAnalyticsData ) : {};
347
- } else if ( isDonateBlock ) {
348
- // Get donation information and append to the modal checkout for GA4:
349
- const donationFreq = formData.get( 'donation_frequency' );
350
- let donationValue = '';
351
- let productId = '';
352
-
353
- for ( const key of formData.keys() ) {
354
- // Find values that match the frequency name, that aren't empty
355
- if (
356
- key.indexOf( 'donation_value_' + donationFreq ) >= 0 &&
357
- 'other' !== formData.get( key ) &&
358
- '' !== formData.get( key )
359
- ) {
360
- donationValue = formData.get( key );
361
- }
362
- }
363
-
364
- // Get IDs for donation frequencies, and compare them to the selected frequency.
365
- const freqIds = JSON.parse( formData.get( 'frequency_ids' ) );
366
- for ( const freq in freqIds ) {
367
- if ( freq === donationFreq ) {
368
- productId = freqIds[freq].toString();
369
- }
370
- }
371
-
372
- // Get product information together to be appended to the modal for GA4 events outside of the iframe.
373
- analyticsData = {
374
- amount: donationValue,
375
- action_type: 'donation',
376
- currency: formData.get( 'donation_currency' ),
377
- product_id: productId,
378
- product_type: 'donation',
379
- recurrence: donationFreq,
380
- referrer: formData.get( '_wp_http_referer' ),
381
- };
382
- }
383
-
384
- // If the checkout started from a content gate, add the gate ID to the payload.
385
- const gateId = formData.get( 'memberships_content_gate' );
386
- if ( gateId ) {
387
- analyticsData.gate_post_id = gateId;
388
- }
389
- const popupId = formData.get( 'newspack_popup_id' );
390
- if ( popupId ) {
391
- analyticsData.newspack_popup_id = popupId;
392
- }
393
348
 
394
349
  // Analytics.
395
350
  if ( ! inCheckoutIntent ) {
396
- manageOpened( analyticsData );
351
+ manageOpened( checkoutData );
397
352
  }
398
353
  inCheckoutIntent = true;
399
354
 
400
355
  if ( shouldPromptRegistration() ) {
401
356
  ev.preventDefault();
402
- let content = '';
403
- let price = '0';
404
- let priceSummary = '';
405
-
406
- if ( isDonateBlock ) {
407
- const frequency = formData.get( 'donation_frequency' );
408
- const donationTiers = form.querySelectorAll(
409
- `.donation-tier__${ frequency }, .donation-frequency__${ frequency }`
410
- );
411
-
412
- if ( donationTiers?.length ) {
413
- const donationTierIndex = formData.get( 'donation_tier_index' );
414
357
 
415
- if ( ! donationTierIndex ) {
416
- // Handle untiered and frequency donations.
417
-
418
- const frequencyInputs = form.querySelectorAll(
419
- `input[name="donation_value_${ frequency }"], input[name="donation_value_${ frequency }_untiered"]`
420
- );
421
-
422
- if ( frequencyInputs?.length ) {
423
- // Handle frequency based donation tiers.
424
- frequencyInputs.forEach( input => {
425
- if ( input.checked && input.value !== 'other' ) {
426
- price = input.value;
427
- }
428
- } );
429
-
430
- donationTiers.forEach( el => {
431
- const donationData = JSON.parse( el.dataset.product );
432
- if ( donationData.hasOwnProperty( `donation_price_summary_${ frequency }` ) ) {
433
- const priceData = donationData[ `donation_price_summary_${ frequency }` ];
434
- const priceRegex = new RegExp( `(?<=\\D)${ price }(?=\\D)` );
435
- if ( priceRegex.test( priceData ) ) {
436
- priceSummary = priceData;
437
- }
438
- }
439
-
440
- if ( price === '0' && priceSummary ) {
441
- // Replace placeholder price with price input for other.
442
- let otherPrice = formData.get( `donation_value_${ frequency }_other` );
443
-
444
- // Fallback to untiered price if other price is not set.
445
- if ( ! otherPrice ) {
446
- otherPrice = formData.get( `donation_value_${ frequency }_untiered` );
447
- }
448
-
449
- if ( otherPrice ) {
450
- priceSummary = priceSummary.replace( '0', otherPrice );
451
- }
452
- }
453
- } );
454
- }
455
- } else {
456
- const donationData = JSON.parse( donationTiers?.[ donationTierIndex ].dataset.product );
457
- if ( donationData.hasOwnProperty( `donation_price_summary_${ frequency }` ) ) {
458
- priceSummary = donationData[ `donation_price_summary_${ frequency }` ];
459
- }
460
- }
461
- }
462
- } else if ( isCheckoutButtonBlock ) {
463
- const priceSummaryInput = form.querySelector( 'input[name="product_price_summary"]' );
464
-
465
- if ( priceSummaryInput ) {
466
- priceSummary = priceSummaryInput.value;
467
- }
468
- }
469
-
470
- if ( priceSummary ) {
471
- content = `<div class="order-details-summary ${ CLASS_PREFIX }__box ${ CLASS_PREFIX }__box--text-center"><p><strong>${ priceSummary }</strong></p></div>`;
472
- }
358
+ const priceSummary = checkoutData.price_summary;
359
+ const content = priceSummary ? `<div class="order-details-summary ${ CLASS_PREFIX }__box ${ CLASS_PREFIX }__box--text-center"><p><strong>${ priceSummary }</strong></p></div>` : '';
473
360
 
474
361
  // Generate cart asynchroneously.
475
- const cartReq = generateCart( formData );
362
+ const cartReq = generateCart( checkoutData );
476
363
 
477
364
  // Update pending checkout URL.
478
365
  cartReq.then( url => {
@@ -490,10 +377,10 @@ domReady( () => {
490
377
  // Populate cart and redirect to checkout if there is an unsupported payment gateway.
491
378
  if ( ! isModalCheckout ) {
492
379
  // Remove modal checkout query string, and trailing question mark (if any).
493
- generateCart( formData ).then( window.location.href = url );
380
+ generateCart( checkoutData ).then( window.location.href = url );
494
381
  } else {
495
382
  const checkoutForm = generateCheckoutPageForm( url );
496
- triggerCheckout( checkoutForm );
383
+ triggerFormSubmit( checkoutForm );
497
384
  }
498
385
  } )
499
386
  .catch( error => {
@@ -506,9 +393,9 @@ domReady( () => {
506
393
  },
507
394
  onDismiss: () => {
508
395
  // Analytics: Track a dismissal event (modal has been manually closed without completing the checkout).
509
- manageDismissed( analyticsData );
396
+ manageDismissed( checkoutData );
510
397
  inCheckoutIntent = false;
511
- document.getElementById( 'newspack_modal_checkout' ).removeAttribute( 'data-order-details' );
398
+ document.getElementById( 'newspack_modal_checkout' ).removeAttribute( 'data-checkout' );
512
399
  },
513
400
  skipSuccess: true,
514
401
  skipNewslettersSignup: true,
@@ -530,7 +417,7 @@ domReady( () => {
530
417
  // Append product data info to the modal, so we can grab it for GA4 events outside of the iframe.
531
418
  document
532
419
  .getElementById( 'newspack_modal_checkout' )
533
- .setAttribute( 'data-order-details', JSON.stringify( analyticsData ) );
420
+ .setAttribute( 'data-checkout', JSON.stringify( checkoutData ) );
534
421
  }
535
422
  };
536
423
 
@@ -582,6 +469,7 @@ domReady( () => {
582
469
  iframe.style.height = iframeHeight + 'px';
583
470
  }
584
471
  } );
472
+
585
473
  const closeCheckout = () => {
586
474
  const container = iframe?.contentDocument?.querySelector( `#${ IFRAME_CONTAINER_ID }` );
587
475
  const afterSuccessUrlInput = container?.querySelector( 'input[name="after_success_url"]' );
@@ -618,6 +506,8 @@ domReady( () => {
618
506
  if ( modalTrigger ) {
619
507
  modalTrigger.focus();
620
508
  }
509
+
510
+ document.dispatchEvent( checkoutClosedEvent );
621
511
  }
622
512
 
623
513
  if ( container?.checkoutComplete ) {
@@ -648,19 +538,25 @@ domReady( () => {
648
538
 
649
539
  // Ensure we always reset the modal title and width once the modal closes.
650
540
  if ( shouldCloseModal ) {
541
+ checkoutTitle = newspackBlocksModal.labels.checkout_modal_title;
651
542
  setModalSize();
652
- setModalTitle( newspackBlocksModal.labels.checkout_modal_title );
543
+ setModalTitle( checkoutTitle );
653
544
  }
654
545
  } else {
655
546
  window?.newspackReaderActivation?.setPendingCheckout?.();
656
547
  // Analytics: Track a dismissal event (modal has been manually closed without completing the checkout).
657
548
  manageDismissed();
658
549
  inCheckoutIntent = false;
659
- document.getElementById( 'newspack_modal_checkout' ).removeAttribute( 'data-order-details' );
550
+ document.getElementById( 'newspack_modal_checkout' ).removeAttribute( 'data-checkout' );
660
551
  }
552
+ document.removeEventListener( 'keydown', handleKeydown );
661
553
  };
662
554
 
663
- const openCheckout = () => {
555
+ const openCheckout = ( url ) => {
556
+ if ( url ) {
557
+ iframe.src = url;
558
+ }
559
+
664
560
  spinner.style.display = 'flex';
665
561
  openModal( modalCheckout );
666
562
  modalContent.appendChild( iframe );
@@ -673,6 +569,8 @@ domReady( () => {
673
569
  a11y.trapFocus( modalCheckout, iframe );
674
570
 
675
571
  iframeReady( handleIframeReady );
572
+
573
+ document.addEventListener( 'keydown', handleKeydown );
676
574
  };
677
575
 
678
576
  const openModal = el => {
@@ -715,8 +613,6 @@ domReady( () => {
715
613
  }
716
614
  };
717
615
 
718
- window.newspackCloseModalCheckout = closeCheckout;
719
-
720
616
  /**
721
617
  * Handle modal checkout close button.
722
618
  */
@@ -745,13 +641,13 @@ domReady( () => {
745
641
  } );
746
642
 
747
643
  /**
748
- * Close the modal with the escape key.
644
+ * Escape key handler to close the modal checkout.
749
645
  */
750
- document.addEventListener( 'keydown', function ( ev ) {
646
+ const handleKeydown = ev => {
751
647
  if ( ev.key === 'Escape' ) {
752
648
  closeCheckout();
753
649
  }
754
- } );
650
+ };
755
651
 
756
652
  /**
757
653
  * Handle modal checkout triggers.
@@ -771,15 +667,6 @@ domReady( () => {
771
667
  } );
772
668
  } );
773
669
 
774
- /**
775
- * Triggers checkout form submit.
776
- *
777
- * @param {HTMLFormElement} form The form element.
778
- */
779
- const triggerCheckout = form => {
780
- // form.submit does not trigger submit event listener, so we use requestSubmit.
781
- form.requestSubmit( form.querySelector( 'button[type="submit"]' ) );
782
- }
783
670
 
784
671
  /**
785
672
  * Handle donation form triggers.
@@ -830,7 +717,7 @@ domReady( () => {
830
717
  }
831
718
  } );
832
719
  if ( form ) {
833
- triggerCheckout( form );
720
+ triggerFormSubmit( form );
834
721
  }
835
722
  }
836
723
 
@@ -850,7 +737,7 @@ domReady( () => {
850
737
  if ( variationModal ) {
851
738
  const forms = variationModal.querySelectorAll( `form[target="${ IFRAME_NAME }"]` );
852
739
  forms.forEach( variationForm => {
853
- const productData = JSON.parse( variationForm.dataset.product );
740
+ const productData = JSON.parse( variationForm.dataset.checkout );
854
741
  if ( productData?.variation_id === Number( variationId ) ) {
855
742
  form = variationForm;
856
743
  }
@@ -863,14 +750,14 @@ domReady( () => {
863
750
  if ( ! checkoutButtonForm ) {
864
751
  return;
865
752
  }
866
- const productData = JSON.parse( checkoutButtonForm.dataset.product );
753
+ const productData = JSON.parse( checkoutButtonForm.dataset.checkout );
867
754
  if ( productData?.product_id === productId ) {
868
755
  form = checkoutButtonForm;
869
756
  }
870
757
  } );
871
758
  }
872
759
  if ( form ) {
873
- triggerCheckout( form );
760
+ triggerFormSubmit( form );
874
761
  }
875
762
  }
876
763
 
@@ -901,11 +788,103 @@ domReady( () => {
901
788
  const url = window.newspackReaderActivation?.getPendingCheckout?.();
902
789
  if ( url ) {
903
790
  const form = generateCheckoutPageForm( url );
904
- triggerCheckout( form );
791
+ triggerFormSubmit( form );
905
792
  }
906
793
  }
907
794
  // Remove the URL param to prevent re-triggering.
908
795
  window.history.replaceState( null, null, window.location.pathname );
909
796
  };
910
797
  handleModalCheckoutUrlParams();
798
+
799
+ /**
800
+ * Open the modal checkout.
801
+ *
802
+ * @param {Object} options Modal checkout options object.
803
+ * @param {string} options.title The title to set for the modal.
804
+ * @param {string} options.actionType The action type to set for the modal.
805
+ * @param {Object} options.afterSuccess The after success configuration object.
806
+ * @param {Function} options.onCheckoutComplete The callback to call when the checkout is complete.
807
+ * @param {Function} options.onClose The callback to call when the modal is closed.
808
+ */
809
+ window.newspackOpenModalCheckout = ( {
810
+ title = null,
811
+ actionType = null,
812
+ afterSuccess = {},
813
+ onCheckoutComplete = null,
814
+ onClose = null,
815
+ } ) => {
816
+ /**
817
+ * Title configuration.
818
+ */
819
+ checkoutTitle = title || newspackBlocksModal.labels.checkout_modal_title;
820
+ // Set the modal title early, even though it may be overridden by the modal content.
821
+ setModalTitle( checkoutTitle );
822
+
823
+ /**
824
+ * Start with the default checkout URL.
825
+ */
826
+ const url = new URL( newspackBlocksModal.checkout_url );
827
+
828
+ /**
829
+ * Custom action type configuration.
830
+ */
831
+ if ( actionType ) {
832
+ url.searchParams.set( 'action_type', actionType );
833
+ }
834
+
835
+ /**
836
+ * After success parameters.
837
+ */
838
+ if ( afterSuccess?.url ) {
839
+ url.searchParams.set( 'after_success_url', afterSuccess.url );
840
+ }
841
+ if ( afterSuccess?.behavior || afterSuccess?.url ) {
842
+ url.searchParams.set( 'after_success_behavior', afterSuccess.behavior || 'custom' );
843
+ }
844
+ if ( afterSuccess?.buttonLabel ) {
845
+ url.searchParams.set( 'after_success_button_label', afterSuccess.buttonLabel );
846
+ }
847
+
848
+ /**
849
+ * On checkout complete callback.
850
+ */
851
+ if ( onCheckoutComplete ) {
852
+ const handleCheckoutComplete = ( { detail: { action, data } } ) => {
853
+ if ( action !== 'checkout_completed' ) {
854
+ return;
855
+ }
856
+ onCheckoutComplete( data );
857
+ };
858
+ window.newspackRAS.push( ras => {
859
+ ras.on( 'activity', handleCheckoutComplete );
860
+ // Unsubscribe from the checkout complete event when the modal is closed.
861
+ const closeHandler = () => {
862
+ ras.off( 'activity', handleCheckoutComplete );
863
+ document.removeEventListener( 'checkout-closed', closeHandler );
864
+ };
865
+ document.addEventListener( 'checkout-closed', closeHandler );
866
+ } );
867
+ }
868
+
869
+ /**
870
+ * On close callback.
871
+ */
872
+ if ( onClose ) {
873
+ const closeHandler = () => {
874
+ onClose();
875
+ document.removeEventListener( 'checkout-closed', closeHandler );
876
+ };
877
+ document.addEventListener( 'checkout-closed', closeHandler );
878
+ }
879
+
880
+ /**
881
+ * Open the modal checkout.
882
+ */
883
+ openCheckout( url.toString() );
884
+ };
885
+
886
+ /**
887
+ * Close the modal checkout.
888
+ */
889
+ window.newspackCloseModalCheckout = closeCheckout;
911
890
  } );