Closed filrak closed 4 years ago
First of all, we have to distinguish two things: checkout itself and payment. The checkout is actually a part of eCommerce platform, without that you can't make an order. Is there any platform that doesn't have a checkout? The only thing we delegate externally is a payment system or more (eg. delivery).
Let's start with defining composables and challenge them. I propose the following ones:
useCheckoutCustomer
- for defining personal details of user (person who makes an order)useCheckoutShipping
- for defining shipping details of user (where you want to send the order to), and shipping methodsuseCheckoutPayment
- for defining billing details (data of the user who pays for this), and handling payment processuseCheckoutOrder
- for handling an orderNow, below a few potential issues/concerns that needs to be solved:
useCheckoutPayment
composable most likely you need the data from the previous ones, how to load that? How to prevent over-fetching?useCheckoutAddress
, they are just taking data from the form and saving it to the API, perhaps it will be something more complex further?To sum up. Checkout is essentially the most difficult part to unify. It has a huge variety of possible implementations. I'm even thinking about preparing totally custom composables per eCommerce integration with the implementation of its own checkout and exactly the same with payment systems.
@andrzejewsky this is looking very good - I see that similarly.
For the shared state - I think there is nothing wrong with sharing the state between checkout compostable similarly to what we have with useUser
. In the end they're just kind of "subcomposables" and the connection is implict.
Also some thoughts about your concerns:
billing and shipping are very similar and can be combined into the one composable eg. useCheckoutAddress, they are just taking data from the form and saving it to the API, perhaps it will be something more complex further?
We could do that. The only thing here that matters to me is extensibility. We have to build it in a way that allows to easily integrate any 3rd-party shipping provider.
payment is the most complicated part, most of the payment systems provides also special secured UI that uses iframes to load inputs, so you can skip that part - what about this? Is it mean that along with composable we need to provide a component as well? Or we'll provide just a composable, but the component is the developer's responsibility? - think this is the most reasonable payment hooks - some of the payment systems require backend (eg. braintree) eg. to validate something, do we provide this as well? nuxt (at least as basic one)? confirmations - sometimes an external payment system redirects us somewhere to our app, with some kind of confirmation eg. when payment succeeded. I think with payment integration we should provide also an example of error/success pages that can be replaced by the developer.
Yes, I thought about ti this way: each payment integration needs to fulfill some specific contract (let it be a typescript interface) so it can be passed to the setup config and extend proper parts of the app (hooks as you called them)
Apart from that there should always be a component that will have to be used within VSF App in a checkout that has to be added manually (in the future we can probably add it automatically as well)
we have one composable for payment and billing details, of course it makes sense as it's a related thing, but what if you need to save billing address to the eCommerce platform but payment is managed by something external (that's a real example in CT)?
I don't see how having both as one composable will prevent what you've written about
Discussion outcome:
configureFormDataModel
function that accepts the key
property which is a unique id of a data model to configure (we assume that we can use this pattern for all sophisticated forms in the app) and a configure
function that enables to change the data model.
configureFormDataModel(key: string, configure: (currentDataModel: FormDataModel) => newDataModel: FormDataModel);
formDataModels: Object;
import { configureFormDataModel, formDataModels } from '@vue-storefront/commercetools`
configureFormDataModel(formDataModels.checkout.customer.id, async currentDataModel => { ...currentDataModel, firstName: { type: 'input', validation: ['required'] }, isCompany: { type: 'radio', validation: ['required'], options: ['yes', 'no'] }, country: { type: 'select', options: currentDataModel.countries.options } // we can map some properties values to others })
3. What if we need to load some data lazily? For example if we select a country we have to load it's regions lazily. In that case we can use `onChange` method available for each form field to fetch additional data:
```js
configureFormDataModel(formDataModels.checkout.customer.id, async currentDataModel => {
...currentDataModel,
country: { type: 'select', options: currentDataModel.countries.options, onChange: async (value) => {
currentDataModel.regions = getRegionsFromApi(value.id)
}
})
This part has to be further discussed, especially overall extensibility. For that part we will focus only on payments.
Vue Storefront Next composable should expose extension points (hooks) that can be accessed through a plugin.Extension points will be the same for every integration.
When we want to register a plugin we should use addPlugin
function.
For example we could do this to register PayPal extension
// you specify which type of plugin you're registering
import { addPlugin } from '@vue-storefront/commercetools`
import { PayPalPlugin } from 'paypal-extension'
// registration
addPlugin(PayPalPlugin)
Inside the plugin we will use addPaymentMethod
hook to register new payment method
// inside payment plugin
export PayPalPlugin = {
addPaymentMethod() {
return { ... }
}
}
Discuss notes (interfaces draft):
interface FormRow {}
interface AgnosticForm {
[field: string]: FormRow
}
interface UseCheckoutCustomer <CUSTOMER, LOAD_PARAMS, SAVE_PARAMS> {
async loadCheckoutCustomer: (params: LOAD_PARAMS) => void;
async saveCheckoutCustomer: (params: SAVE_PARAMS) => void;
checkoutCustomer: ComputedProperty<CUSTOMER>;
form: AgnosticForm;
loading: boolean;
error: any;
}
interface UseCheckoutShipping <SHIPPING, LOAD_PARAMS, SAVE_PARAMS> {
async loadCheckoutShipping: (params: LOAD_PARAMS) => void;
async saveCheckoutShipping: (params: SAVE_PARAMS) => void;
checkoutShipping: ComputedProperty<SHIPPING>;
form: AgnosticForm;
loading: boolean;
error: any;
}
interface UseCheckoutPayment <PAYMENT, LOAD_PARAMS, SAVE_PARAMS> {
async loadCheckoutPayment: (params: LOAD_PARAMS) => void;
async saveCheckoutPayment: (params: SAVE_PARAMS) => void;
checkoutPayment: ComputedProperty<PAYMENT>;
form: AgnosticForm;
loading: boolean;
error: any;
}
interface UseCheckoutOrder <ORDER, LOAD_PARAMS, SAVE_PARAMS> {
async loadCheckoutOrder: (params: LOAD_PARAMS) => void;
async saveCheckoutOrder: (params: SAVE_PARAMS) => void;
checkoutOrder: ComputedProperty<ORDER>;
form: AgnosticForm;
loading: boolean;
error: any;
}
const useCheckoutCustomer = (): UseCheckoutCustomer => {}
const useCheckoutShipping = (): UseCheckoutShipping => {}
const useCheckoutPayment = (): UseCheckoutPayment => {}
const useCheckoutOrder = (): UseCheckoutOrder => {}
Each composable are very similar to each other. They have functions to load the data (loadXXX) and saving them (saveXXX). Normally both load and saving functions are loading everything you need to work on the data, but sometimes there is a need to save or load something independently - for that purpose, we'd like to introduce params for these functions:
const { loadCheckoutPayment } = useCheckoutShipping()
loadCheckoutPayment() // loads everything
loadCheckoutPayment({ shippingData: true }) // loads only shipping data
loadCheckoutPayment({ shippingMethids: tue }) // loads only shipping methods
Each composable has a form
field that contains the only data we need to display on the form. There always a set of default form model (specific for the platform), but is extendable as well by using a special mechanism (extend function). Furthermore, a form
field will be used by our form builder directly to render the given fields. The form fields are being built by using a response from the API
const { form, loadCheckoutShipping, saveCheckoutShipping } = useCheckoutShipping()
loadCheckoutShipping() // loads: { firsrName, lastName, totals, price }
// form contains: { firstName, lastName } - only these fields should be in the form
<template>
<FormBuilder :form="form" @save="saveCheckoutShipping" />
<template>
extend('checkout/shipping', async (currentFormModel) => ({
...currentFormModel, // it contains firstName and lastName
saveForLater: { type: 'input', validation: ['required'] } // we are adding a new field
}))
const { form, loadCheckoutShipping, saveCheckoutShipping } = useCheckoutShipping()
loadCheckoutShipping() // loads: { firsrName, lastName, totals, price }
// form contains: { firstName, lastName, saveForLater } - new field appears here
<template>
<FormBuilder :form="form" @save="saveCheckoutShipping" />
<template>
Sometimes we need to integrate with the external payment gateway, which in most cases comes with extending the form as well. In order to do this, again we can use our extending mechanism (similar to the previous example) for adding the payment extension:
extend('checkout/addPaymentMethod', async () => ({ ... })) // adds the new payment method
const { form, saveCheckoutPayment } = useCheckoutPayment();
// form - new payment method is in the form now
saveCheckoutPayment() // calling this we are selecting payment methond along with billing data
In order to better understanding, here is an example of implementation some of the compostables:
const useCheckoutShipping = () => {
const createForm = provide('checkout/extendShippingForm') // that comes from extending mechanism
const checkoutShipping = ref({});
const form = ref({});
const loading = ref(false);
const error = ref(null);
const loadCheckoutShipping = async (params) => {
const resp = await someApiCall();
checkoutShipping.value = resp.data.shipping;
form.value = createForm(resp.data);
}
const saveCheckoutShipping = async (formData) => {
const resp = await someApiCall(formData);
checkoutShipping.value = resp.data.shipping;
form.value = createForm(resp.data);
};
return {
loadCheckoutShipping,
saveCheckoutShipping,
form,
loading,
error
};
}
const useCheckoutPayment = () => {
const createForm = provide('checkout/extendPaymentForm') // that comes from extending mechanism
const createNewPaymentMethod = provide('checkout/addNewPaymentMethod') // that comes from extending mechanism
const checkoutPayment= ref({});
const form = ref({});
const loading = ref(false);
const error = ref(null);
const loadCheckoutPayment = async (params) => {
const resp = await someApiCall();
const newPaymentMethod = await createNewPaymentMethod(resp); // we are adding a new payment method
checkoutPayment.value = resp.data.payment;
checkoutPayment.value.paymentMethods.push(newPaymentMethod);
form.value = createForm(resp.data);
}
const saveCheckoutPayment = async (formData) => {
const resp = await someApiCall(formData);
checkoutPayment.value = resp.data.payment;
form.value = createForm(resp.data);
};
return {
loadCheckoutPayment,
saveCheckoutPayment,
form,
loading,
error
};
}
Regarding External Payments - we will most likely pass an object with a unified payment interface rather than a function, apart from that I think we are close to writing actual code
The interface will be implemented as described, the extendibility mechanism discussions will be continued in the next issue.
Some examples of usage:
interface UseCheckoutCustomer <CUSTOMER, LOAD_PARAMS, SAVE_PARAMS> {
async loadCheckoutCustomer: (params: LOAD_PARAMS) => void;
async saveCheckoutCustomer: (params: SAVE_PARAMS) => void;
checkoutCustomer: ComputedProperty<CUSTOMER>;
form: AgnosticForm;
loading: boolean;
error: any;
}
loadCheckoutCustomer
loads checkoutCustomer
from the server (if its logged in)
async function loadCheckoutCustomer (reaonlyParams: { checkoutCustomer, form }) {
// fetch customer from API
const customer = await getCustomerFromApi();
// return updated customer and form
return { checkoutCustomer: customer }
}
form: AgnosticForm ({ checkoutCustomer }) => {
firstName: { type: 'input', validation: ['required'], value: checkoutCustomer?.firstName ?? '' }
...
}
saveCheckoutCustomer
saved data to checkoutCustomer
checkoutCustomer
(readonly computed) checkout customer data that will be used by useCheckoutOrder
form
- data model for checkout form with default values
What is the motivation for adding / enhancing this feature?
Currently there is no standardized way to create checkout composable and that's the last missing piece of them. Even though for most of the platforms integrated with VSF Next we are using external checkout as a recommended way there are platforms like commercetools that are purely API-driven and doesn't have this option
Most likely we will need to make this in the same way we made
useuser
composable - split it by smaller entities correlating to specific feature/subpage,In that case we will (probably) need an interface for
useCheckout
for placing orders and order reviewuseCheckoutPersonalDetails
useCheckoutShippingDetails
useCheckoutPayment
Above 3 are connected to useCheckoutWhat are the acceptance criteria
TODO After finishing