diff --git a/demo/peacock/Makefile b/demo/peacock/Makefile index bcd45b4be6..5ee6f027e8 100644 --- a/demo/peacock/Makefile +++ b/demo/peacock/Makefile @@ -9,7 +9,7 @@ dev d: export eval `cat .env` && yarn dev setup: clean - yarn --pure-lockfile + yarn --pure-lockfile build: $(call header, Building) diff --git a/demo/peacock/package.json b/demo/peacock/package.json index c1db649d7e..4915885623 100644 --- a/demo/peacock/package.json +++ b/demo/peacock/package.json @@ -5,6 +5,7 @@ "main": "index.js", "scripts": { "lint": "eslint --ext .js --ext .jsx .", + "fix-lint": "eslint --ext .js --ext .jsx . --fix", "flow": "flow check", "dev": "gulp dev --optimize_for_size --stack_size=4096", "test": "node_modules/mocha/bin/mocha", @@ -87,7 +88,7 @@ "watchify": "^3.9.0" }, "dependencies": { - "@foxcomm/api-js": "^1.2.0", + "@foxcomm/api-js": "^1.2.3", "@foxcomm/babel-plugin-react-stylenames": "^3.1.0", "@foxcomm/storefront-react": "^1.3.0", "@foxcomm/wings": "^1.9.12", diff --git a/demo/peacock/src/components/cart/cart.css b/demo/peacock/src/components/cart/cart.css index e9e9bfa813..b80eecc631 100644 --- a/demo/peacock/src/components/cart/cart.css +++ b/demo/peacock/src/components/cart/cart.css @@ -3,6 +3,19 @@ @import "media-queries.css"; @import "variables.css"; +.apple-pay { + background-color: black; + background-image: -webkit-named-image(apple-pay-logo-white); + background-size: cover; + background-origin: content-box; + background-repeat: no-repeat; + width: 100%; + height: 44px; + border-radius: 0; + margin-top: 20px; + padding: 15px 0; +} + .cart-box { position: fixed; z-index: 3; @@ -86,6 +99,10 @@ border-radius: 0; margin-top: 0; } + + &.with-apple-pay { + height: calc(100% - 186px); + } } .line-items { @@ -102,6 +119,10 @@ height: 58px; position: absolute; bottom: 0; + + &.with-apple-pay { + height: 136px; + } } @media (--small-only), (--medium-only) { @@ -119,16 +140,25 @@ @media (--large) { .cart-content { height: calc(100% - 130px); + + &.with-apple-pay { + height: calc(100% - 206px); + } } .cart-footer { - width: 100%; height: 80px; + width: 100%; position: absolute; bottom: 0; display: flex; - align-items: flex-start; - justify-content: center; + align-items: center; + justify-content: flex-start; + flex-direction: column; + + &.with-apple-pay { + height: 156px; + } } .checkout-button { diff --git a/demo/peacock/src/components/cart/cart.jsx b/demo/peacock/src/components/cart/cart.jsx index 727b8d0ef9..b49721a136 100644 --- a/demo/peacock/src/components/cart/cart.jsx +++ b/demo/peacock/src/components/cart/cart.jsx @@ -8,56 +8,65 @@ import { connect } from 'react-redux'; import { browserHistory } from 'lib/history'; import { autobind } from 'core-decorators'; import * as tracking from 'lib/analytics'; - -// localization import localized from 'lib/i18n'; +import { emailIsSet } from 'paragons/auth'; +import sanitizeAll from 'sanitizers'; +import sanitizeLineItems from 'sanitizers/line-items'; + +// actions +import * as actions from 'modules/cart'; +import { checkApplePay, beginApplePay } from 'modules/checkout'; // components import Currency from 'ui/currency'; import LineItem from './line-item'; import Button from 'ui/buttons'; -import ErrorAlerts from '@foxcomm/wings/lib/ui/alerts/error-alerts'; +import ErrorAlerts from 'ui/alerts/error-alerts'; import { skuIdentity } from '@foxcomm/wings/lib/paragons/sku'; import { parseError } from '@foxcomm/api-js'; import Overlay from 'ui/overlay/overlay'; import ActionLink from 'ui/action-link/action-link'; - -// styles -import styles from './cart.css'; +import GuestAuth from 'pages/checkout/guest-auth/guest-auth'; // types import type { Totals } from 'modules/cart'; -// actions -import * as actions from 'modules/cart'; +// styles +import styles from './cart.css'; type Props = { - fetch: Function, - deleteLineItem: Function, - updateLineItemQuantity: Function, - toggleCart: Function, - hideCart: Function, - skus: Array, + fetch: Function, // signature + deleteLineItem: Function, // siganture + updateLineItemQuantity: Function, // signature + toggleCart: Function, // signature + hideCart: Function, // signature + skus: Array, coupon: ?Object, promotion: ?Object, totals: Totals, user?: ?Object, isVisible: boolean, t: any, + applePayAvailable: boolean, + checkApplePay: () => void, + location: Object, + beginApplePay: (paymentRequest: Object) => Promise<*>, }; type State = { - errors?: Array, + errors?: ?Array, + guestAuth: boolean, }; class Cart extends Component { props: Props; state: State = { - + guestAuth: false, }; componentDidMount() { + this.props.checkApplePay(); if (this.props.user) { this.props.fetch(this.props.user); } else { @@ -65,6 +74,12 @@ class Cart extends Component { } } + componentWillReceiveProps(nextProps: Props) { + if (this.props.applePayAvailable !== nextProps.applePayAvailable) { + this.props.checkApplePay(); + } + } + @autobind deleteLineItem(sku) { tracking.removeFromCart(sku, sku.quantity); @@ -90,6 +105,12 @@ class Cart extends Component { }); } + @autobind + isEmailSetForCheckout() { + const user = _.get(this.props, 'user', null); + return emailIsSet(user); + } + get lineItems() { if (_.isEmpty(this.props.skus)) { return ( @@ -124,14 +145,29 @@ class Cart extends Component { }); } + @autobind + sanitize(err) { + const sanitizedLineItems = sanitizeLineItems(err, this.props.skus); + + return sanitizedLineItems ? sanitizedLineItems : sanitizeAll(err); + } + get errorsLine() { - if (this.state.errors && !_.isEmpty(this.state.errors)) { - return ; - } + const { errors } = this.state; + + if (!errors && _.isEmpty(errors)) return null; + + return ( + + ); } @autobind onCheckout() { + this.setState({ errors: null }); Promise.resolve(this.props.hideCart()) .then(() => { browserHistory.push('/checkout'); @@ -139,6 +175,76 @@ class Cart extends Component { ; } + @autobind + beginApplePay() { + const { total } = this.props.totals; + const amount = (parseFloat(total) / 100).toFixed(2); + const paymentRequest = { + countryCode: 'US', + currencyCode: 'USD', + total: { + label: 'Pure', + amount, + }, + requiredShippingContactFields: [ + 'postalAddress', + 'name', + 'phone', + ], + requiredBillingContactFields: [ + 'postalAddress', + 'name', + ], + }; + + this.props.beginApplePay(paymentRequest).then(() => { + this.setState({ errors: null }); + browserHistory.push('/checkout/done'); + }) + .catch((err) => { + this.setState({ + errors: parseError(err), + }); + }); + } + + @autobind + checkAuth() { + const emailSet = this.isEmailSetForCheckout(); + + if (emailSet) { + this.setState({ guestAuth: false }); + this.beginApplePay(); + } else { + this.setState({ guestAuth: true }); + } + } + + get applePayButton() { + if (!this.props.applePayAvailable) return null; + + const disabled = _.size(this.props.skus) < 1; + return ( + + {this.applePayButton} + {this.guestAuth} ); } } -const mapStateToProps = state => ({ ...state.cart, ...state.auth }); +const mapStateToProps = (state) => { + return { + ...state.cart, + ...state.auth, + applePayAvailable: _.get(state.checkout, 'applePayAvailable', false), + location: _.get(state.routing, 'location', {}), + }; +}; export default connect(mapStateToProps, { ...actions, + checkApplePay, + beginApplePay, })(localized(Cart)); diff --git a/demo/peacock/src/modules/checkout.js b/demo/peacock/src/modules/checkout.js index 97044e89c5..a50434cd29 100644 --- a/demo/peacock/src/modules/checkout.js +++ b/demo/peacock/src/modules/checkout.js @@ -41,6 +41,7 @@ export const setBillingAddress = createAction('CHECKOUT_SET_BILLING_ADDRESS'); export const toggleShippingModal = createAction('TOGGLE_SHIPPING_MODAL'); export const toggleDeliveryModal = createAction('TOGGLE_DELIVERY_MODAL'); export const togglePaymentModal = createAction('TOGGLE_PAYMENT_MODAL'); +const applePayAvailable = createAction('APPLE_PAY_AVAILABLE'); const markAddressAsDeleted = createAction('CHECKOUT_MARK_ADDRESS_AS_DELETED'); const markAddressAsRestored = createAction( 'CHECKOUT_MARK_ADDRESS_AS_RESTORED', @@ -105,6 +106,38 @@ function addressToPayload(address) { return payload; } +const _checkApplePay = createAsyncActions( + 'checkApplePay', + function() { + const { dispatch } = this; + + return foxApi.applePay.available() + .then((resp) => { + dispatch(applePayAvailable(resp)); + }); + } +); + +export const checkApplePay = _checkApplePay.perform; + +const _beginApplePay = createAsyncActions( + 'beginApplePay', + function(paymentRequest) { + const { dispatch } = this; + + return foxApi.applePay.beginApplePay(paymentRequest) + .then((res) => { + tracking.purchase({ + ...res, + }); + dispatch(orderPlaced(res)); + dispatch(resetCart()); + }); + } +); + +export const beginApplePay = _beginApplePay.perform; + const _removeShippingMethod = createAsyncActions( 'removeShippingMethod', function() { @@ -463,6 +496,7 @@ const initialState: CheckoutState = { shippingModalVisible: false, deliveryModalVisible: false, paymentModalVisible: false, + applePayAvailable: false, }; function sortAddresses(addresses: Array
): Array
{ @@ -503,6 +537,12 @@ const reducer = createReducer({ billingData, }; }, + [applePayAvailable]: (state, available) => { + return { + ...state, + applePayAvailable: available, + }; + }, [shippingMethodsActions.succeeded]: (state, list) => { return { ...state, diff --git a/demo/peacock/src/pages/checkout/checkout.css b/demo/peacock/src/pages/checkout/checkout.css index 9cc23a91a5..8b88287147 100644 --- a/demo/peacock/src/pages/checkout/checkout.css +++ b/demo/peacock/src/pages/checkout/checkout.css @@ -81,6 +81,10 @@ .place-order-button { border-radius: 0; } + + .error-alerts { + margin-left: 15px; + } } @media (--large) { diff --git a/demo/peacock/src/pages/checkout/checkout.jsx b/demo/peacock/src/pages/checkout/checkout.jsx index 5a65be6d4c..5c240c0999 100644 --- a/demo/peacock/src/pages/checkout/checkout.jsx +++ b/demo/peacock/src/pages/checkout/checkout.jsx @@ -10,6 +10,12 @@ import { browserHistory } from 'lib/history'; import * as tracking from 'lib/analytics'; import { emailIsSet, isGuest } from 'paragons/auth'; import classNames from 'classnames'; +import sanitizeLineItems from 'sanitizers/line-items'; + +// actions +import * as actions from 'modules/checkout'; +import { fetch as fetchCart, hideCart } from 'modules/cart'; +import { fetchUser } from 'modules/auth'; // components import Shipping from './shipping/shipping'; @@ -22,20 +28,13 @@ import Loader from 'ui/loader'; import OrderTotals from 'components/order-summary/totals'; import Button from 'ui/buttons'; -// styles -import styles from './checkout.css'; - // types import type { CheckoutState, EditStage } from 'modules/checkout'; import type { CheckoutActions } from './types'; import type { AsyncStatus } from 'types/async-actions'; -// actions -import * as actions from 'modules/checkout'; -import { EditStages } from 'modules/checkout'; -import { fetch as fetchCart, hideCart } from 'modules/cart'; -import { fetchUser } from 'modules/auth'; - +// styles +import styles from './checkout.css'; type Props = CheckoutState & CheckoutActions & { setEditStage: (stage: EditStage) => Object, @@ -119,8 +118,9 @@ class Checkout extends Component { @autobind sanitizeError(error) { - if (error && error.startsWith('Not enough onHand units')) { - return 'Unable to checkout — item is out of stock'; + const sanitizedLineItems = sanitizeLineItems(error, this.props.cart.skus); + if (sanitizedLineItems) { + return sanitizedLineItems; } else if (/is blacklisted/.test(error)) { return 'Your account has been blocked from making purchases on this site'; } @@ -170,7 +170,6 @@ class Checkout extends Component { checkout() { return this.props.checkout() .then(() => { - this.props.setEditStage(EditStages.FINISHED); browserHistory.push('/checkout/done'); }); } @@ -295,6 +294,7 @@ class Checkout extends Component { {this.content} diff --git a/demo/peacock/src/sanitizers/addresses.js b/demo/peacock/src/sanitizers/addresses.js index 4a4db98bf3..0aa4298a80 100644 --- a/demo/peacock/src/sanitizers/addresses.js +++ b/demo/peacock/src/sanitizers/addresses.js @@ -4,6 +4,9 @@ export default function(error: string): string { if (/zip must fully match/.test(error)) { return 'Zip code is invalid'; } + if (/phoneNumber must fully match/.test(error)) { + return 'Phone number is invalid'; + } return error; } diff --git a/demo/peacock/src/sanitizers/line-items.js b/demo/peacock/src/sanitizers/line-items.js new file mode 100644 index 0000000000..db40ff35bc --- /dev/null +++ b/demo/peacock/src/sanitizers/line-items.js @@ -0,0 +1,34 @@ +/* @flow */ +import React from 'react'; +import _ from 'lodash'; + +export default function sanitizeLineItems(error: string, lineItems: Array) { + if (/Following SKUs are out/.test(error)) { + const skus = error.split('.')[0].split(':')[1].split(','); + + const products = _.reduce(skus, (acc, outOfStock) => { + const sku = _.find(lineItems, { sku: outOfStock.trim() }); + if (sku) { + return [ + ...acc, + sku.name, + ]; + } + + return acc; + }, []); + + const singleProduct = products.length === 1; + const title = singleProduct ? 'Product' : 'Products'; + const verb = singleProduct ? 'is' : 'are'; + const pronoun = singleProduct ? 'it' : 'them'; + return ( + + {title} {products.join(', ')} {verb} out of stock. + Please remove {pronoun} to complete the checkout. + + ); + } + + return null; +} diff --git a/demo/peacock/src/ui/css/buttons.css b/demo/peacock/src/ui/css/buttons.css index aaa59e4a27..dd7cda0857 100644 --- a/demo/peacock/src/ui/css/buttons.css +++ b/demo/peacock/src/ui/css/buttons.css @@ -15,13 +15,13 @@ cursor: pointer; &:active { - background: var(--button-active-background); + background-color: var(--button-active-background); } &:focus { /* outline: 4px solid var(--baby-blue); */ box-shadow: 0 0 3pt 2pt var(--baby-blue); - background: var(--button-focus-background); + background-color: var(--button-focus-background); } & > span { @@ -35,7 +35,7 @@ &:disabled { color: var(--button-disabled-color); - background: var(--button-disabled-background); + background-color: var(--button-disabled-background); &:hover { cursor: not-allowed; diff --git a/demo/peacock/yarn.lock b/demo/peacock/yarn.lock index ce084c6710..9019c8cccc 100644 --- a/demo/peacock/yarn.lock +++ b/demo/peacock/yarn.lock @@ -3,8 +3,18 @@ "@foxcomm/api-js@^1.2.0": - version "1.2.0" - resolved "https://npm.foxcommerce.com:4873/@foxcomm%2fapi-js/-/api-js-1.2.0.tgz#f9141d22ac772c494018390350a8c59d94af0f59" + version "1.2.1" + resolved "https://npm.foxcommerce.com:4873/@foxcomm%2fapi-js/-/api-js-1.2.1.tgz#0a099a15a4dffb32ab61d3eefd1ecb78299b088d" + dependencies: + debug "^2.2.0" + jwt-decode "^2.0.1" + lodash "^4.17.4" + postinstall-build "^0.2.1" + superagent "^3.5.2" + +"@foxcomm/api-js@^1.2.3": + version "1.2.3" + resolved "https://npm.foxcommerce.com:4873/@foxcomm%2fapi-js/-/api-js-1.2.3.tgz#734ae943e71162be019f8ad7b3c2e580fa734278" dependencies: debug "^2.2.0" jwt-decode "^2.0.1"