From 905f99043bd558e187b3005e9b8f56ef7c442163 Mon Sep 17 00:00:00 2001 From: Jenni Nurmi Date: Thu, 25 Jul 2019 10:44:18 +0300 Subject: [PATCH] Create PaymentMethodsPage --- .../PaymentMethodsPage/PaymentMethodsPage.css | 28 ++ .../PaymentMethodsPage.duck.js | 106 ++++++ .../PaymentMethodsPage/PaymentMethodsPage.js | 306 ++++++++++++++++++ src/containers/index.js | 1 + src/containers/reducers.js | 2 + src/routeConfiguration.js | 10 + src/translations/en.json | 16 +- 7 files changed, 466 insertions(+), 3 deletions(-) create mode 100644 src/containers/PaymentMethodsPage/PaymentMethodsPage.css create mode 100644 src/containers/PaymentMethodsPage/PaymentMethodsPage.duck.js create mode 100644 src/containers/PaymentMethodsPage/PaymentMethodsPage.js diff --git a/src/containers/PaymentMethodsPage/PaymentMethodsPage.css b/src/containers/PaymentMethodsPage/PaymentMethodsPage.css new file mode 100644 index 00000000..360e74b2 --- /dev/null +++ b/src/containers/PaymentMethodsPage/PaymentMethodsPage.css @@ -0,0 +1,28 @@ +@import '../../marketplace.css'; + +.content { + @media (--viewportMedium) { + margin: 32px auto 0 auto; + width: 100%; + max-width: 564px; + } + + @media (--viewportLarge) { + margin: 0; + } +} + +.desktopTopbar, +.mobileTopbar { + box-shadow: none; +} + +.title { + hyphens: auto; + margin-top: 8px; + margin-bottom: 19px; + + @media (--viewportMedium) { + margin-bottom: 47px; + } +} diff --git a/src/containers/PaymentMethodsPage/PaymentMethodsPage.duck.js b/src/containers/PaymentMethodsPage/PaymentMethodsPage.duck.js new file mode 100644 index 00000000..3634eade --- /dev/null +++ b/src/containers/PaymentMethodsPage/PaymentMethodsPage.duck.js @@ -0,0 +1,106 @@ +import { fetchCurrentUser } from '../../ducks/user.duck'; +import { setInitialValues as setInitialValuesForPaymentMethods } from '../../ducks/paymentMethods.duck'; +import { storableError } from '../../util/errors'; +import * as log from '../../util/log'; + +// ================ Action types ================ // + +export const SETUP_INTENT_REQUEST = 'app/PaymentMethodsPage/SETUP_INTENT_REQUEST'; +export const SETUP_INTENT_SUCCESS = 'app/PaymentMethodsPage/SETUP_INTENT_SUCCESS'; +export const SETUP_INTENT_ERROR = 'app/PaymentMethodsPage/SETUP_INTENT_ERROR'; + +export const STRIPE_CUSTOMER_REQUEST = 'app/PaymentMethodsPage/STRIPE_CUSTOMER_REQUEST'; +export const STRIPE_CUSTOMER_SUCCESS = 'app/PaymentMethodsPage/STRIPE_CUSTOMER_SUCCESS'; +export const STRIPE_CUSTOMER_ERROR = 'app/PaymentMethodsPage/STRIPE_CUSTOMER_ERROR'; + +// ================ Reducer ================ // + +const initialState = { + setupIntentInProgress: false, + setupIntentError: null, + setupIntent: null, + stripeCustomerFetched: false, +}; + +export default function payoutMethodsPageReducer(state = initialState, action = {}) { + const { type, payload } = action; + switch (type) { + case SETUP_INTENT_REQUEST: + return { ...state, setupIntentInProgress: true, setupIntentError: null }; + case SETUP_INTENT_SUCCESS: + return { + ...state, + setupIntentInProgress: false, + setupIntentError: null, + setupIntent: payload, + }; + case SETUP_INTENT_ERROR: + console.error(payload); // eslint-disable-line no-console + return { ...state, setupIntentInProgress: false, setupIntentError: null }; + case STRIPE_CUSTOMER_REQUEST: + return { ...state, stripeCustomerFetched: false }; + case STRIPE_CUSTOMER_SUCCESS: + return { ...state, stripeCustomerFetched: true }; + case STRIPE_CUSTOMER_ERROR: + console.error(payload); // eslint-disable-line no-console + return { ...state, stripeCustomerFetchError: payload }; + default: + return state; + } +} + +// ================ Action creators ================ // + +export const setupIntentRequest = () => ({ type: SETUP_INTENT_REQUEST }); +export const setupIntentSuccess = () => ({ type: SETUP_INTENT_SUCCESS }); +export const setupIntentError = e => ({ + type: SETUP_INTENT_ERROR, + error: true, + payload: e, +}); + +export const stripeCustomerRequest = () => ({ type: STRIPE_CUSTOMER_REQUEST }); +export const stripeCustomerSuccess = () => ({ type: STRIPE_CUSTOMER_SUCCESS }); +export const stripeCustomerError = e => ({ + type: STRIPE_CUSTOMER_ERROR, + error: true, + payload: e, +}); +// ================ Thunks ================ // + +export const createStripeSetupIntent = () => (dispatch, getState, sdk) => { + dispatch(setupIntentRequest()); + return sdk.stripeSetupIntents + .create() + .then(response => { + const setupIntent = response.data.data; + dispatch(setupIntentSuccess(setupIntent)); + return setupIntent; + }) + .catch(e => { + const error = storableError(e); + log.error(error, 'create-setup-intent-failed'); + dispatch(setupIntentError(error)); + return { createStripeSetupIntentSuccess: false }; + }); +}; + +export const stripeCustomer = () => (dispatch, getState, sdk) => { + dispatch(stripeCustomerRequest()); + + return dispatch(fetchCurrentUser({ include: ['stripeCustomer.defaultPaymentMethod'] })) + .then(response => { + dispatch(stripeCustomerSuccess()); + }) + .catch(e => { + const error = storableError(e); + log.error(error, 'fetch-stripe-customer-failed'); + dispatch(stripeCustomerError(error)); + }); +}; + +export const loadData = () => (dispatch, getState, sdk) => { + dispatch(setInitialValuesForPaymentMethods()); + + return dispatch(stripeCustomer()); +}; diff --git a/src/containers/PaymentMethodsPage/PaymentMethodsPage.js b/src/containers/PaymentMethodsPage/PaymentMethodsPage.js new file mode 100644 index 00000000..04447c74 --- /dev/null +++ b/src/containers/PaymentMethodsPage/PaymentMethodsPage.js @@ -0,0 +1,306 @@ +import React, { useState } from 'react'; +import { bool, func, object } from 'prop-types'; +import { compose } from 'redux'; +import { connect } from 'react-redux'; +import { injectIntl, intlShape, FormattedMessage } from 'react-intl'; +import { ensureCurrentUser, ensureStripeCustomer, ensurePaymentMethodCard } from '../../util/data'; +import { propTypes } from '../../util/types'; +import { savePaymentMethod, deletePaymentMethod } from '../../ducks/paymentMethods.duck'; +import { handleCardSetup } from '../../ducks/stripe.duck'; +import { manageDisableScrolling, isScrollingDisabled } from '../../ducks/UI.duck'; +import { + SavedCardDetails, + LayoutSideNavigation, + LayoutWrapperMain, + LayoutWrapperSideNav, + LayoutWrapperTopbar, + LayoutWrapperFooter, + Footer, + Page, + UserNav, +} from '../../components'; +import { TopbarContainer } from '../../containers'; +import { PaymentMethodsForm } from '../../forms'; +import { createStripeSetupIntent, stripeCustomer, loadData } from './PaymentMethodsPage.duck.js'; + +import css from './PaymentMethodsPage.css'; + +const PaymentMethodsPageComponent = props => { + const [isSubmitting, setIsSubmitting] = useState(false); + const [cardState, setCardState] = useState(null); + + const { + currentUser, + addPaymentMethodError, + deletePaymentMethodError, + createStripeCustomerError, + handleCardSetupError, + deletePaymentMethodInProgress, + onCreateSetupIntent, + onHandleCardSetup, + onSavePaymentMethod, + onDeletePaymentMethod, + fetchStripeCustomer, + scrollingDisabled, + onManageDisableScrolling, + intl, + stripeCustomerFetched, + } = props; + + const getClientSecret = setupIntent => { + return setupIntent && setupIntent.attributes ? setupIntent.attributes.clientSecret : null; + }; + const getPaymentParams = (currentUser, formValues) => { + const { name, addressLine1, addressLine2, postal, state, city, country } = formValues; + const addressMaybe = + addressLine1 && postal + ? { + address: { + city: city, + country: country, + line1: addressLine1, + line2: addressLine2, + postal_code: postal, + state: state, + }, + } + : {}; + const billingDetails = { + name, + email: ensureCurrentUser(currentUser).attributes.email, + ...addressMaybe, + }; + + const paymentParams = { + payment_method_data: { + billing_details: billingDetails, + }, + }; + + return paymentParams; + }; + + const handleSubmit = params => { + setIsSubmitting(true); + const ensuredCurrentUser = ensureCurrentUser(currentUser); + const stripeCustomer = ensuredCurrentUser.stripeCustomer; + const { stripe, card, formValues } = params; + + onCreateSetupIntent() + .then(setupIntent => { + const stripeParams = { + stripe, + card, + setupIntentClientSecret: getClientSecret(setupIntent), + paymentParams: getPaymentParams(currentUser, formValues), + }; + + return onHandleCardSetup(stripeParams); + }) + .then(result => { + const newPaymentMethod = result.setupIntent.payment_method; + // Note: stripe.handleCardSetup might return an error inside successful call (200), but those are rejected in thunk functions. + + return onSavePaymentMethod(stripeCustomer, newPaymentMethod); + }) + .then(() => { + // Update currentUser entity and its sub entities: stripeCustomer and defaultPaymentMethod + fetchStripeCustomer(); + setIsSubmitting(false); + setCardState('default'); + }) + .catch(error => { + console.error(error); + setIsSubmitting(false); + }); + }; + + const handleRemovePaymentMethod = () => { + onDeletePaymentMethod().then(() => { + fetchStripeCustomer(); + }); + }; + + const tabs = [ + { + text: , + selected: false, + linkProps: { + name: 'ContactDetailsPage', + }, + }, + { + text: , + selected: false, + linkProps: { + name: 'PasswordChangePage', + }, + }, + { + text: , + selected: false, + linkProps: { + name: 'PayoutPreferencesPage', + }, + }, + { + text: , + selected: true, + linkProps: { + name: 'PaymentMethodsPage', + }, + }, + ]; + + const title = intl.formatMessage({ id: 'PaymentMethodsPage.title' }); + + const ensuredCurrentUser = ensureCurrentUser(currentUser); + const currentUserLoaded = !!ensuredCurrentUser.id; + + const hasDefaultPaymentMethod = + currentUser && + ensureStripeCustomer(currentUser.stripeCustomer).attributes.stripeCustomerId && + ensurePaymentMethodCard(currentUser.stripeCustomer.defaultPaymentMethod).id; + + // Get first and last name of the current user and use it in the StripePaymentForm to autofill the name field + const userName = currentUserLoaded + ? `${ensuredCurrentUser.attributes.profile.firstName} ${ + ensuredCurrentUser.attributes.profile.lastName + }` + : null; + + const initalValuesForStripePayment = { name: userName }; + + const card = hasDefaultPaymentMethod + ? ensurePaymentMethodCard(currentUser.stripeCustomer.defaultPaymentMethod).attributes.card + : null; + + const showForm = cardState === 'replaceCard' || !hasDefaultPaymentMethod; + const showCardDetails = !!hasDefaultPaymentMethod; + return ( + + + + + + + + +
+

+ +

+ {!stripeCustomerFetched ? null : ( + <> + {showCardDetails ? ( + + ) : null} + {showForm ? ( + + ) : null} + + )} +
+
+ +