@automattic/newspack-blocks 4.28.0 → 4.29.0-alpha.1

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 (32) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/blocks/checkout-button/block.json +3 -0
  3. package/dist/editor-rtl.css +16 -13
  4. package/dist/editor.asset.php +1 -1
  5. package/dist/editor.css +16 -13
  6. package/dist/editor.js +27 -25
  7. package/dist/modal.asset.php +1 -1
  8. package/dist/modal.js +1 -1
  9. package/dist/modalCheckout-rtl.css +1 -1
  10. package/dist/modalCheckout.asset.php +1 -1
  11. package/dist/modalCheckout.css +1 -1
  12. package/dist/modalCheckout.js +1 -1
  13. package/includes/class-modal-checkout.php +131 -168
  14. package/includes/class-newspack-blocks.php +1 -0
  15. package/includes/modal-checkout/class-checkout-data.php +23 -0
  16. package/newspack-blocks.php +2 -2
  17. package/package.json +5 -5
  18. package/src/blocks/checkout-button/block.json +3 -0
  19. package/src/blocks/checkout-button/coupon-control.js +141 -0
  20. package/src/blocks/checkout-button/edit.js +6 -2
  21. package/src/blocks/checkout-button/edit.scss +3 -2
  22. package/src/blocks/checkout-button/view.php +9 -2
  23. package/src/blocks/donate/frontend/class-newspack-blocks-donate-renderer-frequency-based.php +2 -2
  24. package/src/blocks/donate/frontend/class-newspack-blocks-donate-renderer-tiers-based.php +1 -1
  25. package/src/modal-checkout/checkout.scss +1 -53
  26. package/src/modal-checkout/index.js +7 -89
  27. package/src/modal-checkout/modal.js +0 -12
  28. package/src/modal-checkout/templates/form-checkout.php +10 -20
  29. package/src/modal-checkout/templates/form-coupon.php +9 -0
  30. package/src/modal-checkout/templates/thankyou.php +1 -1
  31. package/vendor/composer/installed.php +2 -2
  32. package/webpack.config.js +6 -12
@@ -0,0 +1,141 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { debounce } from 'lodash';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import { __ } from '@wordpress/i18n';
10
+ import { BaseControl, TextControl, FormTokenField, Button, Spinner } from '@wordpress/components';
11
+ import { useState, useEffect, useMemo } from '@wordpress/element';
12
+ import apiFetch from '@wordpress/api-fetch';
13
+
14
+ /**
15
+ * Searchable WooCommerce coupon picker for the Checkout Button block.
16
+ *
17
+ * Type to search existing coupons; selecting one stores its code in the block's
18
+ * `coupon` attribute, clearing it removes the coupon. Mirrors the product picker.
19
+ *
20
+ * @param {Object} props
21
+ * @param {string} props.value Currently selected coupon code.
22
+ * @param {Function} props.onChange Called with the selected coupon code (or '').
23
+ * @return {JSX.Element} The control.
24
+ */
25
+ export default function CouponControl( { value, onChange } ) {
26
+ const [ inFlight, setInFlight ] = useState( false );
27
+ const [ suggestions, setSuggestions ] = useState( [] );
28
+ const [ selected, setSelected ] = useState( false );
29
+ const [ isChanging, setIsChanging ] = useState( false );
30
+
31
+ function fetchSuggestions( search ) {
32
+ setInFlight( true );
33
+ return apiFetch( {
34
+ path: `/wc/v3/coupons?search=${ encodeURIComponent( search ) }`,
35
+ } )
36
+ .then( coupons => {
37
+ setSuggestions( coupons.map( coupon => coupon.code ) );
38
+ } )
39
+ .finally( () => setInFlight( false ) );
40
+ }
41
+
42
+ function fetchSaved() {
43
+ setInFlight( true );
44
+ return apiFetch( {
45
+ path: `/wc/v3/coupons?code=${ encodeURIComponent( value ) }`,
46
+ } )
47
+ .then( coupons => {
48
+ // Keep the stored value even if the lookup returns nothing.
49
+ setSelected( coupons.length ? coupons[ 0 ].code : value );
50
+ } )
51
+ .catch( () => setSelected( value ) )
52
+ .finally( () => setInFlight( false ) );
53
+ }
54
+
55
+ useEffect( () => {
56
+ setIsChanging( false );
57
+ if ( value ) {
58
+ fetchSaved();
59
+ } else {
60
+ setSelected( false );
61
+ }
62
+ // eslint-disable-next-line react-hooks/exhaustive-deps
63
+ }, [ value ] );
64
+
65
+ function onTokenChange( tokens ) {
66
+ setIsChanging( false );
67
+ onChange( tokens[ 0 ] || '' );
68
+ }
69
+
70
+ const debouncedFetch = useMemo(
71
+ () => debounce( fetchSuggestions, 200 ),
72
+ // fetchSuggestions only closes over stable state setters, so an empty
73
+ // dependency array is intentional.
74
+ // eslint-disable-next-line react-hooks/exhaustive-deps
75
+ []
76
+ );
77
+
78
+ // Cancel a pending debounced fetch if the control unmounts, so it can't
79
+ // fire and update state after the component is gone.
80
+ useEffect( () => () => debouncedFetch.cancel(), [ debouncedFetch ] );
81
+
82
+ function handleInputChange( search ) {
83
+ if ( search.length > 2 ) {
84
+ setInFlight( true );
85
+ debouncedFetch( search );
86
+ } else {
87
+ // Cancel any fetch queued for a longer previous query so it can't
88
+ // fire after the input was shortened or cleared.
89
+ debouncedFetch.cancel();
90
+ setInFlight( false );
91
+ }
92
+ }
93
+
94
+ if ( value && ! selected && inFlight ) {
95
+ return <Spinner />;
96
+ }
97
+
98
+ if ( selected && ! isChanging ) {
99
+ return (
100
+ <div className="newspack-checkout-button__coupon-field">
101
+ <BaseControl label={ __( 'Coupon', 'newspack-blocks' ) } id="newspack-checkout-button-coupon">
102
+ <TextControl value={ selected } __next40pxDefaultSize disabled />
103
+ <Button variant="link" onClick={ () => setIsChanging( true ) }>
104
+ { __( 'Edit', 'newspack-blocks' ) }
105
+ </Button>{ ' ' }
106
+ <Button
107
+ variant="link"
108
+ isDestructive
109
+ onClick={ () => {
110
+ setSelected( false );
111
+ onChange( '' );
112
+ } }
113
+ >
114
+ { __( 'Remove', 'newspack-blocks' ) }
115
+ </Button>
116
+ </BaseControl>
117
+ </div>
118
+ );
119
+ }
120
+
121
+ return (
122
+ <div className="newspack-checkout-button__coupon-field">
123
+ <FormTokenField
124
+ placeholder={ __( 'Type to search for a coupon…', 'newspack-blocks' ) }
125
+ label={ __( 'Coupon', 'newspack-blocks' ) }
126
+ maxLength={ 1 }
127
+ value={ [] }
128
+ onChange={ onTokenChange }
129
+ onInputChange={ handleInputChange }
130
+ suggestions={ suggestions }
131
+ __next40pxDefaultSize
132
+ />
133
+ { inFlight && <Spinner /> }
134
+ { selected && (
135
+ <Button variant="link" onClick={ () => setIsChanging( false ) }>
136
+ { __( 'Cancel', 'newspack-blocks' ) }
137
+ </Button>
138
+ ) }
139
+ </div>
140
+ );
141
+ }
@@ -41,6 +41,7 @@ import apiFetch from '@wordpress/api-fetch';
41
41
  */
42
42
  import './edit.scss';
43
43
  import RedirectAfterSuccess from '../../components/redirect-after-success';
44
+ import CouponControl from './coupon-control';
44
45
 
45
46
  function getVariationName( variation ) {
46
47
  const attributes = [];
@@ -158,7 +159,7 @@ function ProductControl( props ) {
158
159
  return <Spinner />;
159
160
  }
160
161
  return (
161
- <div className="newspack-checkout-button__product-field" style={ { marginBottom: '16px' } }>
162
+ <div className="newspack-checkout-button__product-field">
162
163
  { selected && ! isChanging ? (
163
164
  <>
164
165
  <BaseControl label={ __( 'Product', 'newspack-blocks' ) } id="selected-product-control">
@@ -201,7 +202,7 @@ function ProductControl( props ) {
201
202
 
202
203
  function CheckoutButtonEdit( props ) {
203
204
  const { attributes, setAttributes, className } = props;
204
- const { placeholder, style, text, product, price, variation, width } = attributes;
205
+ const { placeholder, style, text, product, price, variation, width, coupon } = attributes;
205
206
 
206
207
  const [ productData, setProductData ] = useState( {} );
207
208
  const [ variations, setVariations ] = useState( [] );
@@ -337,6 +338,9 @@ function CheckoutButtonEdit( props ) {
337
338
  </>
338
339
  ) }
339
340
  </ProductControl>
341
+ { newspack_blocks_data?.coupons_enabled && (
342
+ <CouponControl value={ coupon } onChange={ value => setAttributes( { coupon: value } ) } />
343
+ ) }
340
344
  <WidthControl selectedWidth={ width } setAttributes={ setAttributes } />
341
345
  </PanelBody>
342
346
  <PanelBody title={ __( 'After purchase', 'newspack-blocks' ) }>
@@ -1,7 +1,9 @@
1
1
  @use "newspack-colors" as np-colors;
2
2
 
3
3
  .newspack-checkout-button {
4
- &__product-field {
4
+ &__product-field,
5
+ &__coupon-field {
6
+ margin-bottom: 16px;
5
7
  &__error {
6
8
  color: np-colors.$error-500;
7
9
  font-size: 0.9em;
@@ -64,4 +66,3 @@
64
66
  width: auto;
65
67
  }
66
68
  }
67
-
@@ -62,6 +62,7 @@ function render_callback( $attributes ) {
62
62
  $after_success_behavior = $attributes['afterSuccessBehavior'] ?? '';
63
63
  $after_success_button_label = $attributes['afterSuccessButtonLabel'] ?? '';
64
64
  $after_success_url = $attributes['afterSuccessURL'] ?? '';
65
+ $coupon = $attributes['coupon'] ?? '';
65
66
  $is_variable = $attributes['is_variable'];
66
67
 
67
68
  if ( $is_variable && $variation_id ) {
@@ -114,6 +115,12 @@ function render_callback( $attributes ) {
114
115
  $hidden_fields .= $after_success_button_label ? '<input type="hidden" name="after_success_button_label" value="' . esc_attr( $after_success_button_label ) . '" />' : '';
115
116
  $hidden_fields .= $after_success_url ? '<input type="hidden" name="after_success_url" value="' . esc_attr( $after_success_url ) . '" />' : '';
116
117
  }
118
+ // Always emit the coupon field (not gated on the gateway check): it is
119
+ // applied server-side for both the modal and the redirect checkout flows.
120
+ // Strict check so a coupon code of "0" is still emitted.
121
+ if ( '' !== $coupon ) {
122
+ $hidden_fields .= '<input type="hidden" name="coupon" value="' . esc_attr( $coupon ) . '" />';
123
+ }
117
124
 
118
125
  ob_start();
119
126
  /**
@@ -153,8 +160,8 @@ function render_callback( $attributes ) {
153
160
  $checkout_data = Checkout_Data::get_checkout_data( $product );
154
161
 
155
162
  $form = sprintf(
156
- '<form data-checkout="%1$s">%2$s %3$s</form>',
157
- esc_attr( wp_json_encode( $checkout_data ) ),
163
+ '<form %1$s>%2$s %3$s</form>',
164
+ Checkout_Data::data_checkout_attr( $checkout_data ),
158
165
  $button,
159
166
  $hidden_fields
160
167
  );
@@ -177,7 +177,7 @@ class Newspack_Blocks_Donate_Renderer_Frequency_Based extends Newspack_Blocks_Do
177
177
  id='tab-panel-<?php echo esc_attr( $frequency_slug . '-' . $uid ); ?>'
178
178
  role='tabpanel'
179
179
  aria-labelledby='tab-newspack-donate-<?php echo esc_attr( $frequency_slug . '-' . $uid ); ?>'
180
- data-checkout='<?php echo esc_attr( wp_json_encode( Checkout_Data::get_donation_checkout_data( $frequency_slug, $amount ) ) ); ?>'
180
+ <?php Checkout_Data::print_data_checkout_attr( Checkout_Data::get_donation_checkout_data( $frequency_slug, $amount ) ); ?>
181
181
  <?php ( $frequency_slug === $configuration['defaultFrequency'] ? 'tabindex="0"' : '' ); ?>
182
182
  >
183
183
  <?php echo self::render_frequency_selection( $frequency_slug, $frequency_name, $uid, $configuration ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
@@ -262,7 +262,7 @@ class Newspack_Blocks_Donate_Renderer_Frequency_Based extends Newspack_Blocks_Do
262
262
  ?>
263
263
  <div
264
264
  class='wp-block-newspack-blocks-donate__tier donation-tier__<?php echo esc_attr( $frequency_slug ); ?>'
265
- data-checkout='<?php echo esc_attr( wp_json_encode( Checkout_Data::get_donation_checkout_data( $frequency_slug, $amount ) ) ); ?>'
265
+ <?php Checkout_Data::print_data_checkout_attr( Checkout_Data::get_donation_checkout_data( $frequency_slug, $amount ) ); ?>
266
266
  >
267
267
  <?php if ( 3 === $index ) : // The "other" tier. ?>
268
268
  <input
@@ -64,7 +64,7 @@ class Newspack_Blocks_Donate_Renderer_Tiers_Based extends Newspack_Blocks_Donate
64
64
  data-frequency-slug="<?php echo esc_attr( $frequency_slug ); ?>"
65
65
  data-amount="<?php echo esc_attr( $amount ); ?>"
66
66
  data-tier-index="<?php echo esc_attr( $index ); ?>"
67
- data-checkout="<?php echo esc_attr( wp_json_encode( Checkout_Data::get_donation_checkout_data( $frequency_slug, $amount ) ) ); ?>"
67
+ <?php Checkout_Data::print_data_checkout_attr( Checkout_Data::get_donation_checkout_data( $frequency_slug, $amount ) ); ?>
68
68
  >
69
69
  <?php echo wp_kses_post( Newspack_Blocks::get_formatted_amount( $amount, $frequency_slug ) ); ?>
70
70
  </span>
@@ -95,56 +95,12 @@
95
95
  }
96
96
 
97
97
  .order-review-wrapper {
98
- // Collapse and expand Transaction Details.
99
- // stylelint-disable-next-line selector-id-pattern
100
- #order_review_heading {
101
- border-radius: 0;
102
- min-height: 0;
103
- padding: 0;
104
-
105
- &:hover {
106
- background: transparent;
107
- color: var(--newspack-ui-color-neutral-60);
108
- }
109
-
110
- &:focus-visible {
111
- outline: 0;
112
- text-decoration: underline;
113
- }
114
-
115
- svg {
116
- margin: 0 -8px;
117
- transition: rotate 125ms ease-in-out;
118
- }
119
- }
98
+ margin-bottom: var(--newspack-ui-spacer-5, 24px);
120
99
 
121
100
  // stylelint-disable-next-line selector-id-pattern
122
101
  #order_review {
123
102
  margin: 0;
124
103
  }
125
-
126
- .transaction-details-content {
127
- display: grid;
128
- grid-template-rows: 0fr;
129
- margin-bottom: var(--newspack-ui-spacer-2, 12px);
130
- transition: grid-template-rows 250ms ease-in-out;
131
- }
132
-
133
- .transaction-details-content-inner {
134
- overflow: hidden;
135
- }
136
- }
137
-
138
- &.transaction-details-expanded {
139
- // stylelint-disable-next-line selector-id-pattern
140
- #order_review_heading svg {
141
- rotate: 180deg;
142
- }
143
-
144
- .transaction-details-content {
145
- grid-template-rows: 1fr;
146
- margin-bottom: var(--newspack-ui-spacer-5, 24px);
147
- }
148
104
  }
149
105
  }
150
106
 
@@ -410,14 +366,6 @@
410
366
  }
411
367
  }
412
368
 
413
- // Checkout form heading.
414
- .woocommerce .order-details-summary {
415
- // Top margin not needed in newspack ui modal.
416
- h2 {
417
- margin-top: 0;
418
- }
419
- }
420
-
421
369
  // Override checkout payment method box.
422
370
 
423
371
  .wc_payment_methods {
@@ -189,110 +189,23 @@ import { domReady, onCheckoutPlaceOrderProcessing } from './utils';
189
189
  $wrapper.removeClass( 'hidden' );
190
190
  }
191
191
 
192
- const $details = $( '#after_customer_details' );
193
- const expanded = $details.hasClass( 'transaction-details-expanded' );
194
-
195
192
  // Move new order review table to the payment methods.
196
193
  const $payment_methods = $( '.payment_methods' );
197
194
  if ( $payment_methods.length ) {
198
195
  const $el = $wrapper.clone();
199
- // Make sure Transaction Details toggle's aria-expanded value is correct in cloned version.
200
- if ( expanded ) {
201
- $( '[id="order_review_heading"]', $el ).attr( 'aria-expanded', 'true' );
202
- }
203
196
  $( '.order-review-wrapper' ).remove();
204
197
  $payment_methods.after( $el );
205
- } else if ( ! expanded ) {
206
- // If there's no payment method, make sure to expand the Transaction Details on load.
207
- $wrapper.find( '#order_review_heading' ).trigger( 'click' );
208
198
  }
209
199
  } );
210
200
 
211
201
  /**
212
- * Keep the static product summary aligned with recalculated order-review totals.
202
+ * Serialize the checkout form for cart-recalculation AJAX requests.
213
203
  */
214
- function setProductSummary( productDetails, allowHtml = true ) {
215
- const $productDetails = $( '#modal-checkout-product-details strong' );
216
- if ( ! $productDetails.length || ! productDetails ) {
217
- return false;
218
- }
219
- if ( allowHtml && $productDetails.html() !== productDetails ) {
220
- $productDetails.html( productDetails );
221
- } else if ( ! allowHtml && $productDetails.text() !== productDetails ) {
222
- $productDetails.text( productDetails );
223
- }
224
- return true;
225
- }
226
- let productSummaryRequest = false;
227
204
  function getCheckoutPostData() {
228
205
  const $checkoutForm = $( 'form.checkout' );
229
206
  // Repeat-trial checks can only resolve once the checkout form includes a billing email.
230
207
  return $checkoutForm.length ? $checkoutForm.serialize() : '';
231
208
  }
232
- function requestUpdatedProductSummary() {
233
- if ( productSummaryRequest ) {
234
- productSummaryRequest.abort();
235
- }
236
- const request = $.ajax( {
237
- url: newspackBlocksModalCheckout.ajax_url,
238
- method: 'POST',
239
- data: {
240
- action: 'get_cart_product_summary',
241
- modal_checkout: 1,
242
- post_data: getCheckoutPostData(),
243
- },
244
- success: response => {
245
- if ( productSummaryRequest === request ) {
246
- setProductSummary( response );
247
- }
248
- },
249
- complete: () => {
250
- if ( productSummaryRequest === request ) {
251
- productSummaryRequest = false;
252
- }
253
- },
254
- } );
255
- productSummaryRequest = request;
256
- }
257
- function updateProductSummaryFromOrderReview() {
258
- const $cartItem = $( '.order-review-wrapper .woocommerce-checkout-review-order-table tr.cart_item' ).first();
259
- if ( ! $cartItem.length ) {
260
- return false;
261
- }
262
-
263
- const $productName = $cartItem.find( '.product-name' ).clone();
264
- $productName.find( '.product-quantity' ).remove();
265
- // Keep the DOM path text-only; the AJAX fallback is the HTML path because PHP runs it through wp_kses.
266
- const productName = $productName.text().replace( /\s+/g, ' ' ).trim();
267
- const productTotal = $cartItem.find( '.product-total' ).text().replace( /\s+/g, ' ' ).trim();
268
- if ( productName && productTotal ) {
269
- return setProductSummary( `${ productName }: ${ productTotal }`, false );
270
- }
271
- return false;
272
- }
273
- function syncProductSummary() {
274
- if ( updateProductSummaryFromOrderReview() ) {
275
- if ( productSummaryRequest ) {
276
- productSummaryRequest.abort();
277
- productSummaryRequest = false;
278
- }
279
- return;
280
- }
281
- requestUpdatedProductSummary();
282
- }
283
- $( document ).on( 'updated_checkout', syncProductSummary );
284
-
285
- /**
286
- * Toggle Transaction Details
287
- */
288
- $( document ).on( 'click', '#order_review_heading', function () {
289
- // Toggle the aria-expanded attribute.
290
- $( this ).attr( 'aria-expanded', function ( index, attr ) {
291
- return attr === 'false' ? 'true' : 'false';
292
- } );
293
- // Toggle the CSS class to show/hide the Transaction Details.
294
- $( '#after_customer_details' ).toggleClass( 'transaction-details-expanded' );
295
- } );
296
209
 
297
210
  /**
298
211
  * Get updated cart total to update the "Place Order" button.
@@ -748,7 +661,12 @@ import { domReady, onCheckoutPlaceOrderProcessing } from './utils';
748
661
  }
749
662
  }
750
663
 
751
- $( '.order-details-summary' ).after( '<div id="checkout_details">' + html.join( '' ) + '</div>' );
664
+ // Anchor the summary to the hidden product-details carrier, falling back to
665
+ // #after_customer_details when the carrier isn't present (e.g. multi-item carts).
666
+ const $anchor = $( '#modal-checkout-product-details' ).length
667
+ ? $( '#modal-checkout-product-details' )
668
+ : $( '#after_customer_details' );
669
+ $anchor.after( '<div id="checkout_details">' + html.join( '' ) + '</div>' );
752
670
  }
753
671
 
754
672
  /**
@@ -165,12 +165,6 @@ domReady( () => {
165
165
  onCheckoutPlaceOrderError( container, hideProcessingPaymentScreen );
166
166
 
167
167
  onCheckoutReady( container, () => {
168
- // Make sure the order summary renders the correct text.
169
- const summaryTextNode = productDetails?.querySelector( 'strong' );
170
- if ( summaryTextNode ) {
171
- summaryTextNode.textContent = checkoutData.price_summary;
172
- }
173
-
174
168
  // Display initial errors if any.
175
169
  if ( modalCheckout.initialErrors ) {
176
170
  const errorContainer = document.createElement( 'div' );
@@ -444,11 +438,6 @@ domReady( () => {
444
438
  if ( shouldPromptRegistration() ) {
445
439
  ev.preventDefault();
446
440
 
447
- const priceSummary = checkoutData.price_summary;
448
- const content = priceSummary
449
- ? `<div class="order-details-summary ${ CLASS_PREFIX }__box ${ CLASS_PREFIX }__box--text-center"><p><strong>${ priceSummary }</strong></p></div>`
450
- : '';
451
-
452
441
  // Generate cart asynchroneously.
453
442
  const cartReq = generateCart( checkoutData );
454
443
 
@@ -499,7 +488,6 @@ domReady( () => {
499
488
  title: newspackBlocksModal.labels.register_modal_title,
500
489
  },
501
490
  },
502
- content,
503
491
  trigger: ev.submitter,
504
492
  closeOnSuccess: isModalCheckout,
505
493
  } );
@@ -42,27 +42,17 @@ if ( ! $checkout->is_registration_enabled() && $checkout->is_registration_requir
42
42
  ?>
43
43
  <div class="order-review-wrapper hidden">
44
44
  <?php do_action( 'woocommerce_checkout_before_order_review_heading' ); ?>
45
- <button id="order_review_heading" aria-expanded="false" aria-controls="order_review_content" class="newspack-ui__button newspack-ui__button--ghost" type="button">
46
- <?php esc_html_e( 'Transaction details', 'newspack-blocks' ); ?>
47
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
48
- <path d="M15.9899 10.8888L12.0018 14.3071L8.01368 10.8888L8.98986 9.74988L12.0018 12.3315L15.0137 9.74988L15.9899 10.8888Z"/>
49
- </svg>
50
- </button>
51
- <div class="transaction-details-content">
52
- <div class="transaction-details-content-inner" id="order_review_content">
53
- <?php do_action( 'woocommerce_checkout_before_order_review' ); ?>
54
- <div id="order_review" class="woocommerce-checkout-review-order newspack-ui__box">
55
- <?php
56
- if ( wp_is_block_theme() && function_exists( 'woocommerce_order_review' ) ) {
57
- woocommerce_order_review();
58
- } else {
59
- do_action( 'woocommerce_checkout_order_review' );
60
- }
61
- ?>
62
- </div>
63
- <?php do_action( 'woocommerce_checkout_after_order_review' ); ?>
64
- </div>
45
+ <?php do_action( 'woocommerce_checkout_before_order_review' ); ?>
46
+ <div id="order_review" class="woocommerce-checkout-review-order newspack-ui__box">
47
+ <?php
48
+ if ( wp_is_block_theme() && function_exists( 'woocommerce_order_review' ) ) {
49
+ woocommerce_order_review();
50
+ } else {
51
+ do_action( 'woocommerce_checkout_order_review' );
52
+ }
53
+ ?>
65
54
  </div>
55
+ <?php do_action( 'woocommerce_checkout_after_order_review' ); ?>
66
56
  </div>
67
57
  <?php do_action( 'woocommerce_checkout_after_customer_details' ); ?>
68
58
  <button class="newspack-ui__button newspack-ui__button--secondary newspack-ui__button--wide" id="checkout_edit_billing" type="button"><?php echo esc_html( Modal_Checkout::get_modal_checkout_labels( 'checkout_edit_billing' ) ); ?></button>
@@ -12,6 +12,15 @@ if ( ! wc_coupons_enabled() ) { // @codingStandardsIgnoreLine.
12
12
  return;
13
13
  }
14
14
 
15
+ // Hide the coupon form when a coupon was auto-applied from a Checkout Button
16
+ // block — the reader didn't enter the code, so don't prompt them for one.
17
+ if (
18
+ class_exists( '\Newspack_Blocks\Modal_Checkout' )
19
+ && \Newspack_Blocks\Modal_Checkout::should_hide_coupon_form()
20
+ ) {
21
+ return;
22
+ }
23
+
15
24
  ?>
16
25
  <form class="modal_checkout_coupon woocommerce-form-coupon" method="post">
17
26
  <h3><?php esc_html_e( 'Apply a coupon code', 'newspack-blocks' ); ?></h3>
@@ -60,7 +60,7 @@ $checkout_data = Checkout_Data::get_checkout_data( $order );
60
60
  <path d="M16.7 7.1l-6.3 8.5-3.3-2.5-.9 1.2 4.5 3.4L17.9 8z"></path>
61
61
  </svg>
62
62
  </span>
63
- <p id="modal-checkout-product-details" data-checkout='<?php echo wp_json_encode( $checkout_data ); ?>'>
63
+ <p id="modal-checkout-product-details" <?php Checkout_Data::print_data_checkout_attr( $checkout_data ); ?>>
64
64
  <strong>
65
65
  <?php
66
66
  echo esc_html( Modal_Checkout::get_post_checkout_success_text() );
@@ -3,7 +3,7 @@
3
3
  'name' => 'automattic/newspack-blocks',
4
4
  'pretty_version' => 'dev-main',
5
5
  'version' => 'dev-main',
6
- 'reference' => 'f937e9f691dbdb938f7478a05bc22d51b771311f',
6
+ 'reference' => '559ee55cab1698e9bea567b8f4bb267cc577fef7',
7
7
  'type' => 'wordpress-plugin',
8
8
  'install_path' => __DIR__ . '/../../',
9
9
  'aliases' => array(),
@@ -13,7 +13,7 @@
13
13
  'automattic/newspack-blocks' => array(
14
14
  'pretty_version' => 'dev-main',
15
15
  'version' => 'dev-main',
16
- 'reference' => 'f937e9f691dbdb938f7478a05bc22d51b771311f',
16
+ 'reference' => '559ee55cab1698e9bea567b8f4bb267cc577fef7',
17
17
  'type' => 'wordpress-plugin',
18
18
  'install_path' => __DIR__ . '/../../',
19
19
  'aliases' => array(),
package/webpack.config.js CHANGED
@@ -9,6 +9,7 @@
9
9
  */
10
10
  const fs = require( 'fs' );
11
11
  const getBaseWebpackConfig = require( 'newspack-scripts/config/getWebpackConfig' );
12
+ const { findSourceFile, resolveSourceFile } = require( 'newspack-scripts/config/resolveSource' );
12
13
  const path = require( 'path' );
13
14
  const isDevelopment = process.env.NODE_ENV !== 'production';
14
15
  const blockListFile = process.env.npm_config_block_list || 'block-list.json';
@@ -20,26 +21,19 @@ const blockList = JSON.parse( fs.readFileSync( blockListFile ) );
20
21
  const editorSetup = path.join( __dirname, 'src', 'setup', 'editor' );
21
22
 
22
23
  function blockScripts( type, inputDir, blocks ) {
23
- return blocks.map( block => path.join( inputDir, 'blocks', block, `${ type }.js` ) ).filter( fs.existsSync );
24
+ return blocks.map( block => findSourceFile( path.join( inputDir, 'blocks', block, type ) ) ).filter( Boolean );
24
25
  }
25
26
 
26
27
  const blocksDir = path.join( __dirname, 'src', 'blocks' );
27
28
  const blocks = fs
28
29
  .readdirSync( blocksDir )
29
30
  .filter( block => isDevelopment || blockList.production.includes( block ) )
30
- .filter( block => fs.existsSync( path.join( __dirname, 'src', 'blocks', block, 'editor.js' ) ) );
31
+ .filter( block => findSourceFile( path.join( blocksDir, block, 'editor' ) ) );
31
32
 
32
33
  // Helps split up each block into its own folder view script
33
34
  const viewBlocksScripts = blocks.reduce( ( viewBlocks, block ) => {
34
- const pathToBlock = [ __dirname, 'src', 'blocks', block ];
35
- let viewScriptPath = path.join( ...pathToBlock, 'view.js' );
36
- let fileExists = fs.existsSync( viewScriptPath );
37
- if ( ! fileExists ) {
38
- // Try TS.
39
- viewScriptPath = path.join( ...pathToBlock, 'view.ts' );
40
- fileExists = fs.existsSync( viewScriptPath );
41
- }
42
- if ( fileExists ) {
35
+ const viewScriptPath = findSourceFile( path.join( blocksDir, block, 'view' ) );
36
+ if ( viewScriptPath ) {
43
37
  viewBlocks[ block + '/view' ] = viewScriptPath;
44
38
  }
45
39
  return viewBlocks;
@@ -56,7 +50,7 @@ const entry = {
56
50
  placeholder_blocks: placeholderBlocksScript,
57
51
  editor: editorScript,
58
52
  block_styles: blockStylesScript,
59
- modal: path.join( __dirname, 'src/modal-checkout/modal.js' ),
53
+ modal: resolveSourceFile( path.join( __dirname, 'src/modal-checkout/modal' ) ),
60
54
  modalCheckout: path.join( __dirname, 'src/modal-checkout' ),
61
55
  frequencyBased: path.join( __dirname, 'src/blocks/donate/frequency-based' ),
62
56
  tiersBased: path.join( __dirname, 'src/blocks/donate/tiers-based' ),