[feature]: Proposal for a better checkout design
See original GitHub issueProposal for a better checkout design
After working with an actual project based on PWA Studio, I realized the Checkout page is not adequately prepared for complex international projects. I have been thinking a lot about improving the checkout in the last few days.
I decided to discuss this publicly to get more opinions and develop the PWA Studio developer experience to the next level.
Challenge what I faced in terms of Customer Requirements
-
Gift card and discount can set total to a lower or zero amount
- A lower or zero amount can affect available and possible payment methods
- Some payments allow adding max order total could affect the payment collection
- Basically, we need to make sure customers can only add gift cards or discounts before they select a payment method
-
Customers want to integrate in-context and redirect payment methods.
- We should allow every payment method to replace the default place order button with its workflow. See the technical proposal for more details.
-
Zip and Region are optional based on the selected country
-
Show Telephone, Company, Fax are optional bassed on store config
-
Store Config can disable guest checkout, but checkout can still be reached.
- The able route should be authorized if this config is enabled https://docs.magento.com/user-guide/sales/checkout-guest.html#disable-guest-checkout.
Technical proposal for more flexible flow in-context and redirect payment methods.
In my opinion, three main challenges in checkout place order are based on useEffect
and not well changeable for developers.
Current Workflow
Better would be here to use a changeable state machine pattern with default workflow.
// function to demo new API
const delay = delay => {
var start = new Date().getTime();
while (new Date().getTime() < start + delay);
}
const cartId = "12233";
const fetchCartDetailOrder = (variables)=> {
console.log("fetchCartDetailOrder", variables);
delay(Math.random()*100);
console.log("fetchCartDetailOrder successfully with ", cartId);
}
const placeOrder = async (variables) => {
console.log("placeOrder", variables);
delay(Math.random() * 100);
const { cartId } = variables;
console.log("Order placed successfully with ", cartId);
};
const removeCart = async () => {
delay(Math.random() * 100);
console.log("Remove Cart ");
};
const clearCartDataFromCache = async () => {
delay(Math.random() * 100);
console.log("clearCartDataFromCache");
};
// Current Design
async function placeOrderAndCleanup() {
await placeOrder({ cartId });
await removeCart();
await clearCartDataFromCache();
}
// New Design idea
async function placeOrderStateMaschine(placeOrderWorkFlow) {
Object.keys(placeOrderWorkFlow).forEach((key) => {
placeOrderWorkFlow[key]();
});
}
// schould be returned by useCheckout
const placeOrderWorkFlow = {
fetchCartDetailOrder: () => fetchCartDetailOrder({ cartId }),
placeOrder: () => placeOrder({ cartId }),
// possible to add more workflows action
removeCart,
placeOrderAndCleanup,
};
console.log("----------------- OLD API ----------------");
placeOrderAndCleanup().then(() => {
console.log("----------------- New API ----------------");
placeOrderStateMaschine(placeOrderWorkFlow);
});
API to allow to replace place order Button with a custom
MyCustomPlaceOrderButton
const MyCustomPlaceOrderButton = props => {
const {handlePlaceOrder,isUpdating,placeOrderLoading,orderDetailsLoading} = props;
const PlaceOrderButton = placeOrderButtonCollection[paymentMethod] || null;
return <Button>My Custome Button </Button>
};
placeButtonOrderCollection
const paypal_express = React.lazy(() => import('@my_namespace/components/MyCustomPlaceOrderButton'));
const placeButtonOrderCollection = {
paypal_express
};
PlaceOrderButton
const PlaceOrderButton = props => {
const { paymentMethod, loading } = usePlaceOrderButton();
const classes = useStyle(defaultClasses, props.classes);
const {handlePlaceOrder,isUpdating,placeOrderLoading,orderDetailsLoading} = props;
if (loading) {
return (
<LoadingIndicator>
<FormattedMessage
id={'express.place.loading'}
defaultMessage={'Collect Order Totals...'}
/>
</LoadingIndicator>
);
}
if (placeButtonOrderCollection[paymentMethod]) {
return productDetailPageCollection[paymentMethod];
}
return (
<Button
onClick={handlePlaceOrder}
priority="high"
className={classes.place_order_button}
disabled={isUpdating || placeOrderLoading || orderDetailsLoading}
>
<FormattedMessage
id={'checkoutPage.placeOrder'}
defaultMessage={'Place Order'}
/>
</Button>
);
}
A three-step approach makes complex induce own workflow for redirect and in-context. Better go here with one-step or two-step checkouts like shipment as the first step. The second step could be here, then payment and review merged in one step.
Issue Analytics
- State:
- Created 2 years ago
- Reactions:2
- Comments:10 (8 by maintainers)
@fooman and @larsroettig, we are looking into extending checkout and making shipping step to be removed/replaced to enable virtual products and virtual gift cards like @fooman pointed out. A use case for replacing a shipping step will be in-store pickup. Do you guys have any other use cases in mind for the shipping step extensibility?
I am just going to drop this into this discussion but it may as well be a separate proposal for overhauling just the totals display.
I am currently adding a new total below the subtotal as shown in the below screenshot:
To achieve this the following is required:
In an ideal world there would be an extension point for totals. This would collect the display component and fragment and then does the rest so we would end up with something like this: