Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Checkout These Talons! #1775

Merged
merged 11 commits into from Sep 30, 2019
38 changes: 38 additions & 0 deletions packages/peregrine/lib/talons/Checkout/Receipt/useReceipt.js
@@ -0,0 +1,38 @@
import { useCallback, useEffect, useRef } from 'react';
import { useCheckoutContext } from '@magento/peregrine/lib/context/checkout';
import { useUserContext } from '@magento/peregrine/lib/context/user';
import { useAppContext } from '@magento/peregrine/lib/context/app';

export const useReceipt = props => {
// TODO replace with useHistory from Router 5.1
Copy link
Contributor

Choose a reason for hiding this comment

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

😄

const { history, onClose } = props;

const [{ drawer }] = useAppContext();
const [, { createAccount, resetReceipt }] = useCheckoutContext();
const [{ isSignedIn }] = useUserContext();

// When the drawer is closed reset the state of the receipt. We use a ref
// because drawer can change if the mask is clicked. Mask updates drawer.
const prevDrawer = useRef(null);
useEffect(() => {
if (prevDrawer.current === 'cart' && drawer !== 'cart') {
resetReceipt();
onClose();
}
prevDrawer.current = drawer;
}, [drawer, onClose, resetReceipt]);

const handleCreateAccount = useCallback(() => {
createAccount(history);
}, [createAccount, history]);

const handleViewOrderDetails = useCallback(() => {
// TODO: Implement/connect/redirect to order details page.
}, []);

return {
handleCreateAccount,
handleViewOrderDetails,
isSignedIn
};
};
25 changes: 25 additions & 0 deletions packages/peregrine/lib/talons/Checkout/useAddressForm.js
@@ -0,0 +1,25 @@
import { useMemo } from 'react';

/**
* Returns values used to render an AddressForm component.
* @param {Object} props
* @param {Object[]} props.fields an array of fields to reduce over for initial values
* @param {Object} props.initialValues Object containing some initial values from state
* @returns {Object} initialValues a map of form fields and corresponding initial values.
*/
export const useAddressForm = props => {
const { fields, initialValues } = props;

const values = useMemo(
() =>
fields.reduce((acc, key) => {
acc[key] = initialValues[key];
return acc;
}, {}),
[fields, initialValues]
);

return {
initialValues: values
};
sirugh marked this conversation as resolved.
Show resolved Hide resolved
};
59 changes: 59 additions & 0 deletions packages/peregrine/lib/talons/Checkout/useEditableForm.js
@@ -0,0 +1,59 @@
import { useCallback } from 'react';

export const useEditableForm = props => {
const {
editing,
isSubmitting,
setEditing,
shippingAddressError,
submitPaymentMethodAndBillingAddress,
submitShippingAddress,
submitShippingMethod,
checkout: { countries }
} = props;

const handleCancel = useCallback(() => {
setEditing(null);
}, [setEditing]);

const handleSubmitAddressForm = useCallback(
async formValues => {
await submitShippingAddress({
formValues
});
setEditing(null);
},
[setEditing, submitShippingAddress]
);

const handleSubmitPaymentsForm = useCallback(
async formValues => {
await submitPaymentMethodAndBillingAddress({
formValues
});
setEditing(null);
},
[setEditing, submitPaymentMethodAndBillingAddress]
);

const handleSubmitShippingForm = useCallback(
async formValues => {
await submitShippingMethod({
formValues
});
setEditing(null);
},
[setEditing, submitShippingMethod]
);

return {
countries,
editing,
handleCancel,
handleSubmitAddressForm,
handleSubmitPaymentsForm,
handleSubmitShippingForm,
isSubmitting,
shippingAddressError
};
};
79 changes: 79 additions & 0 deletions packages/peregrine/lib/talons/Checkout/useFlow.js
@@ -0,0 +1,79 @@
import { useCallback } from 'react';
import { useCartContext } from '@magento/peregrine/lib/context/cart';
import { useCheckoutContext } from '@magento/peregrine/lib/context/checkout';
import isObjectEmpty from '../../util/isObjectEmpty';

const isCheckoutReady = checkout => {
const {
billingAddress,
paymentData,
shippingAddress,
shippingMethod
} = checkout;

const objectsHaveData = [
billingAddress,
paymentData,
shippingAddress
].every(data => {
return !!data && !isObjectEmpty(data);
});

const stringsHaveData = !!shippingMethod && shippingMethod.length > 0;

return objectsHaveData && stringsHaveData;
};

export const useFlow = props => {
const [cartState] = useCartContext();
const [
checkoutState,
{
beginCheckout,
cancelCheckout,
submitOrder,
submitPaymentMethodAndBillingAddress,
submitShippingAddress,
submitShippingMethod
}
] = useCheckoutContext();
const { onSubmitError, step, setStep } = props;

const handleBeginCheckout = useCallback(async () => {
await beginCheckout();
setStep('form');
}, [beginCheckout, setStep]);

const handleCancelCheckout = useCallback(async () => {
await cancelCheckout();
setStep('cart');
}, [cancelCheckout, setStep]);

const handleSubmitOrder = useCallback(async () => {
try {
await submitOrder();
setStep('receipt');
} catch (e) {
onSubmitError(e);
}
}, [onSubmitError, setStep, submitOrder]);

const handleCloseReceipt = useCallback(() => {
setStep('cart');
}, [setStep]);

return {
cartState,
checkoutDisabled: checkoutState.isSubmitting || cartState.isEmpty,
checkoutState,
isReady: isCheckoutReady(checkoutState),
Copy link
Contributor

Choose a reason for hiding this comment

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

Perfect. 👍

submitPaymentMethodAndBillingAddress,
submitShippingAddress,
submitShippingMethod,
handleBeginCheckout,
handleCancelCheckout,
handleCloseReceipt,
handleSubmitOrder,
step
};
sirugh marked this conversation as resolved.
Show resolved Hide resolved
};
40 changes: 40 additions & 0 deletions packages/peregrine/lib/talons/Checkout/useOverview.js
@@ -0,0 +1,40 @@
import { useCallback } from 'react';

/**
* Returns props to render an Overview component.
*
* @param {Object} props.cart cart state object
* @param {boolean} props.isSubmitting is the form already submitting
* @param {boolean} props.ready is the form ready to submit
* @param {function} props.setEditing set editing state object
*/
export const useOverview = props => {
const { cart, isSubmitting, ready, setEditing } = props;

const handleAddressFormClick = useCallback(() => {
setEditing('address');
}, [setEditing]);

const handlePaymentFormClick = useCallback(() => {
setEditing('paymentMethod');
}, [setEditing]);

const handleShippingFormClick = useCallback(() => {
setEditing('shippingMethod');
}, [setEditing]);

const currencyCode =
(cart && cart.totals && cart.totals.quote_currency_code) || 'USD';
const numItems = (cart && cart.details && cart.details.items_qty) || 0;
const subtotal = (cart && cart.totals && cart.totals.subtotal) || 0;

return {
currencyCode,
handleAddressFormClick,
handlePaymentFormClick,
handleShippingFormClick,
isSubmitDisabled: isSubmitting || !ready,
numItems,
subtotal
};
};
47 changes: 47 additions & 0 deletions packages/peregrine/lib/talons/Checkout/usePaymentsForm.js
@@ -0,0 +1,47 @@
import { useCallback, useState } from 'react';
import isObjectEmpty from '../../util/isObjectEmpty';

const DEFAULT_FORM_VALUES = {
addresses_same: true
};

/**
* Returns props necessary to render a PaymentsForm component.
*
* @param {Object} props.initialValues initial values from state
*/
export const usePaymentsForm = props => {
const { initialValues } = props;
const [isSubmitting, setIsSubmitting] = useState(false);

const handleSubmit = useCallback(() => {
setIsSubmitting(true);
}, [setIsSubmitting]);

let initialFormValues;
if (isObjectEmpty(initialValues)) {
initialFormValues = DEFAULT_FORM_VALUES;
} else {
if (initialValues.sameAsShippingAddress) {
// If the addresses are the same, don't populate any fields
// other than the checkbox with an initial value.
initialFormValues = {
addresses_same: true
};
} else {
// The addresses are not the same, populate the other fields.
initialFormValues = {
addresses_same: false,
...initialValues
};
delete initialFormValues.sameAsShippingAddress;
}
}

return {
handleSubmit,
initialValues: initialFormValues,
isSubmitting,
setIsSubmitting
};
};
88 changes: 88 additions & 0 deletions packages/peregrine/lib/talons/Checkout/usePaymentsFormItems.js
@@ -0,0 +1,88 @@
import { useCallback, useEffect, useRef, useState } from 'react';
// TODO install informed?
sirugh marked this conversation as resolved.
Show resolved Hide resolved
import { useFormState } from 'informed';

/**
*
* @param {boolean} props.isSubmitting whether or not the payment form items are
* @param {function} props.setIsSubmitting callback for setting submitting state
* @param {function} props.onSubmit submit callback
*/
export const usePaymentsFormItems = props => {
const [isReady, setIsReady] = useState(false);

const { isSubmitting, setIsSubmitting, onSubmit } = props;

// Currently form state toggles dirty from false to true because of how
// informed is implemented. This effectively causes this child components
// to re-render multiple times. Keep tabs on the following issue:
// https://github.com/joepuzzo/informed/issues/138
// If they resolve it or we move away from informed we can probably get some
// extra performance.
const formState = useFormState();
const anchorRef = useRef(null);
const addressDiffers = formState.values.addresses_same === false;

const handleError = useCallback(() => {
setIsSubmitting(false);
}, [setIsSubmitting]);

// The success callback. Unfortunately since form state is created first and
// then modified when using initialValues any component who uses this
// callback will be rendered multiple times on first render. See above
// comments for more info.
const handleSuccess = useCallback(
value => {
setIsSubmitting(false);
const sameAsShippingAddress = formState.values['addresses_same'];
let billingAddress;
if (!sameAsShippingAddress) {
billingAddress = {
city: formState.values['city'],
email: formState.values['email'],
firstname: formState.values['firstname'],
lastname: formState.values['lastname'],
postcode: formState.values['postcode'],
region_code: formState.values['region_code'],
street: formState.values['street'],
telephone: formState.values['telephone']
};
} else {
billingAddress = {
sameAsShippingAddress
};
}
onSubmit({
billingAddress,
paymentMethod: {
code: 'braintree',
data: value
}
});
},
[formState.values, setIsSubmitting, onSubmit]
);

// When the address checkbox is unchecked, additional fields are rendered.
// This causes the form to grow, and potentially to overflow, so the new
// fields may go unnoticed. To reveal them, we scroll them into view.
useEffect(() => {
if (addressDiffers) {
const { current: element } = anchorRef;

if (element instanceof HTMLElement) {
element.scrollIntoView({ behavior: 'smooth' });
}
}
}, [addressDiffers]);
sirugh marked this conversation as resolved.
Show resolved Hide resolved

return {
addressDiffers,
anchorRef,
handleError,
handleSuccess,
isDisabled: !isReady || isSubmitting,
isSubmitting,
setIsReady
};
};