From 3cea443b2606d31f1a3d5554ae9dca284eeb9379 Mon Sep 17 00:00:00 2001 From: Eric Dobbertin Date: Tue, 8 Jan 2019 11:19:05 -0600 Subject: [PATCH 01/10] docs: fix react-docgen warning --- .../AddressCapture/v1/AddressCapture.js | 42 +++++++++---------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/package/src/components/AddressCapture/v1/AddressCapture.js b/package/src/components/AddressCapture/v1/AddressCapture.js index daba1770d..497b1fa46 100644 --- a/package/src/components/AddressCapture/v1/AddressCapture.js +++ b/package/src/components/AddressCapture/v1/AddressCapture.js @@ -23,12 +23,12 @@ class AddressCapture extends Component { */ errors: PropTypes.arrayOf(PropTypes.shape({ /** - * Error message - */ + * Error message + */ message: PropTypes.string.isRequired, /** - * Error name - */ + * Error name + */ name: PropTypes.string.isRequired })), /** @@ -134,9 +134,8 @@ class AddressCapture extends Component { _form = null; /** - * @method hasAddressSuggestion * @summary returns true if we have a suggested address from a address validation service - * @return {Boolean} - true if address suggestion on props + * @return {Boolean} True if address suggestion on props */ get hasAddressSuggestion() { const { addressReviewProps: { addressSuggestion } } = this.props; @@ -144,9 +143,8 @@ class AddressCapture extends Component { } /** - * @method hasValidationError * @summary returns true if we have any validation errors from a address validation service - * @return {Boolean} - true if validation errors on props + * @return {Boolean} True if validation errors on props */ get hasValidationError() { const { addressReviewProps: { validationError } } = this.props; @@ -154,9 +152,8 @@ class AddressCapture extends Component { } /** - * @method addressEntered * @summary getter that returns the entered address - * @return {Object} addressEntered - Address object + * @return {Object} Address object */ get addressEntered() { const { addressReviewProps: { addressEntered } } = this.props; @@ -164,9 +161,8 @@ class AddressCapture extends Component { } /** - * @method addressSuggestion * @summary getter that returns the suggested address - * @return {Object} addressSuggestion - Address object + * @return {Object} Address object */ get addressSuggestion() { const { addressReviewProps: { addressSuggestion } } = this.props; @@ -174,9 +170,8 @@ class AddressCapture extends Component { } /** - * @method addressProvided * @summary getter that returns the provided address form value - * @return {Object} addressProvided - Address object + * @return {Object} Address object */ get addressProvided() { const { addressFormProps: { value } } = this.props; @@ -184,7 +179,6 @@ class AddressCapture extends Component { } /** - * @method inEntry * @summary getter that returns true if in entry mode * @return {Boolean} True if currently in entry status */ @@ -194,7 +188,6 @@ class AddressCapture extends Component { } /** - * @method inEdit * @summary getter that returns true if in edit mode * @return {Boolean} True if currently in edit status */ @@ -204,7 +197,6 @@ class AddressCapture extends Component { } /** - * @method inReview * @summary getter that returns true if in review mode * @return {Boolean} True if currently in review status */ @@ -214,40 +206,44 @@ class AddressCapture extends Component { } /** - * @method toggleStatus * @summary setter that toggles the Component's status. * @param {String} status The new status - * @return {undefined} */ set toggleStatus(status) { this.setState({ status }); } + /* eslint-disable valid-jsdoc */ /** * @method formRef * @summary binds the active form element to the `_form` property - * @param {Object} form - React ref element - * @return {undefined} + * @param {Object} form React ref element + * Can't include "return {undefined}" because react-docgen doesn't support it. */ + /* eslint-enable valid-jsdoc */ formRef = (form) => { this._form = form; }; + /* eslint-disable valid-jsdoc */ /** * @method submit * @summary Instance method that submits the form, this allows a parent component access to the Form submit event. - * @return {undefined} + * Can't include "return {undefined}" because react-docgen doesn't support it. */ + /* eslint-enable valid-jsdoc */ submit = () => { this._form.submit(); }; + /* eslint-disable valid-jsdoc */ /** * @method handleSubmit * @summary validate or submit the entered address object. * @param {Object} address - submitted address object - * @return {undefined} + * Can't include "return {undefined}" because react-docgen doesn't support it. */ + /* eslint-enable valid-jsdoc */ handleSubmit = async (address) => { const { onAddressValidation, onSubmit } = this.props; if (onAddressValidation && !address.isValid) { From 772e5731754b998bdf7f66401f9d9fd0cbdc1172 Mon Sep 17 00:00:00 2001 From: Eric Dobbertin Date: Tue, 8 Jan 2019 14:42:44 -0600 Subject: [PATCH 02/10] feat: new AddressChoice component --- .../AddressChoice/v1/AddressChoice.js | 133 ++++++++++++++++++ .../AddressChoice/v1/AddressChoice.md | 84 +++++++++++ .../AddressChoice/v1/AddressChoice.test.js | 101 +++++++++++++ .../__snapshots__/AddressChoice.test.js.snap | 25 ++++ .../src/components/AddressChoice/v1/index.js | 1 + .../components/AddressForm/v1/AddressForm.js | 30 ++-- package/src/tests/realComponents.js | 112 +++++++++++++++ package/src/utils/addressToString.js | 20 ++- styleguide.config.js | 1 + styleguide/src/appComponents.js | 2 + 10 files changed, 495 insertions(+), 14 deletions(-) create mode 100644 package/src/components/AddressChoice/v1/AddressChoice.js create mode 100644 package/src/components/AddressChoice/v1/AddressChoice.md create mode 100644 package/src/components/AddressChoice/v1/AddressChoice.test.js create mode 100644 package/src/components/AddressChoice/v1/__snapshots__/AddressChoice.test.js.snap create mode 100644 package/src/components/AddressChoice/v1/index.js create mode 100644 package/src/tests/realComponents.js diff --git a/package/src/components/AddressChoice/v1/AddressChoice.js b/package/src/components/AddressChoice/v1/AddressChoice.js new file mode 100644 index 000000000..dfac78f92 --- /dev/null +++ b/package/src/components/AddressChoice/v1/AddressChoice.js @@ -0,0 +1,133 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { withComponents } from "@reactioncommerce/components-context"; +import { addressToString, CustomPropTypes } from "../../../utils"; + +class AddressChoice extends Component { + static propTypes = { + /** + * A list of addresses to show for selection + */ + addresses: CustomPropTypes.addressBook, + /** + * You can provide a `className` prop that will be applied to the outermost DOM element + * rendered by this component. We do not recommend using this for styling purposes, but + * it can be useful as a selector in some situations. + */ + className: PropTypes.string, + /** + * If you've set up a components context using + * [@reactioncommerce/components-context](https://github.com/reactioncommerce/components-context) + * (recommended), then this prop will come from there automatically. If you have not + * set up a components context or you want to override one of the components in a + * single spot, you can pass in the components prop directly. + */ + components: PropTypes.shape({ + /** + * Pass either the Reaction AddressForm component or your own component that + * accepts compatible props. + */ + AddressForm: CustomPropTypes.component.isRequired, + /** + * A reaction SelectableList component or compatible component. + */ + SelectableList: CustomPropTypes.component.isRequired + }), + /** + * Disable editing? + */ + isReadOnly: PropTypes.bool, + /** + * Called with an address whenever the selected or entered + * address changes. If they selected one, it will be the + * complete address that was passed in `addresses`. If they're + * entering one, it will be whatever they have entered so far + * and may be partial. + */ + onChange: PropTypes.func, + /** + * The label for the "Use a different address" selection item, if it + * is shown. + */ + otherAddressLabel: PropTypes.string + }; + + static defaultProps = { + isReadOnly: false, + onChange() {}, + otherAddressLabel: "Use a different address" + }; + + constructor(props) { + super(props); + + let selectedOption = "OTHER"; + if (Array.isArray(props.addresses) && props.addresses.length > 0) { + selectedOption = "0"; + } + + this.state = { selectedOption }; + } + + handleChangeAddress = (address) => { + this.props.onChange(address); + } + + handleChangeSelection = (selectedOption) => { + const { addresses } = this.props; + + this.setState({ selectedOption }); + + if (selectedOption !== "OTHER" && Array.isArray(addresses)) { + this.props.onChange(addresses[Number(selectedOption)]); + } + } + + renderSelectList() { + const { + addresses, + components: { SelectableList }, + isReadOnly, + otherAddressLabel + } = this.props; + const { selectedOption } = this.state; + + if (!Array.isArray(addresses) || addresses.length === 0) return null; + + const listOptions = addresses.map((address, index) => ({ + id: String(index), + label: addressToString(address, { includeFullName: true }), + value: String(index) + })); + + listOptions.push({ + id: "OTHER", + label: otherAddressLabel, + value: "OTHER" + }); + + return ( + + ); + } + + render() { + const { className, components: { AddressForm }, isReadOnly } = this.props; + const { selectedOption } = this.state; + + return ( +
+ {this.renderSelectList()} + {selectedOption === "OTHER" && } +
+ ); + } +} + +export default withComponents(AddressChoice); diff --git a/package/src/components/AddressChoice/v1/AddressChoice.md b/package/src/components/AddressChoice/v1/AddressChoice.md new file mode 100644 index 000000000..c5bc9a58f --- /dev/null +++ b/package/src/components/AddressChoice/v1/AddressChoice.md @@ -0,0 +1,84 @@ +### Overview + +The `AddressChoice` component is a way of collecting an address while giving an option of choosing an already known address. If no already known addresses are provided, it will render an [AddressForm](./#!/AddressForm). Otherwise it will render a choice list where each address is in the list and the final option is to enter a new address. + +### Usage + +#### Simple +If you don't pass in any `addresses`, it renders an [AddressForm](./#!/AddressForm). + +```jsx + +``` + +#### Read Only +Use `isReadOnly` prop to disable editing, such as when submitting the form or loading something. + +```jsx + +``` + +#### With Addresses +When you provide one or more `addresses`, they are presented in a [SelectableList](./#!/SelectableList) along with a final option that shows the [AddressForm](./#!/AddressForm) when selected. + +`onChange` prop is called whenever the address changes, whether by selecting an existing address or changing a field in the custom address form. + +```jsx +const addresses = [ + { + _id: "20", + address1: "7742 Hwy 23", + address2: "", + country: "US", + city: "Belle Chasse", + fullName: "Salvos Seafood", + postal: "70037", + region: "LA", + phone: "(504) 393-7303" + }, + { + _id: "21", + address1: "35 Akin Adesola St", + address2: "", + country: "NG", + city: "Lagos", + fullName: "Ocean Basket Victoria Island", + postal: "101241", + region: "Victoria Island", + phone: "234 816 059 1821" + } +]; + + +``` + +#### Read Only With Addresses + +```jsx +const addresses = [ + { + _id: "20", + address1: "7742 Hwy 23", + address2: "", + country: "US", + city: "Belle Chasse", + fullName: "Salvos Seafood", + postal: "70037", + region: "LA", + phone: "(504) 393-7303" + }, + { + _id: "21", + address1: "35 Akin Adesola St", + address2: "", + country: "NG", + city: "Lagos", + fullName: "Ocean Basket Victoria Island", + postal: "101241", + region: "Victoria Island", + phone: "234 816 059 1821" + } +]; + + +``` diff --git a/package/src/components/AddressChoice/v1/AddressChoice.test.js b/package/src/components/AddressChoice/v1/AddressChoice.test.js new file mode 100644 index 000000000..9795c30c1 --- /dev/null +++ b/package/src/components/AddressChoice/v1/AddressChoice.test.js @@ -0,0 +1,101 @@ +import React from "react"; +import renderer from "react-test-renderer"; +import { mount } from "enzyme"; +import { ComponentsProvider } from "@reactioncommerce/components-context"; +import mockComponents from "../../../tests/mockComponents"; +import realComponents from "../../../tests/realComponents"; +import AddressChoice from "./AddressChoice"; + +const addresses = [ + { + _id: "20", + address1: "7742 Hwy 23", + address2: "", + country: "US", + city: "Belle Chasse", + fullName: "Salvos Seafood", + postal: "70037", + region: "LA", + phone: "(504) 393-7303" + }, + { + _id: "21", + address1: "35 Akin Adesola St", + address2: "", + country: "NG", + city: "Lagos", + fullName: "Ocean Basket Victoria Island", + postal: "101241", + region: "Victoria Island", + phone: "234 816 059 1821" + } +]; + +test("simple snapshot", () => { + const component = renderer.create(); + + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); +}); + +test("simple snapshot read only", () => { + const component = renderer.create(); + + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); +}); + +test("calls onChange when a field changes", () => { + const onChange = jest.fn(); + + const wrapper = mount(( + + + + )); + + onChange.mockClear(); + + wrapper.find('input[name="address1"]').simulate("blur", { target: { value: "FOO" } }); + + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange).toHaveBeenLastCalledWith({ + address1: "FOO", + address2: null, + addressName: "", + city: null, + country: null, + fullName: null, + isCommercial: false, + phone: null, + postal: null, + region: null + }); +}); + +test("snapshot with addresses", () => { + const component = renderer.create(); + + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); +}); + +test("calls onChange on mount and when a different provided address is selected", () => { + const onChange = jest.fn(); + + const wrapper = mount(( + + + + )); + + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange).toHaveBeenLastCalledWith(addresses[0]); + + onChange.mockClear(); + + wrapper.find('input[value="1"]').simulate("change", { target: { checked: true } }); + + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange).toHaveBeenLastCalledWith(addresses[1]); +}); diff --git a/package/src/components/AddressChoice/v1/__snapshots__/AddressChoice.test.js.snap b/package/src/components/AddressChoice/v1/__snapshots__/AddressChoice.test.js.snap new file mode 100644 index 000000000..9ef896d65 --- /dev/null +++ b/package/src/components/AddressChoice/v1/__snapshots__/AddressChoice.test.js.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`simple snapshot 1`] = ` +
+ AddressForm({"isReadOnly":false}) +
+`; + +exports[`simple snapshot read only 1`] = ` +
+ AddressForm({"isReadOnly":true}) +
+`; + +exports[`snapshot with addresses 1`] = ` +
+ SelectableList({"name":"addressList","isReadOnly":false,"options":"[Object]","value":"0"}) +
+`; diff --git a/package/src/components/AddressChoice/v1/index.js b/package/src/components/AddressChoice/v1/index.js new file mode 100644 index 000000000..9039b3949 --- /dev/null +++ b/package/src/components/AddressChoice/v1/index.js @@ -0,0 +1 @@ +export { default } from "./AddressChoice"; diff --git a/package/src/components/AddressForm/v1/AddressForm.js b/package/src/components/AddressForm/v1/AddressForm.js index e04298872..ee62120ee 100644 --- a/package/src/components/AddressForm/v1/AddressForm.js +++ b/package/src/components/AddressForm/v1/AddressForm.js @@ -99,7 +99,11 @@ class AddressForm extends Component { */ isOnDarkBackground: PropTypes.bool, /** - * Is the shipping address being saved + * Do not allow editing of the form fields + */ + isReadOnly: PropTypes.bool, + /** + * Is the address being saved */ isSaving: PropTypes.bool, /** @@ -154,6 +158,7 @@ class AddressForm extends Component { errors: [], locales: {}, isOnDarkBackground: false, + isReadOnly: false, isSaving: false, name: "address", onCancel() {}, @@ -258,6 +263,7 @@ class AddressForm extends Component { components: { Checkbox, ErrorsBlock, Field, TextInput, Select, PhoneNumberInput, RegionInput }, errors, isOnDarkBackground, + isReadOnly, isSaving, name, onChange, @@ -300,7 +306,7 @@ class AddressForm extends Component { name="addressName" placeholder={addressNamePlaceholder} isOnDarkBackground={isOnDarkBackground} - isReadOnly={isSaving} + isReadOnly={isSaving || isReadOnly} /> @@ -318,7 +324,7 @@ class AddressForm extends Component { options={this.countryOptions} placeholder="Country" isOnDarkBackground={isOnDarkBackground} - isReadOnly={isSaving} + isReadOnly={isSaving || isReadOnly} /> ) : ( )} @@ -340,7 +346,7 @@ class AddressForm extends Component { name="fullName" placeholder="Name" isOnDarkBackground={isOnDarkBackground} - isReadOnly={isSaving} + isReadOnly={isSaving || isReadOnly} /> @@ -353,7 +359,7 @@ class AddressForm extends Component { name="address1" placeholder="Address" isOnDarkBackground={isOnDarkBackground} - isReadOnly={isSaving} + isReadOnly={isSaving || isReadOnly} /> @@ -366,7 +372,7 @@ class AddressForm extends Component { name="address2" placeholder="Address Line 2 (Optional)" isOnDarkBackground={isOnDarkBackground} - isReadOnly={isSaving} + isReadOnly={isSaving || isReadOnly} /> @@ -378,7 +384,7 @@ class AddressForm extends Component { name="city" placeholder="City" isOnDarkBackground={isOnDarkBackground} - isReadOnly={isSaving} + isReadOnly={isSaving || isReadOnly} /> @@ -390,7 +396,7 @@ class AddressForm extends Component { id={regionInputId} options={this.regionOptions} isOnDarkBackground={isOnDarkBackground} - isReadOnly={isSaving} + isReadOnly={isSaving || isReadOnly} name="region" placeholder="Region" /> @@ -404,7 +410,7 @@ class AddressForm extends Component { name="postal" placeholder="Postal Code" isOnDarkBackground={isOnDarkBackground} - isReadOnly={isSaving} + isReadOnly={isSaving || isReadOnly} /> @@ -417,7 +423,7 @@ class AddressForm extends Component { name="phone" placeholder="Phone" isOnDarkBackground={isOnDarkBackground} - isReadOnly={isSaving} + isReadOnly={isSaving || isReadOnly} /> @@ -431,7 +437,7 @@ class AddressForm extends Component { name="isCommercial" label="This is a commercial address." isOnDarkBackground={isOnDarkBackground} - isReadOnly={isSaving} + isReadOnly={isSaving || isReadOnly} /> diff --git a/package/src/tests/realComponents.js b/package/src/tests/realComponents.js new file mode 100644 index 000000000..9872a33d9 --- /dev/null +++ b/package/src/tests/realComponents.js @@ -0,0 +1,112 @@ +/** + * This components context is for tests that need the real components + * available, in order to test event handling and such. + */ + +import iconClear from "../svg/iconClear"; +import iconDismiss from "../svg/iconDismiss"; +import iconError from "../svg/iconError"; +import iconValid from "../svg/iconValid"; +import iconExpand from "../svg/iconExpand"; +import iconPlus from "../svg/iconPlus"; +import iconAmericanExpress from "../svg/iconAmericanExpress"; +import iconDiscover from "../svg/iconDiscover"; +import iconLock from "../svg/iconLock"; +import iconMastercard from "../svg/iconMastercard"; +import iconVisa from "../svg/iconVisa"; +import spinner from "../svg/spinner"; +import Accordion from "../components/Accordion/v1"; +import AccordionFormList from "../components/AccordionFormList/v1"; +import Address from "../components/Address/v1"; +import AddressBook from "../components/AddressBook/v1"; +import AddressCapture from "../components/AddressCapture/v1"; +import AddressChoice from "../components/AddressChoice/v1"; +import AddressForm from "../components/AddressForm/v1"; +import AddressReview from "../components/AddressReview/v1"; +import BadgeOverlay from "../components/BadgeOverlay/v1"; +import Button from "../components/Button/v1"; +import CartItem from "../components/CartItem/v1"; +import CartItemDetail from "../components/CartItemDetail/v1"; +import CartItems from "../components/CartItems/v1"; +import CartSummary from "../components/CartSummary/v1"; +import CatalogGrid from "../components/CatalogGrid/v1"; +import CatalogGridItem from "../components/CatalogGridItem/v1"; +import Checkbox from "../components/Checkbox/v1"; +import CheckoutAction from "../components/CheckoutAction/v1"; +import CheckoutActionComplete from "../components/CheckoutActionComplete/v1"; +import CheckoutActionIncomplete from "../components/CheckoutActionIncomplete/v1"; +import ErrorsBlock from "../components/ErrorsBlock/v1"; +import Field from "../components/Field/v1"; +import InlineAlert from "../components/InlineAlert/v1"; +import InPageMenuItem from "../components/InPageMenuItem/v1"; +import InventoryStatus from "../components/InventoryStatus/v1"; +import Link from "../components/Link/v1"; +import MiniCartSummary from "../components/MiniCartSummary/v1"; +import MultiSelect from "../components/MultiSelect/v1"; +import PhoneNumberInput from "../components/PhoneNumberInput/v1"; +import Price from "../components/Price/v1"; +import ProfileImage from "../components/ProfileImage/v1"; +import ProgressiveImage from "../components/ProgressiveImage/v1"; +import QuantityInput from "../components/QuantityInput/v1"; +import RegionInput from "../components/RegionInput/v1"; +import Select from "../components/Select/v1"; +import StockWarning from "../components/StockWarning/v1"; +import StripeForm from "../components/StripeForm/v1"; +import SelectableItem from "../components/SelectableItem/v1"; +import SelectableList from "../components/SelectableList/v1"; +import TextInput from "../components/TextInput/v1"; + +export default { + Accordion, + AccordionFormList, + Address, + AddressBook, + AddressCapture, + AddressChoice, + AddressForm, + AddressReview, + BadgeOverlay, + Button, + CartItem, + CartItemDetail, + CartItems, + CartSummary, + CatalogGrid, + CatalogGridItem, + Checkbox, + CheckoutAction, + CheckoutActionComplete, + CheckoutActionIncomplete, + ErrorsBlock, + Field, + iconClear, + iconDismiss, + iconError, + iconExpand, + iconPlus, + iconValid, + iconAmericanExpress, + iconDiscover, + iconLock, + iconMastercard, + iconVisa, + InlineAlert, + InPageMenuItem, + InventoryStatus, + Link, + MiniCartSummary, + MultiSelect, + PhoneNumberInput, + Price, + ProfileImage, + ProgressiveImage, + QuantityInput, + RegionInput, + Select, + spinner, + StockWarning, + StripeForm, + SelectableItem, + SelectableList, + TextInput +}; diff --git a/package/src/utils/addressToString.js b/package/src/utils/addressToString.js index b80448efe..7e27cf55c 100644 --- a/package/src/utils/addressToString.js +++ b/package/src/utils/addressToString.js @@ -3,8 +3,24 @@ * @method addressToString * @summary Converts an `address` object to a string * @param {Object} address - Address object to be converted + * @param {Object} [options] - Options that affect the resulting string + * @param {Boolean} [options.includeFullName] - If true, the string will begin with address.fullName. * @return {String} - address as a flat string */ -export default function addressToString({ address1, address2, city, country, postal, region }) { - return `${address1}${address2 ? `, ${address2}` : ""}, ${city}, ${region} ${postal} ${country}`; +export default function addressToString({ + address1, + address2, + city, + country, + fullName, + postal, + region +}, options = {}) { + const result = `${address1}${address2 ? `, ${address2}` : ""}, ${city}, ${region} ${postal} ${country}`; + + if (options.includeFullName && fullName) { + return `${fullName}, ${result}`; + } + + return result; } diff --git a/styleguide.config.js b/styleguide.config.js index 3cdcf46e7..f5716113d 100644 --- a/styleguide.config.js +++ b/styleguide.config.js @@ -497,6 +497,7 @@ module.exports = { generateSection({ componentNames: [ "AddressCapture", + "AddressChoice", "AddressForm", "AddressReview", "GuestForm", diff --git a/styleguide/src/appComponents.js b/styleguide/src/appComponents.js index e1b52d247..b8237710f 100644 --- a/styleguide/src/appComponents.js +++ b/styleguide/src/appComponents.js @@ -15,6 +15,7 @@ import AccordionFormList from "../../package/src/components/AccordionFormList/v1 import Address from "../../package/src/components/Address/v1"; import AddressBook from "../../package/src/components/AddressBook/v1"; import AddressCapture from "../../package/src/components/AddressCapture/v1"; +import AddressChoice from "../../package/src/components/AddressChoice/v1"; import AddressForm from "../../package/src/components/AddressForm/v1"; import AddressReview from "../../package/src/components/AddressReview/v1"; import BadgeOverlay from "../../package/src/components/BadgeOverlay/v1"; @@ -62,6 +63,7 @@ export default { Address, AddressBook, AddressCapture, + AddressChoice, AddressForm: AddressFormWithLocales, AddressReview, BadgeOverlay, From 0473b4805716395c2baa17c2ec89bb82bcb27bd0 Mon Sep 17 00:00:00 2001 From: Eric Dobbertin Date: Tue, 8 Jan 2019 19:47:04 -0600 Subject: [PATCH 03/10] feat: new ExampleIOUPaymentForm component --- .../v1/ExampleIOUPaymentForm.js | 148 ++++++++++++++++++ .../v1/ExampleIOUPaymentForm.md | 34 ++++ .../v1/ExampleIOUPaymentForm.test.js | 99 ++++++++++++ .../ExampleIOUPaymentForm.test.js.snap | 11 ++ .../ExampleIOUPaymentForm/v1/index.js | 1 + styleguide.config.js | 1 + 6 files changed, 294 insertions(+) create mode 100644 package/src/components/ExampleIOUPaymentForm/v1/ExampleIOUPaymentForm.js create mode 100644 package/src/components/ExampleIOUPaymentForm/v1/ExampleIOUPaymentForm.md create mode 100644 package/src/components/ExampleIOUPaymentForm/v1/ExampleIOUPaymentForm.test.js create mode 100644 package/src/components/ExampleIOUPaymentForm/v1/__snapshots__/ExampleIOUPaymentForm.test.js.snap create mode 100644 package/src/components/ExampleIOUPaymentForm/v1/index.js diff --git a/package/src/components/ExampleIOUPaymentForm/v1/ExampleIOUPaymentForm.js b/package/src/components/ExampleIOUPaymentForm/v1/ExampleIOUPaymentForm.js new file mode 100644 index 000000000..7218a2c5e --- /dev/null +++ b/package/src/components/ExampleIOUPaymentForm/v1/ExampleIOUPaymentForm.js @@ -0,0 +1,148 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { Form } from "reacto-form"; +import { uniqueId } from "lodash"; +import { withComponents } from "@reactioncommerce/components-context"; +import { CustomPropTypes } from "../../../utils"; + +/** + * Convert the form document to the object structure + * expected by `PaymentsCheckoutAction` + * @param {Object} Form object + * @returns {Object} Transformed object + */ +function buildResult({ amount, fullName }) { + let floatAmount = amount ? parseFloat(amount) : null; + if (isNaN(floatAmount)) floatAmount = null; + + return { + amount: floatAmount, + data: { fullName }, + displayName: fullName ? `IOU from ${fullName}` : null + }; +} + +class ExampleIOUPaymentForm extends Component { + static propTypes = { + /** + * You can provide a `className` prop that will be applied to the outermost DOM element + * rendered by this component. We do not recommend using this for styling purposes, but + * it can be useful as a selector in some situations. + */ + className: PropTypes.string, + /** + * If you've set up a components context using + * [@reactioncommerce/components-context](https://github.com/reactioncommerce/components-context) + * (recommended), then this prop will come from there automatically. If you have not + * set up a components context or you want to override one of the components in a + * single spot, you can pass in the components prop directly. + */ + components: PropTypes.shape({ + /** + * Pass either the Reaction ErrorsBlock component or your own component that + * accepts compatible props. + */ + ErrorsBlock: CustomPropTypes.component.isRequired, + /** + * Pass either the Reaction Field component or your own component that + * accepts compatible props. + */ + Field: CustomPropTypes.component.isRequired, + /** + * Pass either the Reaction TextInput component or your own component that + * accepts compatible props. + */ + TextInput: CustomPropTypes.component.isRequired + }), + /** + * Is the payment input being saved? + */ + isSaving: PropTypes.bool, + /** + * Called as the form fields are changed + */ + onChange: PropTypes.func, + /** + * When this action's input data switches between being + * ready for saving and not ready for saving, this will + * be called with `true` (ready) or `false` + */ + onReadyForSaveChange: PropTypes.func, + /** + * Called with an object value when this component's `submit` + * method is called. The object may have `data`, `displayName`, + * and `amount` properties. + */ + onSubmit: PropTypes.func + } + + static defaultProps = { + onChange() {}, + onReadyForSaveChange() {}, + onSubmit() {} + }; + + uniqueInstanceIdentifier = uniqueId("ExampleIOUPaymentForm"); + + submit() { + if (this.form) this.form.submit(); + } + + handleChange = (doc) => { + const { onChange, onReadyForSaveChange } = this.props; + + const resultDoc = buildResult(doc); + const stringDoc = JSON.stringify(resultDoc); + if (stringDoc !== this.lastDoc) { + onChange(resultDoc); + } + this.lastDoc = stringDoc; + + const isReady = !!doc.fullName; + if (isReady !== this.lastIsReady) { + onReadyForSaveChange(isReady); + } + this.lastIsReady = isReady; + } + + handleSubmit = (doc) => { + const { onSubmit } = this.props; + return onSubmit(buildResult(doc)); + } + + render() { + const { + className, + components: { + ErrorsBlock, + Field, + TextInput + }, + isSaving + } = this.props; + + const fullNameInputId = `fullName_${this.uniqueInstanceIdentifier}`; + const amountInputId = `amount_${this.uniqueInstanceIdentifier}`; + + return ( +
{ this.form = formRef; }} + > + + + + + + + + +
+ ); + } +} + +export default withComponents(ExampleIOUPaymentForm); diff --git a/package/src/components/ExampleIOUPaymentForm/v1/ExampleIOUPaymentForm.md b/package/src/components/ExampleIOUPaymentForm/v1/ExampleIOUPaymentForm.md new file mode 100644 index 000000000..da6af1346 --- /dev/null +++ b/package/src/components/ExampleIOUPaymentForm/v1/ExampleIOUPaymentForm.md @@ -0,0 +1,34 @@ +### Overview + +The `ExampleIOUPaymentForm` component is intended to be used as the `InputComponent` for the `iou_example` payment method in a Reaction client UI. Provide it in the `paymentMethods` array passed to the [PaymentsCheckoutAction](./#!/PaymentsCheckoutAction) component. + +### Usage + +```jsx +class Example extends React.Component { + constructor(props) { + super(props); + + this.state = { isReady: false }; + } + + render() { + return ( +
+ { this.form = ref; }} + onChange={(...args) => { console.log("onChange", ...args); }} + onReadyForSaveChange={(isReady) => { + console.log("onReadyForSaveChange", isReady); + this.setState({ isReady }); + }} + onSubmit={(doc) => { console.log("onSubmit", doc); }} + /> + +
+ ); + } +} + + +``` diff --git a/package/src/components/ExampleIOUPaymentForm/v1/ExampleIOUPaymentForm.test.js b/package/src/components/ExampleIOUPaymentForm/v1/ExampleIOUPaymentForm.test.js new file mode 100644 index 000000000..90a52b309 --- /dev/null +++ b/package/src/components/ExampleIOUPaymentForm/v1/ExampleIOUPaymentForm.test.js @@ -0,0 +1,99 @@ +import React from "react"; +import renderer from "react-test-renderer"; +import { mount } from "enzyme"; +import { ComponentsProvider } from "@reactioncommerce/components-context"; +import mockComponents from "../../../tests/mockComponents"; +import realComponents from "../../../tests/realComponents"; +import ExampleIOUPaymentForm from "./ExampleIOUPaymentForm"; + +test("basic snapshot", () => { + const component = renderer.create(); + + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); +}); + +test("calls onReadyForSaveChange on mount and change", () => { + const onReadyForSaveChange = jest.fn(); + + const wrapper = mount(( + + + + )); + + expect(onReadyForSaveChange).toHaveBeenCalledTimes(1); + expect(onReadyForSaveChange).toHaveBeenLastCalledWith(false); + + onReadyForSaveChange.mockClear(); + + wrapper.find('input[name="fullName"]').simulate("blur", { target: { value: "Bill" } }); + + expect(onReadyForSaveChange).toHaveBeenCalledTimes(1); + expect(onReadyForSaveChange).toHaveBeenLastCalledWith(true); +}); + +test("calls onChange on mount and change", () => { + const onChange = jest.fn(); + + const wrapper = mount(( + + + + )); + + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange).toHaveBeenLastCalledWith({ + amount: null, + data: { fullName: null }, + displayName: null + }); + + onChange.mockClear(); + + wrapper.find('input[name="fullName"]').simulate("blur", { target: { value: "Bill" } }); + + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange).toHaveBeenLastCalledWith({ + amount: null, + data: { fullName: "Bill" }, + displayName: "IOU from Bill" + }); + + onChange.mockClear(); + + wrapper.find('input[name="amount"]').simulate("blur", { target: { value: "5.95" } }); + + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange).toHaveBeenLastCalledWith({ + amount: 5.95, + data: { fullName: "Bill" }, + displayName: "IOU from Bill" + }); +}); + +test("calls onSubmit when submit method is called", (done) => { + const onSubmit = jest.fn(); + + let formEl; + const wrapper = mount(( + + { formEl = ref; }} onSubmit={onSubmit} /> + + )); + + expect(onSubmit).not.toHaveBeenCalled(); + + wrapper.find('input[name="fullName"]').simulate("blur", { target: { value: "Bill" } }); + formEl.submit(); + + setTimeout(() => { + expect(onSubmit).toHaveBeenCalledTimes(1); + expect(onSubmit).toHaveBeenLastCalledWith({ + amount: null, + data: { fullName: "Bill" }, + displayName: "IOU from Bill" + }); + done(); + }, 0); +}); diff --git a/package/src/components/ExampleIOUPaymentForm/v1/__snapshots__/ExampleIOUPaymentForm.test.js.snap b/package/src/components/ExampleIOUPaymentForm/v1/__snapshots__/ExampleIOUPaymentForm.test.js.snap new file mode 100644 index 000000000..45ae90914 --- /dev/null +++ b/package/src/components/ExampleIOUPaymentForm/v1/__snapshots__/ExampleIOUPaymentForm.test.js.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`basic snapshot 1`] = ` +
+ Field({"name":"fullName","label":"Full name","labelFor":"fullName_ExampleIOUPaymentForm1","children":"[Object]"}) + Field({"name":"amount","label":"Amount (optional)","labelFor":"amount_ExampleIOUPaymentForm1","children":"[Object]"}) +
+`; diff --git a/package/src/components/ExampleIOUPaymentForm/v1/index.js b/package/src/components/ExampleIOUPaymentForm/v1/index.js new file mode 100644 index 000000000..0f180e986 --- /dev/null +++ b/package/src/components/ExampleIOUPaymentForm/v1/index.js @@ -0,0 +1 @@ +export { default } from "./ExampleIOUPaymentForm"; diff --git a/styleguide.config.js b/styleguide.config.js index f5716113d..cb85edcdd 100644 --- a/styleguide.config.js +++ b/styleguide.config.js @@ -500,6 +500,7 @@ module.exports = { "AddressChoice", "AddressForm", "AddressReview", + "ExampleIOUPaymentForm", "GuestForm", "StripeForm" ], From a0105f536e2a1e94be21ea908b4c17bf0dc9b82e Mon Sep 17 00:00:00 2001 From: Eric Dobbertin Date: Tue, 8 Jan 2019 20:25:40 -0600 Subject: [PATCH 04/10] feat: new StripePaymentInput component --- .../v1/StripePaymentInput.js | 113 ++++++++++++++++++ .../v1/StripePaymentInput.md | 42 +++++++ .../v1/StripePaymentInput.test.js | 95 +++++++++++++++ .../StripePaymentInput.test.js.snap | 47 ++++++++ .../components/StripePaymentInput/v1/index.js | 1 + styleguide.config.js | 7 +- 6 files changed, 302 insertions(+), 3 deletions(-) create mode 100644 package/src/components/StripePaymentInput/v1/StripePaymentInput.js create mode 100644 package/src/components/StripePaymentInput/v1/StripePaymentInput.md create mode 100644 package/src/components/StripePaymentInput/v1/StripePaymentInput.test.js create mode 100644 package/src/components/StripePaymentInput/v1/__snapshots__/StripePaymentInput.test.js.snap create mode 100644 package/src/components/StripePaymentInput/v1/index.js diff --git a/package/src/components/StripePaymentInput/v1/StripePaymentInput.js b/package/src/components/StripePaymentInput/v1/StripePaymentInput.js new file mode 100644 index 000000000..91bc7f9b9 --- /dev/null +++ b/package/src/components/StripePaymentInput/v1/StripePaymentInput.js @@ -0,0 +1,113 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { withComponents } from "@reactioncommerce/components-context"; +import styled from "styled-components"; +import { addTypographyStyles, CustomPropTypes } from "../../../utils"; + +const SecureCaption = styled.div` + ${addTypographyStyles("StripePaymentInputCaption", "captionText")} +`; + +const IconLockSpan = styled.span` + display: inline-block; + height: 20px; + width: 20px; +`; + +const Span = styled.span` + vertical-align: super; +`; + +class StripePaymentInput extends Component { + static propTypes = { + /** + * You can provide a `className` prop that will be applied to the outermost DOM element + * rendered by this component. We do not recommend using this for styling purposes, but + * it can be useful as a selector in some situations. + */ + className: PropTypes.string, + /** + * If you've set up a components context using + * [@reactioncommerce/components-context](https://github.com/reactioncommerce/components-context) + * (recommended), then this prop will come from there automatically. If you have not + * set up a components context or you want to override one of the components in a + * single spot, you can pass in the components prop directly. + */ + components: PropTypes.shape({ + /** + * Secured lock icon + */ + iconLock: PropTypes.node, + /** + * Pass either the Reaction StripeForm component or your own component that + * accepts compatible props. + */ + StripeForm: CustomPropTypes.component.isRequired + }), + /** + * Is the payment input being saved? + */ + isSaving: PropTypes.bool, + /** + * When this action's input data switches between being + * ready for saving and not ready for saving, this will + * be called with `true` (ready) or `false` + */ + onReadyForSaveChange: PropTypes.func, + /** + * Called with an object value when this component's `submit` + * method is called. The object may have `data`, `displayName`, + * and `amount` properties. + */ + onSubmit: PropTypes.func + }; + + static defaultProps = { + onReadyForSaveChange() {}, + onSubmit() {} + }; + + componentDidMount() { + const { onReadyForSaveChange } = this.props; + onReadyForSaveChange(false); + } + + async submit() { + const { onSubmit } = this.props; + const { token } = await this._stripe.createToken(); + + await onSubmit({ + displayName: `${token.card.brand} ending in ${token.card.last4}`, + data: { + stripeTokenId: token.id + } + }); + } + + handleChangeReadyState = (isReady) => { + const { onReadyForSaveChange } = this.props; + + if (isReady !== this.lastIsReady) { + onReadyForSaveChange(isReady); + } + this.lastIsReady = isReady; + } + + render() { + const { className, components: { iconLock, StripeForm } } = this.props; + + return ( +
+ { this._stripe = stripe; }} + /> + + {iconLock} Your Information is private and secure. + +
+ ); + } +} + +export default withComponents(StripePaymentInput); diff --git a/package/src/components/StripePaymentInput/v1/StripePaymentInput.md b/package/src/components/StripePaymentInput/v1/StripePaymentInput.md new file mode 100644 index 000000000..0d889a1d5 --- /dev/null +++ b/package/src/components/StripePaymentInput/v1/StripePaymentInput.md @@ -0,0 +1,42 @@ +### Overview + +The `StripePaymentInput` component is intended to be used as the `InputComponent` for the `stripe_card` payment method in a Reaction client UI. Provide it in the `paymentMethods` array passed to the [PaymentsCheckoutAction](./#!/PaymentsCheckoutAction) component. + +### Usage + +```jsx +class Example extends React.Component { + constructor(props) { + super(props); + + this.state = { isReady: false }; + } + + render() { + return ( +
+ { this.form = ref; }} + onChange={(...args) => { console.log("onChange", ...args); }} + onReadyForSaveChange={(isReady) => { + console.log("onReadyForSaveChange", isReady); + this.setState({ isReady }); + }} + onSubmit={(doc) => { console.log("onSubmit", doc); }} + /> + +
+ ); + } +} + + +``` + +### Theme + +Assume that any theme prop that does not begin with "rui" is within `rui_components`. See [Theming Components](./#!/Theming%20Components). + +#### Typography + +- The "Your Information is private and secure" text uses `captionText` style with `rui_components.StripePaymentInputCaption` override diff --git a/package/src/components/StripePaymentInput/v1/StripePaymentInput.test.js b/package/src/components/StripePaymentInput/v1/StripePaymentInput.test.js new file mode 100644 index 000000000..767c04bd8 --- /dev/null +++ b/package/src/components/StripePaymentInput/v1/StripePaymentInput.test.js @@ -0,0 +1,95 @@ +import React from "react"; +import renderer from "react-test-renderer"; +import { mount } from "enzyme"; +import { StripeProvider } from "react-stripe-elements"; +import { ComponentsProvider } from "@reactioncommerce/components-context"; +import mockComponents from "../../../tests/mockComponents"; +import realComponents from "../../../tests/realComponents"; +import StripePaymentInput from "./StripePaymentInput"; + +// Mock the Stripe instance +const elementMock = { + mount: jest.fn(), + destroy: jest.fn(), + on: jest.fn(), + update: jest.fn() +}; +const elementsMock = { + create: jest.fn().mockReturnValue(elementMock) +}; +const stripeMock = { + elements: jest.fn().mockReturnValue(elementsMock), + createToken: jest.fn(), + createSource: jest.fn() +}; + +test("basic snapshot", () => { + const component = renderer.create(); + + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); +}); + +test("calls onReadyForSaveChange on mount and change", () => { + const onReadyForSaveChange = jest.fn(); + + let formEl; + mount(( + + + { formEl = ref; }} + onReadyForSaveChange={onReadyForSaveChange} + /> + + + )); + + expect(onReadyForSaveChange).toHaveBeenCalledTimes(1); + expect(onReadyForSaveChange).toHaveBeenLastCalledWith(false); + + onReadyForSaveChange.mockClear(); + + formEl.handleChangeReadyState(true); + + expect(onReadyForSaveChange).toHaveBeenCalledTimes(1); + expect(onReadyForSaveChange).toHaveBeenLastCalledWith(true); +}); + +test("calls onSubmit when submit method is called", (done) => { + const onSubmit = jest.fn(); + + stripeMock.createToken.mockReturnValueOnce(Promise.resolve({ + token: { + card: { + brand: "Visa", + last4: "1234" + }, + id: "abc123" + } + })); + + let formEl; + mount(( + + + { formEl = ref; }} onSubmit={onSubmit} /> + + + )); + + expect(onSubmit).not.toHaveBeenCalled(); + + formEl.submit(); + + setTimeout(() => { + expect(onSubmit).toHaveBeenCalledTimes(1); + expect(onSubmit).toHaveBeenLastCalledWith({ + data: { + stripeTokenId: "abc123" + }, + displayName: "Visa ending in 1234" + }); + done(); + }, 0); +}); diff --git a/package/src/components/StripePaymentInput/v1/__snapshots__/StripePaymentInput.test.js.snap b/package/src/components/StripePaymentInput/v1/__snapshots__/StripePaymentInput.test.js.snap new file mode 100644 index 000000000..19333242e --- /dev/null +++ b/package/src/components/StripePaymentInput/v1/__snapshots__/StripePaymentInput.test.js.snap @@ -0,0 +1,47 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`basic snapshot 1`] = ` +.c0 { + -webkit-font-smoothing: antialiased; + color: #b3b3b3; + font-family: 'Source Sans Pro','Helvetica Neue',Helvetica,sans-serif; + font-size: 14px; + font-style: normal; + font-stretch: normal; + font-weight: 400; + -webkit-letter-spacing: .02em; + -moz-letter-spacing: .02em; + -ms-letter-spacing: .02em; + letter-spacing: .02em; + line-height: 1.25; +} + +.c1 { + display: inline-block; + height: 20px; + width: 20px; +} + +.c2 { + vertical-align: super; +} + +
+ StripeForm({}) +
+ + + + Your Information is private and secure. + +
+
+`; diff --git a/package/src/components/StripePaymentInput/v1/index.js b/package/src/components/StripePaymentInput/v1/index.js new file mode 100644 index 000000000..0ab587dfc --- /dev/null +++ b/package/src/components/StripePaymentInput/v1/index.js @@ -0,0 +1 @@ +export { default } from "./StripePaymentInput"; diff --git a/styleguide.config.js b/styleguide.config.js index cb85edcdd..432ad2359 100644 --- a/styleguide.config.js +++ b/styleguide.config.js @@ -481,15 +481,17 @@ module.exports = { generateSection({ componentNames: [ "CheckoutAction", - "CheckoutActions", "CheckoutActionComplete", "CheckoutActionIncomplete", + "CheckoutActions", "CheckoutEmailAddress", "CheckoutTopHat", + "ExampleIOUPaymentForm", "FinalReviewCheckoutAction", + "FulfillmentOptionsCheckoutAction", "ShippingAddressCheckoutAction", "StripePaymentCheckoutAction", - "FulfillmentOptionsCheckoutAction" + "StripePaymentInput" ], content: "styleguide/src/sections/Checkout.md", name: "Checkout" @@ -500,7 +502,6 @@ module.exports = { "AddressChoice", "AddressForm", "AddressReview", - "ExampleIOUPaymentForm", "GuestForm", "StripeForm" ], From 2cd8c7ea67d7f063bec9dbe966d57562a33ade68 Mon Sep 17 00:00:00 2001 From: Eric Dobbertin Date: Wed, 9 Jan 2019 06:32:45 -0600 Subject: [PATCH 05/10] feat: add formatMoney util function --- package/package.json | 1 + package/src/utils/currencyDefinitions.js | 508 +++++++++++++++++++++++ package/src/utils/formatMoney.js | 35 ++ package/src/utils/index.js | 1 + package/yarn.lock | 8 + 5 files changed, 553 insertions(+) create mode 100644 package/src/utils/currencyDefinitions.js create mode 100644 package/src/utils/formatMoney.js diff --git a/package/package.json b/package/package.json index d1668137a..3b531ed5e 100644 --- a/package/package.json +++ b/package/package.json @@ -29,6 +29,7 @@ }, "dependencies": { "@material-ui/core": "^3.1.0", + "accounting-js": "^1.1.1", "lodash.debounce": "^4.0.8", "lodash.get": "^4.4.2", "lodash.isempty": "^4.4.0", diff --git a/package/src/utils/currencyDefinitions.js b/package/src/utils/currencyDefinitions.js new file mode 100644 index 000000000..307aaf296 --- /dev/null +++ b/package/src/utils/currencyDefinitions.js @@ -0,0 +1,508 @@ +export default { + AED: { + enabled: false, + format: "%v %s", + symbol: "د.إ." + }, + AFN: { + enabled: false, + format: "%v%s", + symbol: "؋" + }, + ALL: { + enabled: false, + format: "%s%v", + symbol: "Lek", + decimal: ",", + thousand: "." + }, + AMD: { + enabled: false, + format: "%v %s", + symbol: "AMD", + decimal: ",", + thousand: "." + }, + ANG: { + enabled: false, + format: "%s%v", + symbol: "ƒ", + decimal: ",", + thousand: "." + }, + AOA: { + enabled: false, + format: "%s%v", + symbol: "Kz" + }, + ARS: { + enabled: false, + format: "%s%v", + symbol: "$", + decimal: ",", + thousand: "." + }, + AUD: { + enabled: false, + format: "%s%v", + symbol: "$" + }, + AWG: { + enabled: false, + format: "%s%v", + symbol: "ƒ" + }, + AZN: { + enabled: false, + format: "%s%v", + symbol: "₼", + decimal: ",", + thousand: "." + }, + BAM: { + enabled: false, + format: "%s%v", + symbol: "KM", + decimal: ",", + thousand: "." + }, + BBD: { + enabled: false, + format: "%s%v", + symbol: "Bds$" + }, + BDT: { + enabled: false, + format: "%s%v", + symbol: "৳" + }, + BGN: { + enabled: false, + format: "%v %s", + symbol: "лв.", + decimal: ".", + thousand: "'" + }, + BHD: { + enabled: false, + format: "%v %s", + symbol: ".د.ب", + scale: 3 + }, + BIF: { + enabled: false, + format: "%s%v", + symbol: "$", + scale: 0 + }, + BMD: { + enabled: false, + format: "%v %s", + symbol: "FBu" + }, + BND: { + enabled: false, + format: "%s%v", + symbol: "B$" + }, + BOB: { + enabled: false, + format: "%s%v", + symbol: "$b" + }, + BRL: { + enabled: false, + format: "%s%v", + symbol: "R$", + decimal: ",", + thousand: "." + }, + BSD: { + enabled: false, + format: "%s%v", + symbol: "$" + }, + BTN: { + enabled: false, + format: "%s%v", + symbol: "Nu." + }, + BWP: { + enabled: false, + format: "%s%v", + symbol: "P" + }, + BYR: { + enabled: false, + format: "%s%v", + symbol: "p.", + decimal: ",", + thousand: ".", + scale: 0 + }, + BZD: { + enabled: false, + format: "%s%v", + symbol: "BZ$" + }, + CAD: { + enabled: false, + format: "%s%v", + symbol: "$" + }, + CDF: { + enabled: false, + format: "%v %s", + symbol: "CDF" + }, + CHF: { + enabled: false, + format: "%s%v", + symbol: "CHF" + }, + CLP: { + enabled: false, + format: "%s%v", + symbol: "$", + decimal: ",", + thousand: ".", + scale: 0 + }, + CNY: { + enabled: true, + format: "%s%v", + symbol: "¥" + }, + COP: { + enabled: false, + format: "%s%v", + symbol: "$", + decimal: ",", + thousand: "." + }, + CRC: { + enabled: false, + format: "%s%v", + symbol: "₡", + decimal: ",", + thousand: "." + }, + CUC: { + enabled: false, + format: "%s%v", + symbol: "CUC$", + decimal: ",", + thousand: "." + }, + CUP: { + enabled: false, + format: "%s%v", + symbol: "₱", + decimal: ",", + thousand: "." + }, + CVE: { + enabled: false, + format: "%s%v", + symbol: "$" + }, + CZK: { + enabled: false, + format: "%v%s", + symbol: "Kč", + decimal: ",", + thousand: "." + }, + DJF: { + enabled: false, + format: "%v %s", + symbol: "Fdj", + scale: 0 + }, + DKK: { + enabled: false, + format: "%s%v", + symbol: "kr" + }, + DOP: { + enabled: false, + format: "%s%v", + symbol: "RD$", + decimal: ",", + thousand: "." + }, + DZD: { + enabled: true, + format: "%v %s", + symbol: "دج" + }, + EGP: { + enabled: true, + format: "%s%v", + symbol: "£", + decimal: ",", + thousand: "." + }, + INR: { + enabled: false, + format: "%s%v", + symbol: "₹" + }, + NOK: { + enabled: false, + format: "%s%v", + symbol: "kr" + }, + USD: { + enabled: true, + format: "%s%v", + symbol: "$" + }, + EUR: { + enabled: true, + format: "%v %s", + symbol: "€", + decimal: ",", + thousand: "." + }, + ERN: { + enabled: false, + format: "%v %s", + symbol: "ናቕፋ", + decimal: ",", + thousand: "." + }, + ETB: { + enabled: false, + format: "%s%v", + symbol: "Br" + }, + FJD: { + enabled: false, + format: "%s%v", + symbol: "$" + }, + FKP: { + enabled: false, + format: "%s%v", + symbol: "£" + }, + GBP: { + enabled: true, + format: "%s%v", + symbol: "£" + }, + GEL: { + enabled: false, + format: "%v %s", + symbol: "GEL" + }, + GHS: { + enabled: false, + format: "%s%v", + symbol: "GH¢" + }, + GIP: { + enabled: false, + format: "%s%v", + symbol: "£" + }, + GNF: { + enabled: false, + format: "%v %s", + symbol: "FG", + scale: 0 + }, + GTQ: { + enabled: false, + format: "%s%v", + symbol: "Q" + }, + GYD: { + enabled: false, + format: "%s%v", + symbol: "$" + }, + HKD: { + enabled: false, + format: "%s%v", + symbol: "HK$", + decimal: "," + }, + HRK: { + enabled: false, + format: "%s%v", + symbol: "kn" + }, + HUF: { + enabled: false, + format: "%s%v", + symbol: "Ft" + }, + IDR: { + enabled: false, + format: "%s%v", + symbol: "Rp", + decimal: ",", + thousand: "." + }, + ILS: { + enabled: true, + format: "%s%v", + symbol: "₪" + }, + ISK: { + enabled: false, + format: "%s%v", + symbol: "kr", + scale: 0 + }, + JPY: { + enabled: false, + format: "%s%v", + symbol: "¥", + scale: 0 + }, + KWD: { + enabled: false, + format: "%s%v", + symbol: "ك" + }, + KRW: { + enabled: false, + format: "%s%v", + symbol: "₩", + scale: 0 + }, + KZT: { + enabled: false, + format: "%v %s", + symbol: "KZT" + }, + MAD: { + enabled: false, + format: "%v %s", + symbol: "د.م.", + decimal: ",", + thousand: "." + }, + MMK: { + enabled: false, + format: "%s%v", + symbol: "K" + }, + MRO: { + enabled: false, + format: "%v %s", + symbol: "UM" + }, + MXN: { + enabled: false, + format: "%s%v", + symbol: "$" + }, + MYR: { + enabled: false, + format: "%s%v", + symbol: "RM" + }, + NZD: { + enabled: false, + format: "%s%v", + symbol: "$" + }, + NGN: { + enabled: true, + format: "%s%v", + symbol: "₦" + }, + PHP: { + enabled: false, + format: "%s %v", + symbol: "PHP" + }, + PLN: { + enabled: false, + format: "%v %s", + symbol: "zł", + decimal: ",", + thousand: " " + }, + QAR: { + enabled: false, + format: "%s%v", + symbol: "﷼" + }, + RUB: { + enabled: true, + format: "%v %s", + symbol: "руб.", + decimal: ",", + thousand: " ", + scale: 0, + where: "right" + }, + SAR: { + enabled: false, + format: "%s%v", + symbol: "﷼" + }, + SEK: { + enabled: false, + format: "%s%v", + symbol: "kr" + }, + SGD: { + enabled: false, + format: "%s%v", + symbol: "$" + }, + THB: { + enabled: false, + format: "%s%v", + symbol: "฿" + }, + TND: { + enabled: false, + format: "%v %s", + symbol: "DT", + decimal: "," + }, + TWD: { + enabled: false, + format: "%s%v", + symbol: "NT$" + }, + UAH: { + enabled: false, + format: "%s%v", + symbol: "₴", + decimal: "," + }, + VND: { + enabled: false, + format: "%v %s", + symbol: "₫", + decimal: ",", + thousand: ".", + scale: -2, + where: "right" + }, + XAF: { + enabled: false, + format: "%v %s", + symbol: "CFA", + scale: 0 + }, + XCD: { + enabled: false, + format: "%s%v", + symbol: "$" + }, + XOF: { + enabled: false, + format: "%v %s", + symbol: "CFA", + scale: 0 + } +}; diff --git a/package/src/utils/formatMoney.js b/package/src/utils/formatMoney.js new file mode 100644 index 000000000..4e536eae6 --- /dev/null +++ b/package/src/utils/formatMoney.js @@ -0,0 +1,35 @@ +import accounting from "accounting-js"; +import currencyDefinitions from "./currencyDefinitions"; + +/** + * A wrapper around accounting.formatMoney that handles minor differences between Reaction + * API and accounting.js API. + * @param {Number} price - A price (float) + * @param {String} [currencyCode] A currency code, case insensitive. Defaults to "USD". + * @returns {String} Formatted currency string such as "$15.99". If a matching currency is not provided, + * returns `accounting.toFixed(price, 2)`. + */ +export default function formatMoney(price, currencyCode = "USD") { + const currencyInfo = currencyDefinitions[currencyCode.toUpperCase()]; + + // Implementation of toFixed() that treats floats more like decimal values than binary, + // fixing inconsistent precision rounding in JavaScript (where some .05 values round up, + // while others round down): + if (!currencyInfo) return accounting.toFixed(price, 2); + + // If there are no decimal places, in the case of the Japanese Yen, we adjust it here. + let priceToFormat = price; + if (currencyInfo.scale === 0) { + priceToFormat = price * 100; + } + + const currencyFormatSettings = { ...currencyInfo }; + + // Precision is mis-used in accounting js. Scale is the proper term for number + // of decimal places. Let's adjust it here so accounting.js does not break. + if (typeof currencyInfo.scale === "number") { + currencyFormatSettings.precision = currencyInfo.scale; + } + + return accounting.formatMoney(priceToFormat, currencyFormatSettings); +} diff --git a/package/src/utils/index.js b/package/src/utils/index.js index 78a8ce8c4..f3496e3ff 100644 --- a/package/src/utils/index.js +++ b/package/src/utils/index.js @@ -7,3 +7,4 @@ export { default as getRequiredValidator } from "./getRequiredValidator"; export { default as getPhoneNumberValidator } from "./getPhoneNumberValidator"; export { default as withStripeElements } from "./withStripeElements"; export { default as addressToString } from "./addressToString"; +export { default as formatMoney } from "./formatMoney"; diff --git a/package/yarn.lock b/package/yarn.lock index 29ed2ec85..f4cc1eb7b 100644 --- a/package/yarn.lock +++ b/package/yarn.lock @@ -232,6 +232,14 @@ abbrev@1: version "1.1.1" resolved "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" +accounting-js@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/accounting-js/-/accounting-js-1.1.1.tgz#7fe4b3f70c01ebe0b85c02c5f107f1393b880c9e" + integrity sha1-f+Sz9wwB6+C4XALF8QfxOTuIDJ4= + dependencies: + is-string "^1.0.4" + object-assign "^4.0.1" + acorn-globals@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.1.0.tgz#ab716025dbe17c54d3ef81d32ece2b2d99fe2538" From fc1bb8af963ea21cc75537c1518ca51f6bfdfae0 Mon Sep 17 00:00:00 2001 From: Eric Dobbertin Date: Wed, 9 Jan 2019 06:33:41 -0600 Subject: [PATCH 06/10] feat: new PaymentsCheckoutAction component --- .../CheckoutAction/v1/CheckoutAction.js | 40 +-- .../CheckoutActions/v1/CheckoutActions.js | 12 +- .../CheckoutActions/v1/CheckoutActions.md | 165 ++++----- .../v1/FulfillmentOptionsCheckoutAction.md | 10 +- .../v1/PaymentsCheckoutAction.js | 297 +++++++++++++++++ .../v1/PaymentsCheckoutAction.md | 314 ++++++++++++++++++ .../v1/PaymentsCheckoutAction.test.js | 11 + .../PaymentsCheckoutAction/v1/index.js | 1 + .../v1/ShippingAddressCheckoutAction.md | 10 +- .../v1/StripePaymentCheckoutAction.md | 22 +- styleguide.config.js | 1 + 11 files changed, 760 insertions(+), 123 deletions(-) create mode 100644 package/src/components/PaymentsCheckoutAction/v1/PaymentsCheckoutAction.js create mode 100644 package/src/components/PaymentsCheckoutAction/v1/PaymentsCheckoutAction.md create mode 100644 package/src/components/PaymentsCheckoutAction/v1/PaymentsCheckoutAction.test.js create mode 100644 package/src/components/PaymentsCheckoutAction/v1/index.js diff --git a/package/src/components/CheckoutAction/v1/CheckoutAction.js b/package/src/components/CheckoutAction/v1/CheckoutAction.js index ab3c123d3..6ed0aa9ed 100644 --- a/package/src/components/CheckoutAction/v1/CheckoutAction.js +++ b/package/src/components/CheckoutAction/v1/CheckoutAction.js @@ -58,50 +58,40 @@ class CheckoutAction extends Component { incompleteLabel: "Incomplete Step" }; - renderActiveAction = () => { + renderActiveAction() { const { activeStepElement, activeLabel, status, stepNumber } = this.props; - const component = React.cloneElement(activeStepElement, { + if (status !== "active") return null; + + return React.cloneElement(activeStepElement, { label: (activeStepElement.props && activeStepElement.props.label) || activeLabel, stepNumber: (activeStepElement.props && activeStepElement.props.stepNumber) || stepNumber }); + } - if (status === "active") { - return component; - } - - return null; - }; - - renderCompleteAction = () => { + renderCompleteAction() { const { completeStepElement, completeLabel, status, stepNumber } = this.props; + if (status !== "complete") return null; + const component = React.cloneElement(completeStepElement, { label: (completeStepElement.props && completeStepElement.props.label) || completeLabel, stepNumber: (completeStepElement.props && completeStepElement.props.stepNumber) || stepNumber }); - if (status === "complete") { - return {component}; - } - - return null; - }; + return {component}; + } - renderIncompleteAction = () => { + renderIncompleteAction() { const { incompleteStepElement, incompleteLabel, status, stepNumber } = this.props; - const component = React.cloneElement(incompleteStepElement, { + if (status !== "incomplete") return null; + + return React.cloneElement(incompleteStepElement, { label: (incompleteStepElement.props && incompleteStepElement.props.label) || incompleteLabel, stepNumber: (incompleteStepElement.props && incompleteStepElement.props.stepNumber) || stepNumber }); - - if (status === "incomplete") { - return component; - } - - return null; - }; + } render() { return ( diff --git a/package/src/components/CheckoutActions/v1/CheckoutActions.js b/package/src/components/CheckoutActions/v1/CheckoutActions.js index b03ec44b2..8f81eacb0 100644 --- a/package/src/components/CheckoutActions/v1/CheckoutActions.js +++ b/package/src/components/CheckoutActions/v1/CheckoutActions.js @@ -128,6 +128,8 @@ class CheckoutActions extends Component { static defaultProps = {}; + state = {}; + static getDerivedStateFromProps(props, state) { if (!isEqual(props.actions, state.previousActionsProp)) { const { currentActions = [] } = state; @@ -150,8 +152,6 @@ class CheckoutActions extends Component { return null; } - state = {}; - _refs = {}; getCurrentActionIndex(id) { @@ -299,12 +299,14 @@ class CheckoutActions extends Component { return ( } + status={actionStatus} + stepNumber={this.getCurrentActionIndex(action.id) + 1} /> ); diff --git a/package/src/components/CheckoutActions/v1/CheckoutActions.md b/package/src/components/CheckoutActions/v1/CheckoutActions.md index 4770be21e..f50d3c95b 100644 --- a/package/src/components/CheckoutActions/v1/CheckoutActions.md +++ b/package/src/components/CheckoutActions/v1/CheckoutActions.md @@ -30,7 +30,7 @@ const actions = [ incompleteLabel: "Shipping method", status: "incomplete", component: FulfillmentOptionsCheckoutAction, - onSubmit: setFulfillmentOption + onSubmit: this.setFulfillmentOption props: { availableFulfillmentGroups: cart.checkout.fulfillmentGroups[0] } @@ -40,11 +40,16 @@ const actions = [ activeLabel: "Enter payment information", completeLabel: "Payment information", incompleteLabel: "Payment information", - status: "incomplete", - component: PaymentCheckoutAction, - onSubmit: setPayment, + status: remainingAmountDue === 0 && !actionAlerts["3"] ? "complete" : "incomplete", + component: PaymentsCheckoutAction, + onSubmit: this.handlePaymentSubmit, props: { - payment: cart.checkout.payments[0] + addresses, + alert: actionAlerts["3"], + onReset: this.handlePaymentsReset, + payments, + paymentMethods, + remainingAmountDue } }, { @@ -162,14 +167,20 @@ const checkoutSummary = { }] }; -const paymentMethods = [{ - _id: 1, - name: "reactionstripe", - data: { - billingAddress: null, - displayName: null +const paymentMethods = [ + { + displayName: "Credit Card", + InputComponent: StripePaymentInput, + name: "stripe_card", + shouldCollectBillingAddress: true + }, + { + displayName: "IOU", + InputComponent: ExampleIOUPaymentForm, + name: "iou_example", + shouldCollectBillingAddress: true } -}]; +]; class CheckoutActionsExample extends React.Component { constructor(props) { @@ -183,15 +194,16 @@ class CheckoutActionsExample extends React.Component { 4: null }, checkout: { - fulfillmentGroups, - payments: paymentMethods - } - } - + fulfillmentGroups + }, + payments: [] + }; + this.validateShippingAddress = this.validateShippingAddress.bind(this); this.setShippingAddress = this.setShippingAddress.bind(this); this.setFulfillmentOption = this.setFulfillmentOption.bind(this); - this.setPaymentMethod = this.setPaymentMethod.bind(this); + this.handlePaymentSubmit = this.handlePaymentSubmit.bind(this); + this.handlePaymentsReset = this.handlePaymentsReset.bind(this); } getFulfillmentOptionStatus() { @@ -209,14 +221,6 @@ class CheckoutActionsExample extends React.Component { return (groupWithoutAddress) ? "incomplete" : "complete"; } - getPaymentStatus() { - const paymentWithoutData = this.state.checkout.payments.find((payment) => { - return !payment.data.displayName; - }) - - return (paymentWithoutData) ? "incomplete" : "complete"; - } - validateShippingAddress(data) { return new Promise((resolve, reject) => { setTimeout(() => { @@ -227,7 +231,7 @@ class CheckoutActionsExample extends React.Component { title: "The address you entered may be incorrect or incomplete.", message: "Please review your entered address below. Possible errors are shown in red." }; - + return { addressValidationResults: { submittedAddress: data, @@ -250,15 +254,14 @@ class CheckoutActionsExample extends React.Component { setShippingAddress(data) { return new Promise((resolve, reject) => { - setTimeout(() => { - this.setState((state, props) => { - const { actionAlerts, checkout } = state; - actionAlerts["1"] = null; - return { - actionAlerts, - addressValidationResults: null, - checkout: { - payments: checkout.payments, + setTimeout(() => { + this.setState((state, props) => { + const { actionAlerts, checkout } = state; + actionAlerts["1"] = null; + return { + actionAlerts, + addressValidationResults: null, + checkout: { fulfillmentGroups: [{ data: { shippingAddress: data, @@ -270,54 +273,46 @@ class CheckoutActionsExample extends React.Component { } }); resolve(data); - }, 1000, { data }); + }, 1000, { data }); }); } setFulfillmentOption(data) { return new Promise((resolve, reject) => { - setTimeout(() => { - this.setState((state, props) => { - const { checkout } = state; - return { - checkout: { - payments: checkout.payments, - fulfillmentGroups: [{ - data: { - shippingAddress: checkout.fulfillmentGroups[0].data.shippingAddress - }, - selectedFulfillmentOption: data.selectedFulfillmentOption, - availableFulfillmentOptions: checkout.fulfillmentGroups[0].availableFulfillmentOptions - }] - } - }; - } - ); + setTimeout(() => { + this.setState((state, props) => { + const { checkout } = state; + return { + checkout: { + fulfillmentGroups: [{ + data: { + shippingAddress: checkout.fulfillmentGroups[0].data.shippingAddress + }, + selectedFulfillmentOption: data.selectedFulfillmentOption, + availableFulfillmentOptions: checkout.fulfillmentGroups[0].availableFulfillmentOptions + }] + } + }; + }); resolve(data); - }, 1000, { data }); + }, 1000, { data }); }); } - setPaymentMethod(data) { - const { billingAddress, token: { card } } = data; - const payment = { - data: { - billingAddress, - displayName: `${card.brand} ending in ${card.last4}` - } - } - + handlePaymentSubmit(paymentInput) { return new Promise((resolve, reject) => { - setTimeout(() => { - this.setState((state, props) => ( - { checkout: { - fulfillmentGroups: state.checkout.fulfillmentGroups, - payments: [payment] - } - } - )); - resolve(payment); - }, 1000, { payment }); + setTimeout(() => { + this.setState({ + payments: [...this.state.payments, paymentInput] + }); + resolve(); + }, 1000); + }); + }; + + handlePaymentsReset() { + this.setState({ + payments: [] }); } @@ -330,12 +325,16 @@ class CheckoutActionsExample extends React.Component { } render() { - const { actionAlerts, addressValidationResults, checkout } = this.state; + const { actionAlerts, addressValidationResults, checkout, payments } = this.state; + + const remainingAmountDue = payments.reduce((val, { payment }) => val - (payment.amount || val), 105.75); + + const addresses = checkout.fulfillmentGroups && checkout.fulfillmentGroups[0] && [checkout.fulfillmentGroups[0].data.shippingAddress]; const actions = [ { id: "1", - activeLabel: "Enter a shipping address", + activeLabel: "Enter a shipping address", completeLabel: "Shipping address", incompleteLabel: "Shipping address", status: this.getShippingStatus(), @@ -367,12 +366,16 @@ class CheckoutActionsExample extends React.Component { activeLabel: "Enter payment information", completeLabel: "Payment information", incompleteLabel: "Payment information", - status: this.getPaymentStatus(), - component: StripePaymentCheckoutAction, - onSubmit: this.setPaymentMethod, + status: remainingAmountDue === 0 && !actionAlerts["3"] ? "complete" : "incomplete", + component: PaymentsCheckoutAction, + onSubmit: this.handlePaymentSubmit, props: { - payment: checkout.payments[0], - alert: actionAlerts["3"] + addresses, + alert: actionAlerts["3"], + onReset: this.handlePaymentsReset, + payments, + paymentMethods, + remainingAmountDue } }, { diff --git a/package/src/components/FulfillmentOptionsCheckoutAction/v1/FulfillmentOptionsCheckoutAction.md b/package/src/components/FulfillmentOptionsCheckoutAction/v1/FulfillmentOptionsCheckoutAction.md index afc4f2034..307cdaff0 100644 --- a/package/src/components/FulfillmentOptionsCheckoutAction/v1/FulfillmentOptionsCheckoutAction.md +++ b/package/src/components/FulfillmentOptionsCheckoutAction/v1/FulfillmentOptionsCheckoutAction.md @@ -1,5 +1,9 @@ ### Overview +The `FulfillmentOptionsCheckoutAction` component is a checkout action component responsible for presenting a list of available fulfillment options (e.g., shipping methods) during a checkout flow. It will render a [SelectableList](./#!/AddressChoice) of the provided options. + +Typically you will use this component with the [CheckoutActions](./#!/CheckoutActions) component rather than directly rendering it. + ### Usage - The cheapest fulfillment option will be selected by default @@ -144,12 +148,12 @@ const alert = { }; - ``` diff --git a/package/src/components/PaymentsCheckoutAction/v1/PaymentsCheckoutAction.js b/package/src/components/PaymentsCheckoutAction/v1/PaymentsCheckoutAction.js new file mode 100644 index 000000000..c3f330eff --- /dev/null +++ b/package/src/components/PaymentsCheckoutAction/v1/PaymentsCheckoutAction.js @@ -0,0 +1,297 @@ +import React, { Component, Fragment } from "react"; +import PropTypes from "prop-types"; +import { withComponents } from "@reactioncommerce/components-context"; +import styled from "styled-components"; +import { addTypographyStyles, CustomPropTypes, formatMoney } from "../../../utils"; + +const Title = styled.h3` + ${addTypographyStyles("PaymentsCheckoutActionTitle", "subheadingTextBold")} +`; + +const ActionCompleteDiv = styled.div` + ${addTypographyStyles("PaymentsCheckoutActionComplete", "bodyText")}; +`; + +class PaymentsCheckoutAction extends Component { + static renderComplete({ payments }) { + if (!Array.isArray(payments) || payments.length === 0) return null; + + const paymentLines = payments.map(({ displayName, payment }, index) => ( +
{displayName}{payment.amount ? ` (${formatMoney(payment.amount)})` : null}
+ )); + + return ( + {paymentLines} + ); + } + + static propTypes = { + /** + * Provide the shipping address and any other known addresses. + * The user will be able to choose from these rather than entering + * the billing address, if the billing address is one of them. + */ + addresses: CustomPropTypes.addressBook, + /** + * Alert object provides alert into to InlineAlert. + */ + alert: CustomPropTypes.alert, + /** + * You can provide a `className` prop that will be applied to the outermost DOM element + * rendered by this component. We do not recommend using this for styling purposes, but + * it can be useful as a selector in some situations. + */ + className: PropTypes.string, + /** + * If you've set up a components context using + * [@reactioncommerce/components-context](https://github.com/reactioncommerce/components-context) + * (recommended), then this prop will come from there automatically. If you have not + * set up a components context or you want to override one of the components in a + * single spot, you can pass in the components prop directly. + */ + components: PropTypes.shape({ + /** + * Pass either the Reaction AddressChoice component or your own component that + * accepts compatible props. + */ + AddressChoice: CustomPropTypes.component.isRequired, + /** + * Pass either the Reaction InlineAlert component or your own component that + * accepts compatible props. + */ + InlineAlert: CustomPropTypes.component.isRequired, + /** + * A reaction SelectableList component or compatible component. + */ + SelectableList: CustomPropTypes.component.isRequired + }), + /** + * Is the payment input being saved? + */ + isSaving: PropTypes.bool, + /** + * Label of workflow step + */ + label: PropTypes.string.isRequired, + /** + * When this action's input data switches between being + * ready for saving and not ready for saving, this will + * be called with `true` (ready) or `false` + */ + onReadyForSaveChange: PropTypes.func, + /** + * When called, the parent should clear all previously submitted + * payments from state. Currently this is called only on mount. + */ + onReset: PropTypes.func, + /** + * Called with an object value when this component's `submit` + * method is called. The object has a `payment` property, where + * `payment` is the Payment that should be passed to the `placeOrder` + * mutation, and a `displayName` property. + */ + onSubmit: PropTypes.func, + /** + * List of all payment methods available for this shop / checkout + */ + paymentMethods: PropTypes.arrayOf(PropTypes.shape({ + displayName: PropTypes.string.isRequired, + InputComponent: CustomPropTypes.component, + name: PropTypes.string.isRequired, + shouldCollectBillingAddress: PropTypes.bool.isRequired + })).isRequired, + /** + * Pass in payment objects previously passed to onSubmit + */ + payments: PropTypes.arrayOf(PropTypes.object), + /** + * If provided, this component will ensure that no new + * payment is added with an `amount` greater than this. + */ + remainingAmountDue: PropTypes.number, + /** + * Checkout process step number + */ + stepNumber: PropTypes.number.isRequired + }; + + static defaultProps = { + onReadyForSaveChange() {}, + onReset() {}, + onSubmit() {} + }; + + constructor(props) { + super(props); + + const { addresses, paymentMethods } = props; + + let selectedPaymentMethodName = null; + if (Array.isArray(paymentMethods)) { + const [method] = paymentMethods; + if (method) { + selectedPaymentMethodName = method.name; + } + } + + this.state = { + billingAddress: addresses && addresses[0] ? addresses[0] : null, + inputIsComplete: false, + selectedPaymentMethodName + }; + } + + componentDidMount() { + this.checkIfReadyForSaveChange(); + this.props.onReset(); + } + + _inputComponent = null; + + submit = async () => { + if (this._inputComponent) { + this._inputComponent.submit(); + } else { + this.handleInputComponentSubmit(); + } + } + + handleInputComponentSubmit = async ({ amount = null, data, displayName } = {}) => { + const { onSubmit, paymentMethods, remainingAmountDue } = this.props; + const { billingAddress, selectedPaymentMethodName } = this.state; + + const selectedPaymentMethod = paymentMethods.find((method) => method.name === selectedPaymentMethodName); + + let cappedPaymentAmount = amount; + if (cappedPaymentAmount && typeof remainingAmountDue === "number") { + cappedPaymentAmount = Math.min(cappedPaymentAmount, remainingAmountDue); + } + + await onSubmit({ + displayName: displayName || selectedPaymentMethod.displayName, + payment: { + amount: cappedPaymentAmount, + billingAddress, + data, + method: selectedPaymentMethodName + } + }); + } + + checkIfReadyForSaveChange() { + const { onReadyForSaveChange, paymentMethods } = this.props; + const { billingAddress, inputIsComplete, selectedPaymentMethodName } = this.state; + + const isFilled = billingAddress && + Object.keys(billingAddress).every((key) => (["address2", "company"].indexOf(key) > -1 ? true : billingAddress[key] !== null)); + + const selectedPaymentMethod = paymentMethods.find((method) => method.name === selectedPaymentMethodName); + const isInputReady = !selectedPaymentMethod || !selectedPaymentMethod.InputComponent || inputIsComplete; + + onReadyForSaveChange(!!(isInputReady && isFilled)); + } + + handleAddressChange = (billingAddress = null) => { + this.setState({ billingAddress }, () => { + this.checkIfReadyForSaveChange(); + }); + }; + + handleInputReadyForSaveChange = (inputIsComplete) => { + this.setState({ inputIsComplete }, () => { + this.checkIfReadyForSaveChange(); + }); + } + + handleSelectedPaymentMethodChange = (selectedPaymentMethodName) => { + this.setState({ selectedPaymentMethodName }, () => { + this.checkIfReadyForSaveChange(); + }); + }; + + renderBillingAddressForm() { + const { addresses, components: { AddressChoice }, isSaving } = this.props; + + return ( + + Billing Address + + + ); + } + + renderPartialPayments() { + const { components: { InlineAlert }, payments } = this.props; + + if (!Array.isArray(payments) || payments.length === 0) return null; + + const message = payments.map(({ displayName, payment }) => `${displayName} - ${formatMoney(payment.amount)}`).join(", "); + + return ( + + ); + } + + renderPaymentMethodList() { + const { + components: { SelectableList }, + isSaving, + paymentMethods + } = this.props; + + if (paymentMethods.length < 2) return null; + + const { selectedPaymentMethodName } = this.state; + const options = paymentMethods.map((method) => ({ + id: method.name, + label: method.displayName, + value: method.name + })); + + return ( + + ); + } + + render() { + const { + alert, + className, + components: { InlineAlert }, + isSaving, + label, + paymentMethods, + stepNumber + } = this.props; + + const { selectedPaymentMethodName } = this.state; + const selectedPaymentMethod = paymentMethods.find((method) => method.name === selectedPaymentMethodName); + + return ( +
+ + {stepNumber}. {label} + + {alert ? : ""} + {this.renderPartialPayments()} + {this.renderPaymentMethodList()} + {!!selectedPaymentMethod && !!selectedPaymentMethod.InputComponent && + { this._inputComponent = instance; }} + />} + {!!selectedPaymentMethod && !!selectedPaymentMethod.shouldCollectBillingAddress && this.renderBillingAddressForm()} +
+ ); + } +} + +export default withComponents(PaymentsCheckoutAction); diff --git a/package/src/components/PaymentsCheckoutAction/v1/PaymentsCheckoutAction.md b/package/src/components/PaymentsCheckoutAction/v1/PaymentsCheckoutAction.md new file mode 100644 index 000000000..f2ac72948 --- /dev/null +++ b/package/src/components/PaymentsCheckoutAction/v1/PaymentsCheckoutAction.md @@ -0,0 +1,314 @@ +### Overview + +The `PaymentsCheckoutAction` component is a checkout action component responsible for collecting payment information for one or more payments during a checkout flow. It will also render an [AddressChoice](./#!/AddressChoice) component to collect a billing address for any payment methods that require it. + +Typically you will use this component with the [CheckoutActions](./#!/CheckoutActions) component rather than directly rendering it. + +Note that this component allows collecting information for multiple payments but does not keep its own payments state. Each time a payment is submitted, it will be passed to `onSubmit` and the parent should save it somewhere in state and add that payment to the `payments` prop array. It's also best if the parent sums the amounts of those payments, subtracts that from the total due, and passes the result in the `remainingAmountDue` prop. The `amount` of any payment passed to `onSubmit` will be capped at this value. + +### Usage + +#### Basic Incomplete State + +```jsx +const paymentMethods = [ + { + displayName: "Credit Card", + InputComponent: StripePaymentInput, + name: "stripe_card", + shouldCollectBillingAddress: true + }, + { + displayName: "IOU", + InputComponent: ExampleIOUPaymentForm, + name: "iou_example", + shouldCollectBillingAddress: true + } +]; + + +``` + +#### Basic Incomplete State While Saving + +```jsx +const paymentMethods = [ + { + displayName: "Credit Card", + InputComponent: StripePaymentInput, + name: "stripe_card", + shouldCollectBillingAddress: true + }, + { + displayName: "IOU", + InputComponent: ExampleIOUPaymentForm, + name: "iou_example", + shouldCollectBillingAddress: true + } +]; + + +``` + +#### Incomplete State With Alert + +```jsx +const paymentMethods = [ + { + displayName: "Credit Card", + InputComponent: StripePaymentInput, + name: "stripe_card", + shouldCollectBillingAddress: true + }, + { + displayName: "IOU", + InputComponent: ExampleIOUPaymentForm, + name: "iou_example", + shouldCollectBillingAddress: true + } +]; + +const alert = { + alertType: "error", + title: "The payment information you entered may be incorrect.", + message: "Please review our error." +}; + + +``` + +#### Incomplete State With Addresses + +```jsx +const paymentMethods = [ + { + displayName: "Credit Card", + InputComponent: StripePaymentInput, + name: "stripe_card", + shouldCollectBillingAddress: true + }, + { + displayName: "IOU", + InputComponent: ExampleIOUPaymentForm, + name: "iou_example", + shouldCollectBillingAddress: true + } +]; + +const addresses = [ + { + _id: "20", + address1: "7742 Hwy 23", + address2: "", + country: "US", + city: "Belle Chasse", + fullName: "Salvos Seafood", + postal: "70037", + region: "LA", + phone: "(504) 393-7303" + }, + { + _id: "21", + address1: "35 Akin Adesola St", + address2: "", + country: "NG", + city: "Lagos", + fullName: "Ocean Basket Victoria Island", + postal: "101241", + region: "Victoria Island", + phone: "234 816 059 1821" + } +]; + + +``` + +#### Incomplete State With Addresses and Alert + +```jsx +const paymentMethods = [ + { + displayName: "Credit Card", + InputComponent: StripePaymentInput, + name: "stripe_card", + shouldCollectBillingAddress: true + }, + { + displayName: "IOU", + InputComponent: ExampleIOUPaymentForm, + name: "iou_example", + shouldCollectBillingAddress: true + } +]; + +const addresses = [ + { + _id: "20", + address1: "7742 Hwy 23", + address2: "", + country: "US", + city: "Belle Chasse", + fullName: "Salvos Seafood", + postal: "70037", + region: "LA", + phone: "(504) 393-7303" + }, + { + _id: "21", + address1: "35 Akin Adesola St", + address2: "", + country: "NG", + city: "Lagos", + fullName: "Ocean Basket Victoria Island", + postal: "101241", + region: "Victoria Island", + phone: "234 816 059 1821" + } +]; + +const alert = { + alertType: "error", + title: "The payment information you entered may be incorrect.", + message: "Please review our error." +}; + + +``` + +#### Incomplete State With Partial Payment + +```jsx +const paymentMethods = [ + { + displayName: "Credit Card", + InputComponent: StripePaymentInput, + name: "stripe_card", + shouldCollectBillingAddress: true + }, + { + displayName: "IOU", + InputComponent: ExampleIOUPaymentForm, + name: "iou_example", + shouldCollectBillingAddress: true + } +]; + +const addresses = [ + { + _id: "20", + address1: "7742 Hwy 23", + address2: "", + country: "US", + city: "Belle Chasse", + fullName: "Salvos Seafood", + postal: "70037", + region: "LA", + phone: "(504) 393-7303" + }, + { + _id: "21", + address1: "35 Akin Adesola St", + address2: "", + country: "NG", + city: "Lagos", + fullName: "Ocean Basket Victoria Island", + postal: "101241", + region: "Victoria Island", + phone: "234 816 059 1821" + } +]; + +const payments = [ + { + displayName: "IOU from Fats Domino", + payment: { + amount: 15, + data: { + fullName: "Fats Domino" + }, + method: "iou_example" + } + } +]; + + +``` + +#### Completed State + +```jsx +const payments = [ + { + displayName: "Gift card", + payment: { + amount: 50, + data: { + cardNumber: "ABC123" + }, + method: "gift_card" + } + }, + { + displayName: "Visa ending in 7777", + payment: { + amount: null, + data: { + stripeTokenId: "ABC123" + }, + method: "stripe_card" + } + } +]; + +PaymentsCheckoutAction.renderComplete({ payments }); +``` + +#### Full Example + +For a full example of this action in context, see [CheckoutActions](./#!/CheckoutActions). + +### Theme + +See [Theming Components](./#!/Theming%20Components). + +#### Typography + +- The label text uses `subheadingTextBold` style with `rui_components.PaymentsCheckoutActionTitle` override +- The completed state text uses `bodyText` style with `rui_components.PaymentsCheckoutActionComplete` override diff --git a/package/src/components/PaymentsCheckoutAction/v1/PaymentsCheckoutAction.test.js b/package/src/components/PaymentsCheckoutAction/v1/PaymentsCheckoutAction.test.js new file mode 100644 index 000000000..995b11ea6 --- /dev/null +++ b/package/src/components/PaymentsCheckoutAction/v1/PaymentsCheckoutAction.test.js @@ -0,0 +1,11 @@ +import React from "react"; +import renderer from "react-test-renderer"; +import { shallow } from "enzyme"; +import PaymentsCheckoutAction from "./PaymentsCheckoutAction"; + +test("basic snapshot", () => { + const component = renderer.create(); + + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); +}); diff --git a/package/src/components/PaymentsCheckoutAction/v1/index.js b/package/src/components/PaymentsCheckoutAction/v1/index.js new file mode 100644 index 000000000..a6898b914 --- /dev/null +++ b/package/src/components/PaymentsCheckoutAction/v1/index.js @@ -0,0 +1 @@ +export { default } from "./PaymentsCheckoutAction"; diff --git a/package/src/components/ShippingAddressCheckoutAction/v1/ShippingAddressCheckoutAction.md b/package/src/components/ShippingAddressCheckoutAction/v1/ShippingAddressCheckoutAction.md index e4c1189d8..2097864b5 100644 --- a/package/src/components/ShippingAddressCheckoutAction/v1/ShippingAddressCheckoutAction.md +++ b/package/src/components/ShippingAddressCheckoutAction/v1/ShippingAddressCheckoutAction.md @@ -5,7 +5,7 @@ The `ShippingAddressCheckoutAction` serves 4 purposes: * To provide `CheckoutActions` a way to know the action is ready for save, * To Provide `CheckoutActions` a way to capture the `AddressForm` values. -For a full implementation example see the [CheckoutActions](/#!/CheckoutActions) +Typically you will use this component with the [CheckoutActions](./#!/CheckoutActions) component rather than directly rendering it. For a full implementation example see [CheckoutActions](/#!/CheckoutActions). ### Usage @@ -199,10 +199,10 @@ const props = { };
- {state.fulfillmentGroup && state.fulfillmentGroup.data.shippingAddress + {state.fulfillmentGroup && state.fulfillmentGroup.data.shippingAddress ? action.renderComplete(state) : (
- {_form = el}} /> + {_form = el}} />
)}
@@ -291,10 +291,10 @@ const props = { };
- {state.fulfillmentGroup && state.fulfillmentGroup.data.shippingAddress + {state.fulfillmentGroup && state.fulfillmentGroup.data.shippingAddress ? action.renderComplete(state) : (
- {_form = el}} /> + {_form = el}} />
)}
diff --git a/package/src/components/StripePaymentCheckoutAction/v1/StripePaymentCheckoutAction.md b/package/src/components/StripePaymentCheckoutAction/v1/StripePaymentCheckoutAction.md index 132489e4a..01620266e 100644 --- a/package/src/components/StripePaymentCheckoutAction/v1/StripePaymentCheckoutAction.md +++ b/package/src/components/StripePaymentCheckoutAction/v1/StripePaymentCheckoutAction.md @@ -1,4 +1,18 @@ ### Overview + +DEPRECATED. Use [PaymentsCheckoutAction](/#!/PaymentsCheckoutAction) with the following payment method config instead: + +```js static +const paymentMethods = [ + { + displayName: "Credit Card", + InputComponent: StripePaymentInput, + name: "stripe_card", + shouldCollectBillingAddress: true + } +]; +``` + This checkout action is responsible for rendering the [StripeForm](/#!/StripeForm) component used to collect payment information. It is also capable of rendering a form to collect billing address information if the user chooses to use a new billing address. ### Usage @@ -27,12 +41,12 @@ const alert = { }; - ``` diff --git a/styleguide.config.js b/styleguide.config.js index 432ad2359..8aed7c27f 100644 --- a/styleguide.config.js +++ b/styleguide.config.js @@ -489,6 +489,7 @@ module.exports = { "ExampleIOUPaymentForm", "FinalReviewCheckoutAction", "FulfillmentOptionsCheckoutAction", + "PaymentsCheckoutAction", "ShippingAddressCheckoutAction", "StripePaymentCheckoutAction", "StripePaymentInput" From e8eff454ac70fdd85d54e7d50712318bc1060f2f Mon Sep 17 00:00:00 2001 From: Eric Dobbertin Date: Wed, 9 Jan 2019 09:59:03 -0600 Subject: [PATCH 07/10] test: additional tests for PaymentsCheckoutAction --- .../v1/PaymentsCheckoutAction.test.js | 124 +++++++++++++++++- .../PaymentsCheckoutAction.test.js.snap | 116 ++++++++++++++++ package/src/tests/mockComponents.js | 6 +- 3 files changed, 243 insertions(+), 3 deletions(-) create mode 100644 package/src/components/PaymentsCheckoutAction/v1/__snapshots__/PaymentsCheckoutAction.test.js.snap diff --git a/package/src/components/PaymentsCheckoutAction/v1/PaymentsCheckoutAction.test.js b/package/src/components/PaymentsCheckoutAction/v1/PaymentsCheckoutAction.test.js index 995b11ea6..fce3b323a 100644 --- a/package/src/components/PaymentsCheckoutAction/v1/PaymentsCheckoutAction.test.js +++ b/package/src/components/PaymentsCheckoutAction/v1/PaymentsCheckoutAction.test.js @@ -1,11 +1,131 @@ import React from "react"; import renderer from "react-test-renderer"; -import { shallow } from "enzyme"; +import { mount } from "enzyme"; +import { ComponentsProvider } from "@reactioncommerce/components-context"; +import mockComponents from "../../../tests/mockComponents"; +import realComponents from "../../../tests/realComponents"; import PaymentsCheckoutAction from "./PaymentsCheckoutAction"; +const paymentMethods = [ + { + displayName: "Credit Card", + InputComponent: mockComponents.StripePaymentInput, + name: "stripe_card", + shouldCollectBillingAddress: true + }, + { + displayName: "IOU", + InputComponent: mockComponents.ExampleIOUPaymentForm, + name: "iou_example", + shouldCollectBillingAddress: true + } +]; + test("basic snapshot", () => { - const component = renderer.create(); + const component = renderer.create(( + + )); + + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); +}); + +test("snapshot with a partial payment", () => { + const payments = [ + { + displayName: "IOU from Fats Domino", + payment: { + amount: 15, + data: { + fullName: "Fats Domino" + }, + method: "iou_example" + } + } + ]; + + const component = renderer.create(( + + )); + + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); +}); + +test("snapshot with an alert", () => { + const alert = { + alertType: "error", + title: "The payment information you entered may be incorrect.", + message: "Please review our error." + }; + + const component = renderer.create(( + + )); const tree = component.toJSON(); expect(tree).toMatchSnapshot(); }); + +test("renders a selectable list and the first method's input component", () => { + const wrapper = mount(( + + + + )); + + expect(wrapper.find('input[value="stripe_card"]').length).toBe(1); + expect(wrapper.find(mockComponents.StripePaymentInput).length).toBe(1); +}); + +test("does not render the SelectableList if there's only one method", () => { + const wrapper = mount(( + + + + )); + + expect(wrapper.find('input[value="stripe_card"]').length).toBe(0); +}); + +test("does not render the AddressForm if the method doesn't need it", () => { + const wrapper = mount(( + + + + )); + + expect(wrapper.find('input[name="address1"]').length).toBe(0); +}); diff --git a/package/src/components/PaymentsCheckoutAction/v1/__snapshots__/PaymentsCheckoutAction.test.js.snap b/package/src/components/PaymentsCheckoutAction/v1/__snapshots__/PaymentsCheckoutAction.test.js.snap new file mode 100644 index 000000000..0b48641ec --- /dev/null +++ b/package/src/components/PaymentsCheckoutAction/v1/__snapshots__/PaymentsCheckoutAction.test.js.snap @@ -0,0 +1,116 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`basic snapshot 1`] = ` +.c0 { + -webkit-font-smoothing: antialiased; + color: #505558; + font-family: 'Source Sans Pro','Helvetica Neue',Helvetica,sans-serif; + font-size: 18px; + font-style: normal; + font-stretch: normal; + font-weight: 700; + -webkit-letter-spacing: .03em; + -moz-letter-spacing: .03em; + -ms-letter-spacing: .03em; + letter-spacing: .03em; + line-height: 1.25; +} + +
+

+ 3 + . + Payment +

+ + SelectableList({"name":"paymentMethodList","options":"[Object]","value":"stripe_card"}) + StripePaymentInput({}) +

+ Billing Address +

+ AddressChoice({}) +
+`; + +exports[`snapshot with a partial payment 1`] = ` +.c0 { + -webkit-font-smoothing: antialiased; + color: #505558; + font-family: 'Source Sans Pro','Helvetica Neue',Helvetica,sans-serif; + font-size: 18px; + font-style: normal; + font-stretch: normal; + font-weight: 700; + -webkit-letter-spacing: .03em; + -moz-letter-spacing: .03em; + -ms-letter-spacing: .03em; + letter-spacing: .03em; + line-height: 1.25; +} + +
+

+ 3 + . + Payment +

+ + InlineAlert({"alertType":"success","message":"IOU from Fats Domino - $15.00","title":"Partial Payments"}) + SelectableList({"name":"paymentMethodList","options":"[Object]","value":"stripe_card"}) + StripePaymentInput({}) +

+ Billing Address +

+ AddressChoice({}) +
+`; + +exports[`snapshot with an alert 1`] = ` +.c0 { + -webkit-font-smoothing: antialiased; + color: #505558; + font-family: 'Source Sans Pro','Helvetica Neue',Helvetica,sans-serif; + font-size: 18px; + font-style: normal; + font-stretch: normal; + font-weight: 700; + -webkit-letter-spacing: .03em; + -moz-letter-spacing: .03em; + -ms-letter-spacing: .03em; + letter-spacing: .03em; + line-height: 1.25; +} + +
+

+ 3 + . + Payment +

+ InlineAlert({"alertType":"error","title":"The payment information you entered may be incorrect.","message":"Please review our error."}) + SelectableList({"name":"paymentMethodList","options":"[Object]","value":"stripe_card"}) + StripePaymentInput({}) +

+ Billing Address +

+ AddressChoice({}) +
+`; diff --git a/package/src/tests/mockComponents.js b/package/src/tests/mockComponents.js index dde84f46c..8eb9ab119 100644 --- a/package/src/tests/mockComponents.js +++ b/package/src/tests/mockComponents.js @@ -46,6 +46,7 @@ function stringifyJSONCircularSafe(obj) { "Address", "AddressBook", "AddressCapture", + "AddressChoice", "AddressForm", "AddressReview", "AddressSelect", @@ -62,6 +63,7 @@ function stringifyJSONCircularSafe(obj) { "CheckoutActionComplete", "CheckoutActionIncomplete", "ErrorsBlock", + "ExampleIOUPaymentForm", "Field", "InlineAlert", "InPageMenuItem", @@ -69,6 +71,7 @@ function stringifyJSONCircularSafe(obj) { "ItemEditForm", "Link", "MiniCartSummary", + "PaymentsCheckoutAction", "PhoneNumberInput", "Price", "ProgressiveImage", @@ -80,7 +83,8 @@ function stringifyJSONCircularSafe(obj) { "TextInput", "SelectableItem", "SelectableList", - "StripeForm" + "StripeForm", + "StripePaymentInput" ].forEach((componentName) => { mockComponents[componentName] = makeMockedComponent(componentName); }); From eaaf602ea1e7a8321ed682b8f0f8859f4a9addbe Mon Sep 17 00:00:00 2001 From: Eric Dobbertin Date: Wed, 9 Jan 2019 10:03:07 -0600 Subject: [PATCH 08/10] test: update CheckoutActions snapshot Only the order of the props changed --- .../v1/__snapshots__/CheckoutActions.test.js.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/src/components/CheckoutActions/v1/__snapshots__/CheckoutActions.test.js.snap b/package/src/components/CheckoutActions/v1/__snapshots__/CheckoutActions.test.js.snap index c3f340152..03784f4d3 100644 --- a/package/src/components/CheckoutActions/v1/__snapshots__/CheckoutActions.test.js.snap +++ b/package/src/components/CheckoutActions/v1/__snapshots__/CheckoutActions.test.js.snap @@ -34,7 +34,7 @@ exports[`basic snapshot 1`] = `
- CheckoutAction({"status":"active","activeLabel":"mock active action one","completeLabel":"mock complete action one","incompleteLabel":"mock inactive action one","stepNumber":1,"activeStepElement":"[Object]","completeStepElement":"[Object]","incompleteStepElement":"[Object]"}) + CheckoutAction({"activeLabel":"mock active action one","activeStepElement":"[Object]","completeLabel":"mock complete action one","completeStepElement":"[Object]","incompleteLabel":"mock inactive action one","incompleteStepElement":"[Object]","status":"active","stepNumber":1})
`; From ed19011b88e359212bad1485a9e9c19e571f3c9d Mon Sep 17 00:00:00 2001 From: Eric Dobbertin Date: Wed, 9 Jan 2019 10:06:11 -0600 Subject: [PATCH 09/10] chore: fix lint --- package/src/components/CheckoutActions/v1/CheckoutActions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package/src/components/CheckoutActions/v1/CheckoutActions.js b/package/src/components/CheckoutActions/v1/CheckoutActions.js index 8f81eacb0..0f40e78e9 100644 --- a/package/src/components/CheckoutActions/v1/CheckoutActions.js +++ b/package/src/components/CheckoutActions/v1/CheckoutActions.js @@ -128,8 +128,6 @@ class CheckoutActions extends Component { static defaultProps = {}; - state = {}; - static getDerivedStateFromProps(props, state) { if (!isEqual(props.actions, state.previousActionsProp)) { const { currentActions = [] } = state; @@ -152,6 +150,8 @@ class CheckoutActions extends Component { return null; } + state = {}; + _refs = {}; getCurrentActionIndex(id) { From bc51f73075680e483d2efe3b24d0e806dc756bc0 Mon Sep 17 00:00:00 2001 From: Eric Dobbertin Date: Fri, 11 Jan 2019 15:12:54 -0600 Subject: [PATCH 10/10] docs: update prop definitions and other docs --- package/src/components/AddressChoice/v1/AddressChoice.js | 2 +- package/src/components/AddressChoice/v1/AddressChoice.md | 2 +- package/src/components/AddressForm/v1/AddressForm.js | 5 +++-- .../ExampleIOUPaymentForm/v1/ExampleIOUPaymentForm.js | 3 ++- .../PaymentsCheckoutAction/v1/PaymentsCheckoutAction.js | 8 +++++--- .../StripePaymentInput/v1/StripePaymentInput.js | 3 ++- 6 files changed, 14 insertions(+), 9 deletions(-) diff --git a/package/src/components/AddressChoice/v1/AddressChoice.js b/package/src/components/AddressChoice/v1/AddressChoice.js index dfac78f92..3e5be8e25 100644 --- a/package/src/components/AddressChoice/v1/AddressChoice.js +++ b/package/src/components/AddressChoice/v1/AddressChoice.js @@ -34,7 +34,7 @@ class AddressChoice extends Component { SelectableList: CustomPropTypes.component.isRequired }), /** - * Disable editing? + * If true, choosing an address and typing in address fields is disabled */ isReadOnly: PropTypes.bool, /** diff --git a/package/src/components/AddressChoice/v1/AddressChoice.md b/package/src/components/AddressChoice/v1/AddressChoice.md index c5bc9a58f..c2b5d2aec 100644 --- a/package/src/components/AddressChoice/v1/AddressChoice.md +++ b/package/src/components/AddressChoice/v1/AddressChoice.md @@ -1,6 +1,6 @@ ### Overview -The `AddressChoice` component is a way of collecting an address while giving an option of choosing an already known address. If no already known addresses are provided, it will render an [AddressForm](./#!/AddressForm). Otherwise it will render a choice list where each address is in the list and the final option is to enter a new address. +The `AddressChoice` component is a way of collecting an address while giving an option of choosing a previously saved address. If no previously saved addresses are provided, it will render an [AddressForm](./#!/AddressForm). Otherwise it will render a choice list where each address is in the list and the final option is to enter a new address. ### Usage diff --git a/package/src/components/AddressForm/v1/AddressForm.js b/package/src/components/AddressForm/v1/AddressForm.js index ee62120ee..6c5d3f0a7 100644 --- a/package/src/components/AddressForm/v1/AddressForm.js +++ b/package/src/components/AddressForm/v1/AddressForm.js @@ -99,11 +99,12 @@ class AddressForm extends Component { */ isOnDarkBackground: PropTypes.bool, /** - * Do not allow editing of the form fields + * If true, typing in address fields is disabled */ isReadOnly: PropTypes.bool, /** - * Is the address being saved + * Pass true if the address is in the process of being saved. + * While true, typing in address fields is disabled. */ isSaving: PropTypes.bool, /** diff --git a/package/src/components/ExampleIOUPaymentForm/v1/ExampleIOUPaymentForm.js b/package/src/components/ExampleIOUPaymentForm/v1/ExampleIOUPaymentForm.js index 7218a2c5e..20ae28b4e 100644 --- a/package/src/components/ExampleIOUPaymentForm/v1/ExampleIOUPaymentForm.js +++ b/package/src/components/ExampleIOUPaymentForm/v1/ExampleIOUPaymentForm.js @@ -55,7 +55,8 @@ class ExampleIOUPaymentForm extends Component { TextInput: CustomPropTypes.component.isRequired }), /** - * Is the payment input being saved? + * Pass true while the input data is in the process of being saved. + * While true, the form fields are disabled. */ isSaving: PropTypes.bool, /** diff --git a/package/src/components/PaymentsCheckoutAction/v1/PaymentsCheckoutAction.js b/package/src/components/PaymentsCheckoutAction/v1/PaymentsCheckoutAction.js index c3f330eff..5cbe01e15 100644 --- a/package/src/components/PaymentsCheckoutAction/v1/PaymentsCheckoutAction.js +++ b/package/src/components/PaymentsCheckoutAction/v1/PaymentsCheckoutAction.js @@ -27,9 +27,9 @@ class PaymentsCheckoutAction extends Component { static propTypes = { /** - * Provide the shipping address and any other known addresses. + * Provide the shipping address and any other previously saved addresses. * The user will be able to choose from these rather than entering - * the billing address, if the billing address is one of them. + * the billing address if they want. */ addresses: CustomPropTypes.addressBook, /** @@ -66,7 +66,9 @@ class PaymentsCheckoutAction extends Component { SelectableList: CustomPropTypes.component.isRequired }), /** - * Is the payment input being saved? + * Pass true while the input data is in the process of being saved. + * This is passed down as the `isSaving` prop of each payment method's + * InputComponent, and typically will result in the form fields being disabled. */ isSaving: PropTypes.bool, /** diff --git a/package/src/components/StripePaymentInput/v1/StripePaymentInput.js b/package/src/components/StripePaymentInput/v1/StripePaymentInput.js index 91bc7f9b9..126fe581e 100644 --- a/package/src/components/StripePaymentInput/v1/StripePaymentInput.js +++ b/package/src/components/StripePaymentInput/v1/StripePaymentInput.js @@ -45,7 +45,8 @@ class StripePaymentInput extends Component { StripeForm: CustomPropTypes.component.isRequired }), /** - * Is the payment input being saved? + * Pass true while the input data is in the process of being saved. + * While true, the form fields are disabled. */ isSaving: PropTypes.bool, /**