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

Add payment method api and components to checkout steps #1349

Merged
merged 28 commits into from
Jan 6, 2020

Conversation

nerrad
Copy link
Contributor

@nerrad nerrad commented Dec 8, 2019

Addresses: #1347 and #1302

This pull is a reference pull (so may not be directly implemented as is) for a proposed approach to implementing payment methods in the new checkout block. It takes some inspiration from the composite checkout api in calypso, however, there are aspects of this that differ due to the need for extensibility in a WordPress plugin environment.

Some goals of this proposal:

  • create a relatively trivial and clear api for payment methods to register their ui/ux and logic.
  • protect the integrity of the new checkout ui (limit what payment methods can do to the bare minimum necessary).
  • isolate (encapsulate) the payment method processing and handling so that the payment methods logic is kept separate from the checkout. The checkout block should be mostly agnostic towards what payment methods are in use. All data the payment methods need should be passed in through props.
  • giving control to the payment method for it's logic should aid in allowing payment methods to keep as much of their current processing logic intact as possible.
  • While the checkout is "mostly agnostic" of payment method, it might still need to be aware of what state the payment method processing is in.
  • For the MVP, the checkout block will detect existing payment methods we want to support (Stripe with apple pay, and paypal (express?). That means the necessary payment method logic will be registered in the blocks plugin for now if those payment methods are present and active, however we should move the logic to the specific payment methods as soon as possible. What we develop here will serve as the example for all payment methods to hook into the new checkout.

Note: this pull is functional with dummy payment method examples. Functional in the sense of seeing the different parts in action but not functional in the sense of processing actual payments (or being wired up to a payment gateway).

With this in mind, this pull documents a structural approach to handling payment method integration with the new checkout block:

API

CheckoutContext (commit b85b11a)

The checkout context is the mechanism by which payment method and checkout data etc is made available to all components within the checkout component tree.

The CheckoutProvider initializes the api for the checkout and allows for setting an initialActivePaymentMethod (which may be something configured in Woo Core?) .

Consumers of the context should generally not reference the various elements on the context directly but instead consume from the more granular hooks. These hooks will be documented further in later paragraphs including the api attached to them.

You can view the CheckoutProvider implemented in the checkout block here: f0509e8

Checkout Hooks (see 316656d)

These are various hooks that can be used in various checkout components (along with being implemented in payment method components) to expose different slices of the checkout context api.

useCheckoutData

For this example, this is hardcoded. However, most likely this will be wired up to any wp.data stores retrieving cart/checkout data. This data is passed through to the registered payment method components. There is also a updateCheckoutData callback that could be used by other components in the checkout form (when things change?).

Questions:

  • will it be sufficient to pass along this checkout data to payment method components?
  • what checkout data will be needed by payment methods to complete the transaction?

useCheckoutEvents

The useCheckoutEvents hook exposes the following from the Checkout context.

  • checkoutComplete and setCheckoutComplete. This event state would be set when the checkout has completed and provides a callback for setting that state.
  • checkoutHasError and setCheckoutHasError. This event state would be used when indicating that the checkout currently has an error state. Errors would likely be exposed via notices (see useCheckoutNotice). It might also influence validation schemes (which aren't a part of this pull currently).
  • isCalculating and setIsCalculating. This checkout event state could be utilized when the checkout is recalculating totals. Things that might trigger this are shipping selections, changes in product quantity, variation adjustments etc.

Questions:

  • Is the checkout complete state something that should be exposed to the payment method or something that should be controlled solely by the checkout component itself? I think the latter in case there is any followup work needed by the checkout form before redirecting after payment success or failure?
  • Are there any other event type states that need to be exposed to payment methods?
  • What checkout components would be affected by these checkout event states?

useCheckoutNotices

This hook still isn't fully fleshed out, but it would be utilized as a way for managing notices in the checkout context. Three interfaces are exposed via this hook:

  • notices - an array of current notices.
  • addNotice - a callback for adding a notice.
  • removeNotice - a callback for removing a notice.

In building out this api, it'd probably be good to formalize a Notice object so we can validate type etc.

useCheckoutRedirectUrls

This hook allows for setting different urls that the checkout should redirect too on completion. There are 4 interfaces exposed by this hook:

  • successRedirectUrl and setSuccessRedirectUrl: This is the url that would be redirected to on successful payment.
  • failureRedirectUrl and setFailureRedirectUrl: This is the url that would be redirected to on failed payment.

Questions:

  • Is this something that needs exposed to payment methods? Or is it consistent enough that this would be solely determined by the checkout block (which might remove the need for this context api)?
  • Are there any other url type data that needs exposed to checkout by payment methods or vice-versa?

Payment Method Hooks (see 30bd5f1)

These hooks expose various api and interfaces to payment methods that help with communicating payment and payment method state to the checkout components.

useActivePaymentMethod

This hook exposes activePaymentMethod and setActivePaymentMethod. activePaymentMethod is the slug of the active registered payment method in the regular payment area (not express payment methods) and the callback (setActivePaymentMethod) is used to update the active payment method when the user selects a tab. This can be fed to payment method components so the payment method component can know when it is active and have the correct logic.

If initialActivePaymentMethod is not configured on the CheckoutProvider then the activePaymentMethod will be initialized to the first registered payment method.

usePaymentEvents

The usePaymentEvents hook exposes the current payment status and a callback for setting the payment status via select and dispatch callbacks. Passed through to payment methods, the payment method uses this to indicate it's current state to the checkout (and also the payment method itself). Payment event status would be one of the following:

  • pristine: (select.isPristine() or dispatch.pristine()). This status is when payment processing is idle and in an initialized state.
  • started: (select.isStarted() or dispatch.started()). This status indicates that payment processing has started.
  • finished: (select.isFinished() or dispatch.finished()). This status indicates the payment processing has finished. I think we might drop this one, since I don't think this state would exist considering the following three...
  • has_error: (select.hasError() or dispatch.error()). Indicates there was an error in payment processing (may or may not be finished, but is interrupted by the error. I'm assuming this state would be used for a recoverable error where the payment method could be tried again (validation error, invalid credit card, temporary communication error etc).
  • failed: (select.hasFailed() or dispatch.failed(). Indicates the payment processing failed. I'm assuming the checkout would use this state to trigger redirect to the failure url.
  • success: (select.isSuccessful or dispatch.success(). Indicates the payment processing succeeded. I'm assuming the checkout would use this state to trigger redirect to the success url.

Questions:

  • Will these payment event statuses be sufficient in communicating payment state to the checkout (and for internal payment method use)? If not, what other event states do you think are necessary?
  • Is there use-case for having more than one status active at a time?

Payment method registration api (see 83e14ad)

To fulfill the goal of keeping payment method logic separate from the checkout (and extendable). The registration api enables payment methods to register themselves with the checkout. The api is exposed on the @woocommerce/block-registry (or wc.wcBlocksRegistry global via the wc-blocks-registry handle) and would be implemented by payment methods by enqueuing their components via something like this php side:

<?php
wp_enqueue_script( 'paypal-script', $url_to_paypal_component_scripts, [ 'wc-blocks-registry' ], $version );

Then the paypal component javascript would register it's components and logic via the following api client side:

registerPaymentMethod

This api expects two arguments. The first, slug is expected to be a unique slug identifying the payment method. The second is expected to be an object with the following properties and values:

Property Value Type Value Description
tab React component This will be the component returning the content for the payment method tab. Typically it'd be a img or text.
content React component This will be the main component returning a ui additional information needed by the payment method (cc info, paypal email address?) when the payment method is active. It also is the entry point for any payment method processing that is triggered when the checkout is submitted. This will receive various props when called related to the checkout context mentioned earlier.
ariaLabel string This is the content for the aria label attached to the payment method to assist with accessibility for the tab.

Something we may need to add to this api will be billingFields if a payment method exposes billing fields for embedding in a checkout step (as opposed to part of a payment method modal process).

registerExpressPaymentMethod

This api interface expects two arguments. The first, slug is expected to be a unique slug identifying the payment method. The second is expected to be a react component that returns a ui interface for triggering the express payment process. Similar to the registered payment methods mentioned earlier, this component will receive the necessary interfaces from the checkout context as props.

Payment Method Wrapper Components (see 0b430a2)

These components are the wrappers around the payment methods and express payment methods registered via the payment method registration api. They handle prepping the payment methods and feeding them the needed props from the checkout context.

Example Demo of Payment Method registration/implementation (see 0b430a2)

In this commit, you can view an example of how payment methods might get registered and implemented. Obviously this is not fully functional, but illustrates how pms are kept distinct from the checkout. They can serve as the shell from which to build out functional payment methods for our initial MVP.

Next steps:

Now that this reference pull is in place, the following should happen as next steps:

  • Present this pull for feedback to involved parties (payment method extensions, various woo team members working on payment methods, feedback from this team).
  • Decide if this (or a modification of this) is the accepted strategy/approach for completing the MVP.

Once the above is done, I would suggest breaking this pull down into smaller individual pulls that can be reviewed and merged in separately (or we could do this all in one merge and iterate...)

  • Tabs component. I added an example in here, but we should evaluate whether we should grab this from either @wordpress/components or some other source (or just iterate on what I started here - if we do that, we may want to look into the tab component in reakit
  • There are other components we're likely going to need to expose to payment methods to assist them in composing their uis with consistent styling for the checkout block. We may need to temporarily exposing these on wc.wcBlockComponents global (and potentially move to @woocommerce/components and serve from @woocommerce/components). Components like various form fields/cc fields etc (can we grab some of these from the composite-checkout work?)
  • Each commit in this pull could feasibly become it's own pull.
  • Finish building out payment method examples using payment method logic from the payment methods we'll support in the initial iteration.
  • Add tests where it makes sense.

@nerrad nerrad self-assigned this Dec 8, 2019
@nerrad nerrad added focus: components Work that introduces new or updates existing components. type: feature request labels Dec 8, 2019
@nerrad nerrad force-pushed the add/payment-method-steps-checkout branch from e33ae7c to 4c1e5de Compare December 11, 2019 15:09
@mikejolley
Copy link
Member

@nerrad On the payment side, is it safe to do payments on client side? I worry about tampering. I was thinking we'd need to a) create an order (draft) on server side so values are correct, then b) trigger payment from client side, to be handled on server side by a gateway. e.g. Ref an order ID and send the payment handler (on server) a token, or payment details, or simply process and return something such as a redirect URL.

@nerrad
Copy link
Contributor Author

nerrad commented Dec 11, 2019

payments won't be handled by handled by the new checkout flow. they will be handled by existing payment methods hooked into the checkout flow (so their existing process for handling payments should remain intact for the most part)

@mikejolley
Copy link
Member

@nerrad Ok, I was looking at usePaymentData which if in wp.data is on the client side right? if you're taking properties from the wp.data cart and not an order that has gone through proper validation there is a risk that data could be invalid.

@nerrad
Copy link
Contributor Author

nerrad commented Dec 11, 2019

usePaymentData is maybe a bad name, it's only intended to be data to pass from the checkout to the payment method that it can use to process any payments. Essentially payment methods will "plug in" as their own modules that can process payments however they want and communicate status back to the checkout as needed. So the majority of existing payment method logic (any ajax requests, library requests etc) should for the most part be able to stay intact.

@nerrad nerrad force-pushed the add/payment-method-steps-checkout branch 3 times, most recently from 65774f3 to be42c0a Compare December 15, 2019 22:10
@nerrad nerrad marked this pull request as ready for review December 16, 2019 00:50
@nerrad nerrad requested a review from a team December 16, 2019 00:50
Copy link

@DanReyLop DanReyLop left a comment

Choose a reason for hiding this comment

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

I have some thoughts, some of them trivial. This looks promising so far :)

Copy link

@sirbrillig sirbrillig left a comment

Choose a reason for hiding this comment

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

Very nice work here. I'd love to talk though the similarities/differences with CompositeCheckout to see how both could be improved!

activePaymentMethod,
setActivePaymentMethod,
} = useActivePaymentMethod();
const getRenderedTab = useCallback(

Choose a reason for hiding this comment

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

What's the purpose of memoizing this function? Is it to preserve something in the payment method component?

If it's just to inject the selectedTab as a render prop, could we use a custom hook (eg: getSelectedTab) instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Probably no need to memoize actually.

If it's just to inject the selectedTab as a render prop, could we use a custom hook (eg: getSelectedTab) instead?

Hmm... I'm not sure I follow what you are suggesting here. Do you mean useSelectedTab?

Choose a reason for hiding this comment

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

Hmm... I'm not sure I follow what you are suggesting here. Do you mean useSelectedTab?

Oh, um, yes. I think that is probably what I meant. 😅 Sorry.

assets/js/base/components/tabs/index.js Outdated Show resolved Hide resolved
assets/js/base/components/tabs/index.js Outdated Show resolved Hide resolved
assets/js/blocks-registry/payment-methods/assertions.js Outdated Show resolved Hide resolved
assets/js/blocks-registry/payment-methods/assertions.js Outdated Show resolved Hide resolved
@nerrad nerrad force-pushed the add/payment-method-steps-checkout branch from be42c0a to 8acd0bc Compare December 17, 2019 12:33
@nerrad nerrad force-pushed the add/payment-method-steps-checkout branch from 51216a2 to d6c7e4f Compare December 19, 2019 15:49
@nerrad nerrad added skip-changelog PRs that you don't want to appear in the changelog. status: in progress and removed status: needs review labels Dec 19, 2019
( Object.keys( paymentMethods ).length === 0 && isInitialized )
) {
// @todo this can be a placeholder informing the user there are no
// payment methods setup?
Copy link
Contributor Author

@nerrad nerrad Dec 19, 2019

Choose a reason for hiding this comment

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

...but only in the editor. Might want to consider something for the frontend to communicate to customers that payments can't be completed?

@nerrad nerrad force-pushed the add/payment-method-steps-checkout branch from 289bfe4 to 484771e Compare December 20, 2019 14:07
@sirbrillig
Copy link

Looking good!

@nerrad nerrad force-pushed the add/payment-method-steps-checkout branch from d7ac549 to 8149971 Compare January 3, 2020 20:54
this breaks the failing unit tests
@nerrad
Copy link
Contributor Author

nerrad commented Jan 3, 2020

I feel good about the state of this pull, getting it merged, and continuing on with tasks in followups (see #1349 (comment) for already created list).

So this could use a final review to catch any major issues and get it merged in?

Copy link
Contributor

@Aljullu Aljullu left a comment

Choose a reason for hiding this comment

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

All the feedback I provided above has been addressed (thanks @nerrad!) and taking an overall look at the code it looks good to me. I added some suggestions after a second look at the code. They are all nitpicking comments in case you want to consider them, but I'm ok with how this PR is now so approving. 👍

assets/js/base/components/tabs/index.js Show resolved Hide resolved
assets/js/base/components/tabs/index.js Outdated Show resolved Hide resolved
assets/js/base/components/tabs/index.js Show resolved Hide resolved
assets/js/base/components/tabs/index.js Outdated Show resolved Hide resolved
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
focus: components Work that introduces new or updates existing components. 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.

7 participants