Skip to content
This repository has been archived by the owner on Feb 23, 2024. It is now read-only.

Replace filters with an extendibility API to hook into Cart and Checkout blocks #3774

Merged
merged 16 commits into from
Feb 9, 2021

Conversation

Aljullu
Copy link
Contributor

@Aljullu Aljullu commented Feb 1, 2021

Fixes #3735.

This PR refactors the filter added in #3716 so instead of using @wordpress/hooks, it uses our own API. This adds an abstraction layer that should make life easier for extensions and it allows us to more easily make internal changes without breaking 3rd party extensions.

There are a couple of PRs that will need to be updated if we go ahead with this approach: #3750 and #3763. cc @opr

How to test the changes in this Pull Request:

  1. Check out this branch in WC Subscriptions: fix/filters-api (see associated PR: 3979-gh-woocommerce/woocommerce-subscriptions).
  2. Add a subscription product to your cart + any other product which is not a subscription.
  3. Go to the Cart or Checkout blocks and verify the sidebar Total label is Total due today.
  4. Remove the subscription from your cart and verify the Total due today label changes to Total.

@Aljullu Aljullu added the skip-changelog PRs that you don't want to appear in the changelog. label Feb 1, 2021
@Aljullu Aljullu self-assigned this Feb 1, 2021
@github-actions
Copy link
Contributor

github-actions bot commented Feb 1, 2021

Size Change: +338 B (0%)

Total Size: 1.15 MB

Filename Size Change
build/active-filters-frontend.js 8.32 kB -3 B (0%)
build/active-filters.js 8.5 kB +2 B (0%)
build/all-products-frontend.js 34.7 kB -3 B (0%)
build/atomic-block-components/add-to-cart-frontend.js 9.23 kB +1 B (0%)
build/atomic-block-components/category-list-frontend.js 469 B -2 B (0%)
build/atomic-block-components/image-frontend.js 1.76 kB -2 B (0%)
build/atomic-block-components/price-frontend.js 1.83 kB +2 B (0%)
build/atomic-block-components/sale-badge-frontend.js 859 B +2 B (0%)
build/atomic-block-components/stock-indicator-frontend.js 569 B +1 B (0%)
build/atomic-block-components/title-frontend.js 1.35 kB +2 B (0%)
build/attribute-filter-frontend.js 18.2 kB +2 B (0%)
build/attribute-filter.js 12.5 kB +2 B (0%)
build/blocks-checkout.js 16.8 kB +127 B (+1%)
build/cart-frontend.js 75.7 kB +111 B (0%)
build/cart.js 38.6 kB +9 B (0%)
build/checkout-frontend.js 80.5 kB +90 B (0%)
build/checkout.js 41.6 kB +15 B (0%)
build/featured-category.js 7.81 kB -1 B (0%)
build/featured-product.js 10 kB -1 B (0%)
build/handpicked-products.js 7.49 kB -1 B (0%)
build/price-filter-frontend.js 14.5 kB +1 B (0%)
build/price-filter.js 9.93 kB +2 B (0%)
build/product-best-sellers.js 7.56 kB -2 B (0%)
build/product-category.js 8.5 kB -2 B (0%)
build/product-new.js 7.73 kB -2 B (0%)
build/product-on-sale.js 8.12 kB -1 B (0%)
build/product-tag.js 6.56 kB -1 B (0%)
build/product-top-rated.js 7.7 kB -1 B (0%)
build/products-by-attribute.js 8.48 kB -1 B (0%)
build/reviews-by-category.js 11.9 kB -4 B (0%)
build/reviews-by-product.js 13.5 kB -2 B (0%)
build/reviews-frontend.js 9.5 kB -5 B (0%)
build/single-product-frontend.js 37.9 kB +2 B (0%)
build/single-product.js 10.3 kB -1 B (0%)
build/vendors.js 418 kB +2 B (0%)
ℹ️ View Unchanged
Filename Size Change
build/all-products.js 36.3 kB 0 B
build/all-reviews.js 9.88 kB 0 B
build/atomic-block-components/add-to-cart--atomic-block-components/button.js 3.38 kB 0 B
build/atomic-block-components/add-to-cart--atomic-block-components/image--atomic-block-components/title.js 335 B 0 B
build/atomic-block-components/add-to-cart.js 7.68 kB 0 B
build/atomic-block-components/button-frontend.js 2.38 kB 0 B
build/atomic-block-components/button.js 841 B 0 B
build/atomic-block-components/category-list.js 476 B 0 B
build/atomic-block-components/image.js 1.23 kB 0 B
build/atomic-block-components/price.js 1.85 kB 0 B
build/atomic-block-components/rating-frontend.js 520 B 0 B
build/atomic-block-components/rating.js 526 B 0 B
build/atomic-block-components/sale-badge.js 863 B 0 B
build/atomic-block-components/sku-frontend.js 390 B 0 B
build/atomic-block-components/sku.js 393 B 0 B
build/atomic-block-components/stock-indicator.js 574 B 0 B
build/atomic-block-components/summary-frontend.js 918 B 0 B
build/atomic-block-components/summary.js 927 B 0 B
build/atomic-block-components/tag-list-frontend.js 466 B 0 B
build/atomic-block-components/tag-list.js 472 B 0 B
build/atomic-block-components/title.js 1.21 kB 0 B
build/blocks.js 3.49 kB 0 B
build/editor-rtl.css 14.9 kB 0 B
build/editor.css 14.9 kB 0 B
build/price-format.js 1.34 kB 0 B
build/product-categories.js 3.23 kB 0 B
build/product-search.js 3.56 kB 0 B
build/style-rtl.css 18.9 kB 0 B
build/style.css 18.9 kB 0 B
build/vendors--atomic-block-components/price-frontend.js 5.73 kB 0 B
build/vendors-style-rtl.css 1.05 kB 0 B
build/vendors-style.css 1.05 kB 0 B
build/wc-blocks-data.js 6.98 kB 0 B
build/wc-blocks-middleware.js 1.1 kB 0 B
build/wc-blocks-registry.js 2.67 kB 0 B
build/wc-payment-method-bacs.js 820 B 0 B
build/wc-payment-method-cheque.js 816 B 0 B
build/wc-payment-method-cod.js 913 B 0 B
build/wc-payment-method-paypal.js 853 B 0 B
build/wc-payment-method-stripe.js 12.2 kB 0 B
build/wc-settings.js 2.4 kB 0 B
build/wc-shared-context.js 1.53 kB 0 B
build/wc-shared-hocs.js 1.69 kB 0 B

compressed-size-action

@Aljullu Aljullu changed the title [WIP] Replace filters with an extendibility API to hook into Cart and Checkout blocks Replace filters with an extendibility API to hook into Cart and Checkout blocks Feb 2, 2021
@Aljullu Aljullu added category: extensibility Work involving adding or updating extensibility. Useful to combine with other scopes impacted. block: cart Issues related to the cart block. block: checkout Issues related to the checkout block. status: needs review labels Feb 2, 2021
@Aljullu Aljullu marked this pull request as ready for review February 2, 2021 17:08
@Aljullu Aljullu requested a review from a team as a code owner February 2, 2021 17:08
@Aljullu Aljullu requested review from ralucaStan and removed request for a team February 2, 2021 17:08
Copy link
Contributor

@ralucaStan ralucaStan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed I added a comment about applying multiple filters and the expected result. 👍

packages/checkout/registry/index.js Show resolved Hide resolved
const newValue = filter( value, args );
value = validate( newValue ) ? newValue : value;
} );
return value;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works in this case because we have one filter: totalLabel.
But if there were more filters the value returned would be of the last filter applied. Is this the expected result, or should the filters have some sort of priority?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, that's an accepted tradeoff, we don't plan on introducing priority for the time.

Copy link
Member

@senadir senadir left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your effort here Albert, I still think we should apply some changes before shipping this, mainly removing the ability to pass Elements and guarding the executed functions.

Comment on lines 30 to 35
'totalLabel',
__( 'Total', 'woo-gutenberg-products-block' ),
{
extensions,
},
__experimentalValidateElementOrString
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using an object would make reading this much easier.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. 👍

export * from './registry';
export * from './registry/validations.js';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'./registry/validations.js' would probably make more sense to be inside utils

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the changes in 094eea7 (we only accept strings for this filter), I ended up removing that file altogether.

const filters = getCheckoutFilters( filterName );
let value = defaultValue;
filters.forEach( ( filter ) => {
const newValue = filter( value, args );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is too dangerous to just execute like this, any malfunction in the 3PD code would break our block.
You can test this by changing extensions to extension in WCS code, the whole cart block is now broken.

The error can be at runtime or can happen with a version mismatch (so the 3PD import something that is no longer available) or just anything.

A very important principle when developing the new extensibility points is to not trust the developer.

The filter call should be wrapped in a try/catch closure that will skip that particular filter something wrong happens.

Skipping filters is only possible on raw values, we can't know for sure if an element is faulty or not until we evaluate it, meaning we would break other extensions.

We could wrap our __experimentalApplyCheckoutFilter call in a BlockErrorBoudary but that would still neglect the rest of filters.

We can also nest all filters into each other somehow, but I'm not sure how would that happen.

I'd suggest we skip element for now and only have raw values until we have a strong case for elements and then we can think about it very well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good points, thanks for the feedback. In 660c026 I wrapped the filter function in a try { ... } catch { ... } block and in 094eea7 I updated the validation so only strings are accepted. Let me know if changes make sense.

let value = defaultValue;
filters.forEach( ( filter ) => {
const newValue = filter( value, args );
value = validate( newValue ) ? newValue : value;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validate only checks the returned value but doesn't guarantee no error was thrown before reaching the return point.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be fixed in the commits linked in the previous comment.

@senadir
Copy link
Member

senadir commented Feb 7, 2021

@Aljullu I rebased this PR to trunk so it wouldn't conflict more (and to resolve conflict on my PR).

@Aljullu Aljullu force-pushed the fix/3735-filters-api branch from cf84ca4 to 73cb8e4 Compare February 8, 2021 14:45
@Aljullu
Copy link
Contributor Author

Aljullu commented Feb 8, 2021

Thanks for the feedback @ralucaStan and @senadir. I made several changes to apply the feedback, so I think this is ready for another look.

Comment on lines +59 to +60
// eslint-disable-next-line no-console
console.log( e );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logging errors is not very practical, it wouldn't trigger an error on the console (easily missed) and would happen to all users or admins.
I will write a P2 to suggest how we can catch and expose errors, for now, how about you check if the current use is an admin, fi so, you throw, or else you neglect the error and move on.

Suggested change
// eslint-disable-next-line no-console
console.log( e );
if ( CURRENT_USER_IS_ADMIN ) {
throw e;
}

Copy link
Contributor Author

@Aljullu Aljullu Feb 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will probably need to revisit this after the P2 discussion, but for now I made the change you suggested (throw when the user is an admin) but still kept the console.error for costumers: 1712766. Let me know how it looks.

Comment on lines +12 to +20
test( 'should return default value if there are no filters', () => {
const value = 'Hello World';
const newValue = __experimentalApplyCheckoutFilter( {
filterName,
defaultValue: value,
} );

expect( newValue ).toBe( value );
} );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another test we can add (we need to mock CURRENT_USER_IS_ADMIN) is passing in invalid code (that throws an exception) and making sure previous filter still passed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a bad time mocking CURRENT_USER_IS_ADMIN so it had different values in two different tests. I ended up splitting them in two files: 1d5b636. There might better ways of doing this (let me know if you find how!), but couldn't figure it out how after spending some time on this.

Copy link
Member

@senadir senadir left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
block: cart Issues related to the cart block. block: checkout Issues related to the checkout block. category: extensibility Work involving adding or updating extensibility. Useful to combine with other scopes impacted. skip-changelog PRs that you don't want to appear in the changelog.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Integration Tracking (WC Subscriptions): create an API so extensions can hook into Cart and Checkout blocks
3 participants