-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Data Stores: Refactor partner portal credit card store to createReduxStore()
- take 2
#75377
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,41 +1,40 @@ | ||
import { Button, FormStatus, useFormStatus } from '@automattic/composite-checkout'; | ||
import { useElements, CardElement } from '@stripe/react-stripe-js'; | ||
import { useSelect } from '@wordpress/data'; | ||
import { useDispatch, useSelect } from '@wordpress/data'; | ||
import { useI18n } from '@wordpress/react-i18n'; | ||
import debugFactory from 'debug'; | ||
import { useMemo } from 'react'; | ||
import { creditCardStore } from 'calypso/state/partner-portal/credit-card-form'; | ||
import type { StripeConfiguration } from '@automattic/calypso-stripe'; | ||
import type { ProcessPayment } from '@automattic/composite-checkout'; | ||
import type { StoreState } from '@automattic/wpcom-checkout'; | ||
import type { Stripe } from '@stripe/stripe-js'; | ||
import type { I18n } from '@wordpress/i18n'; | ||
import type { State } from 'calypso/state/partner-portal/credit-card-form/reducer'; | ||
import type { CreditCardSelectors } from 'calypso/state/partner-portal/types'; | ||
|
||
const debug = debugFactory( 'calypso:partner-portal:credit-card' ); | ||
|
||
export default function CreditCardSubmitButton( { | ||
disabled, | ||
onClick, | ||
store, | ||
stripe, | ||
stripeConfiguration, | ||
activeButtonText, | ||
}: { | ||
disabled?: boolean; | ||
onClick?: ProcessPayment; | ||
store: State; | ||
stripe: Stripe | null; | ||
stripeConfiguration: StripeConfiguration | null; | ||
activeButtonText: string | undefined; | ||
} ) { | ||
const { __ } = useI18n(); | ||
const fields: StoreState< string > = useSelect( | ||
( select ) => ( select( 'credit-card' ) as CreditCardSelectors ).getFields(), | ||
[] | ||
); | ||
const useAsPrimaryPaymentMethod: boolean = useSelect( | ||
( select ) => ( select( 'credit-card' ) as CreditCardSelectors ).useAsPrimaryPaymentMethod(), | ||
const { fields, useAsPrimaryPaymentMethod, errors, incompleteFieldKeys } = useSelect( | ||
( select ) => { | ||
const store = select( creditCardStore ); | ||
return { | ||
fields: store.getFields(), | ||
useAsPrimaryPaymentMethod: store.useAsPrimaryPaymentMethod(), | ||
errors: store.getCardDataErrors(), | ||
incompleteFieldKeys: store.getIncompleteFieldKeys(), | ||
}; | ||
}, | ||
Comment on lines
+28
to
+37
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This contains 2 different sets of changes:
|
||
[] | ||
); | ||
const cardholderName = fields.cardholderName; | ||
|
@@ -54,30 +53,53 @@ export default function CreditCardSubmitButton( { | |
return __( 'Please wait…' ); | ||
}, [ formStatus, activeButtonText, __ ] ); | ||
|
||
const { setCardDataError, setFieldValue, setFieldError } = useDispatch( creditCardStore ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We're using |
||
|
||
const handleButtonClick = () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We're inlining this function to be able to use the data from the hooks. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice. This is way neater now. |
||
debug( 'validating credit card fields' ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We're also inlining the credit card validation function here, that way we can take advantage of the existing store values without having to pass the entire store object. We could keep the function separate, but then we'd still need to pass all the various state data and mapped action creators, which is an extra step of unnecessary complication IMHO. |
||
|
||
if ( ! cardholderName?.value.length ) { | ||
// Touch the field so it displays a validation error | ||
setFieldValue( 'cardholderName', '' ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See how natural it is to dispatch actions that way. |
||
setFieldError( 'cardholderName', __( 'This field is required' ) ); | ||
} | ||
const areThereErrors = Object.keys( errors ).some( ( errorKey ) => errors[ errorKey ] ); | ||
|
||
if ( incompleteFieldKeys.length > 0 ) { | ||
// Show "this field is required" for each incomplete field | ||
incompleteFieldKeys.map( ( key: string ) => | ||
setCardDataError( key, __( 'This field is required' ) ) | ||
); | ||
} | ||
|
||
if ( areThereErrors || ! cardholderName?.value.length || incompleteFieldKeys.length > 0 ) { | ||
// credit card is invalid | ||
return false; | ||
} | ||
|
||
debug( 'submitting stripe payment' ); | ||
|
||
if ( ! onClick ) { | ||
throw new Error( | ||
'Missing onClick prop; CreditCardSubmitButton must be used as a payment button in CheckoutSubmitButton' | ||
); | ||
} | ||
|
||
onClick( { | ||
stripe, | ||
name: cardholderName?.value, | ||
stripeConfiguration, | ||
cardElement, | ||
useAsPrimaryPaymentMethod, | ||
} ); | ||
return; | ||
}; | ||
|
||
return ( | ||
<Button | ||
className={ ! formSubmitting ? 'button is-primary' : '' } | ||
disabled={ disabled } | ||
onClick={ () => { | ||
if ( isCreditCardFormValid( store, __ ) ) { | ||
debug( 'submitting stripe payment' ); | ||
|
||
if ( ! onClick ) { | ||
throw new Error( | ||
'Missing onClick prop; CreditCardSubmitButton must be used as a payment button in CheckoutSubmitButton' | ||
); | ||
} | ||
|
||
onClick( { | ||
stripe, | ||
name: cardholderName?.value, | ||
stripeConfiguration, | ||
cardElement, | ||
useAsPrimaryPaymentMethod, | ||
} ); | ||
return; | ||
} | ||
} } | ||
onClick={ handleButtonClick } | ||
buttonType="primary" | ||
isBusy={ formSubmitting } | ||
fullWidth | ||
|
@@ -86,31 +108,3 @@ export default function CreditCardSubmitButton( { | |
</Button> | ||
); | ||
} | ||
|
||
function isCreditCardFormValid( store: State, __: I18n[ '__' ] ) { | ||
debug( 'validating credit card fields' ); | ||
|
||
const fields = store.selectors.getFields( store.getState() ); | ||
const cardholderName = fields.cardholderName; | ||
if ( ! cardholderName?.value.length ) { | ||
// Touch the field so it displays a validation error | ||
store.dispatch( store.actions.setFieldValue( 'cardholderName', '' ) ); | ||
store.dispatch( | ||
store.actions.setFieldError( 'cardholderName', __( 'This field is required' ) ) | ||
); | ||
} | ||
const errors = store.selectors.getCardDataErrors( store.getState() ); | ||
const incompleteFieldKeys = store.selectors.getIncompleteFieldKeys( store.getState() ); | ||
const areThereErrors = Object.keys( errors ).some( ( errorKey ) => errors[ errorKey ] ); | ||
|
||
if ( incompleteFieldKeys.length > 0 ) { | ||
// Show "this field is required" for each incomplete field | ||
incompleteFieldKeys.map( ( key: string ) => | ||
store.dispatch( store.actions.setCardDataError( key, __( 'This field is required' ) ) ) | ||
); | ||
} | ||
if ( areThereErrors || ! cardholderName?.value.length || incompleteFieldKeys.length > 0 ) { | ||
return false; | ||
} | ||
return true; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,20 +7,16 @@ import { Elements } from '@stripe/react-stripe-js'; | |
import { loadStripe } from '@stripe/stripe-js'; | ||
import { render, screen } from '@testing-library/react'; | ||
import userEvent from '@testing-library/user-event'; | ||
import { useSelect } from '@wordpress/data'; | ||
import { Provider } from 'react-redux'; | ||
import configureStore from 'redux-mock-store'; | ||
import * as actions from 'calypso/state/partner-portal/credit-card-form/actions'; | ||
import * as selectors from 'calypso/state/partner-portal/credit-card-form/selectors'; | ||
import CreditCardSubmitButton from '../credit-card-submit-button'; | ||
|
||
const mockUseSelector = () => () => null; | ||
|
||
jest.mock( '@stripe/stripe-js', () => ( { | ||
loadStripe: () => null, | ||
} ) ); | ||
|
||
jest.mock( '@wordpress/data' ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't need all those mocks; we can just let |
||
jest.mock( 'calypso/state/partner-portal/credit-card-form/selectors', () => { | ||
const items = jest.requireActual( 'calypso/state/partner-portal/credit-card-form/selectors' ); | ||
return { | ||
|
@@ -71,7 +67,6 @@ describe( '<CreditCardSubmitButton>', () => { | |
beforeEach( () => { | ||
// Re-mock dependencies | ||
jest.clearAllMocks(); | ||
useSelect.mockImplementation( mockUseSelector ); | ||
useFormStatus.mockImplementation( () => { | ||
return { | ||
formStatus: 'ready', | ||
|
@@ -81,7 +76,6 @@ describe( '<CreditCardSubmitButton>', () => { | |
} ); | ||
|
||
afterEach( () => { | ||
useSelect.mockClear(); | ||
useFormStatus.mockClear(); | ||
} ); | ||
|
||
|
@@ -106,7 +100,6 @@ describe( '<CreditCardSubmitButton>', () => { | |
const buttonText = 'Save payment method'; | ||
|
||
const props = { | ||
store: newStore, | ||
stripe, | ||
stripeConfiguration, | ||
disabled: false, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,5 @@ | ||
import { useMemo } from 'react'; | ||
import { createStoredCreditCardMethod } from 'calypso/jetpack-cloud/sections/partner-portal/payment-methods/stored-credit-card-method'; | ||
import { createStoredCreditCardPaymentMethodStore } from 'calypso/state/partner-portal/credit-card-form'; | ||
import type { StripeConfiguration, StripeLoadingError } from '@automattic/calypso-stripe'; | ||
import type { PaymentMethod } from '@automattic/composite-checkout'; | ||
import type { Stripe } from '@stripe/stripe-js'; | ||
|
@@ -20,18 +19,15 @@ export function useCreateStoredCreditCardMethod( { | |
} ): PaymentMethod | null { | ||
const shouldLoadStripeMethod = ! isStripeLoading && ! stripeLoadingError; | ||
|
||
const store = useMemo( () => createStoredCreditCardPaymentMethodStore(), [] ); | ||
|
||
return useMemo( | ||
() => | ||
shouldLoadStripeMethod | ||
? createStoredCreditCardMethod( { | ||
store, | ||
stripe, | ||
stripeConfiguration, | ||
activePayButtonText, | ||
} ) | ||
: null, | ||
[ shouldLoadStripeMethod, store, stripe, stripeConfiguration, activePayButtonText ] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We no longer need the store since we reference it directly in the component. |
||
[ shouldLoadStripeMethod, stripe, stripeConfiguration, activePayButtonText ] | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,15 +3,12 @@ import CreditCardSubmitButton from 'calypso/jetpack-cloud/sections/partner-porta | |
import type { StripeConfiguration } from '@automattic/calypso-stripe'; | ||
import type { PaymentMethod } from '@automattic/composite-checkout'; | ||
import type { Stripe } from '@stripe/stripe-js'; | ||
import type { State } from 'calypso/state/partner-portal/credit-card-form/reducer'; | ||
|
||
export function createStoredCreditCardMethod( { | ||
store, | ||
stripe, | ||
stripeConfiguration, | ||
activePayButtonText = undefined, | ||
}: { | ||
store: State; | ||
stripe: Stripe | null; | ||
stripeConfiguration: StripeConfiguration | null; | ||
activePayButtonText?: string | undefined; | ||
|
@@ -23,7 +20,6 @@ export function createStoredCreditCardMethod( { | |
activeContent: <CreditCardFields />, | ||
submitButton: ( | ||
<CreditCardSubmitButton | ||
store={ store } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We no longer need the store since we reference it directly in the component. |
||
stripe={ stripe } | ||
stripeConfiguration={ stripeConfiguration } | ||
activeButtonText={ activePayButtonText } | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,12 @@ | ||
import { registerStore } from '@wordpress/data'; | ||
import { createReduxStore, register } from '@wordpress/data'; | ||
import * as actions from 'calypso/state/partner-portal/credit-card-form/actions'; | ||
import reducer from 'calypso/state/partner-portal/credit-card-form/reducer'; | ||
import * as selectors from 'calypso/state/partner-portal/credit-card-form/selectors'; | ||
|
||
export function createStoredCreditCardPaymentMethodStore(): Record< string, unknown > { | ||
const store = registerStore( 'credit-card', { | ||
reducer, | ||
actions, | ||
selectors, | ||
} ); | ||
export const creditCardStore = createReduxStore( 'credit-card', { | ||
reducer, | ||
actions, | ||
selectors, | ||
} ); | ||
|
||
return { ...store, actions, selectors }; | ||
} | ||
register( creditCardStore ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nothing special about most of the changed instances - we reference the store through the existing store object instead of its name, which is the recommended way in Gutenberg nowadays.