vuestorefront / vue-storefront

Alokai is a Frontend as a Service solution that simplifies composable commerce. It connects all the technologies needed to build and deploy fast & scalable ecommerce frontends. It guides merchants to deliver exceptional customer experiences quickly and easily.
https://www.alokai.com
MIT License
10.59k stars 2.08k forks source link

[Checkout] feature components #5502

Closed andrzejewsky closed 3 years ago

andrzejewsky commented 3 years ago

As some of the parts in VSF are really complicated and most of the logic belongs to the components, it's better to provide entire components with the ability for customizations instead of just composables.

Considering payment - we have a lot of variety of implementing such a thing, and each depends on the payment provider. Clearly, most of logic is hidden inside the components, while the composables are handling only a small piece of it.

To solve that issue we can introduce feature components, or agnostic (to some extend) components that are handling a implementation of this specific case, just based on the platform we integrate with.

We can assume that kind of component should take some input props (that comes from integration) and emits some predefined events that are mandatory to be consistent with a platform.

Example:

<template>
  <div>
    <input name="firstName" />
    <input name="lastName" />

    <PaymentProvider
      :data="shipping"
      @initialized="handleInitialize"
      @loaded="handleLoaded"
      @payment:beforePay="handleBeforePay"
      @payment:afterPay="handleAfterPay"
      @methods:loaded="handleMethodsLoaded"
      @methods:selected="handleSelectMethod"
      @methods:changed="handleChangeMethod"
    >
      <tepmlate #method="{ id, name, price }">
        <div>render single payment method</div> 
      </template>

      <tepmlate #methods="{ methods }">
        <div>render entire methods container</div> 
      </template>

      <tepmlate #applePay="{ id, name, apple }">
        <div>render specific method</div> 
      </template>

      <tepmlate #error="{ message }">
        <div>render error</div> 
      </template>

      <tepmlate #submit="{ next }">
        <div>render submit button</div> 
      </template>
    </PaymentProvider
  </div>
</template>

<script>
import { PaymentProvider } from '@vue-storefront/checkout-com'
import { useShipping, useOrder } from '@vue-storefront/commercetools'

export default {
  components: {
    PaymentProvider
  },
  setup(props, ctx) {
    const { shipping } = useShipping();
    const { placeOrder } = useOrder()

    // called on initialization, we can change configuration if it's needed
    const handleInitialize = (config) => {
      return config
    }

    // called once payment sdk is loaded
    const handleLoaded = (sdk) => {

    }

    // returns arguments to the `pay` inside of PaymentProvider
    const handleBeforePay = async (currentArgs) => {
      const { order } =  await placeOrder()

      return { ...curentArgs, order }
    }

    // called after payment request has been sent, we can react on response
    const handleAfterPay = async (paymentResponse) => {
      ctx.root.$router.redirect('/checkout/next-step')
      // or:
      ctx.root.$router.redirect(paymentResponse.redirectUrl)
    }

    // called when payment methods are loaded
    const handleMethodsLoaded = async () => {

    }

    // called when user pick payment method
    const handleSelectMethod = async (method) => {

    }

    // called when user configure method. eg. fill card details
    const handleChangeMethod = async (method) => {

    }

    return {
      shipping,
      handleInitialize,
      handleLoaded,
      handleBeforePay,
      handleAfterPay,
      handleMethodsLoaded,
      handleSelectMethod,
      handleChangeMethod
    }
  }
}

</script>

We have the following parts:

Utilities

import { createStripe } from '@vue-storefront/stripe'

const sdk = createStripe({ ... })

sdk.initForm()

const methods = sdk.loadMethods()

const selected = sdk.selectMethod({ method: {} })

const paymentResp = sdk.makePayment()

But based on integration the flow can be different which comes with different functions:

import { createCheckoutCom } from '@vue-storefront/checkout-com'

const sdk = createCheckoutCom({ ... })

const form = sdk.loadForm()

onMouted(() => {
  form.Init();
})

const paymentResp = sdk.makeCardPayment({ cardToken: '' })

const paymentResp = sdk.makeApplePayPayment()
andrzejewsky commented 3 years ago

Meeting nodes:

filrak commented 3 years ago

use enums for event types

Fifciu commented 3 years ago

We will share const like:

const PaymentProviderEvents = {
  BEFORE_PAY: 'payment:beforePay',
  AFTER_PAY: 'payment:afterPay,
  BEFORE_LOAD: 'methods:beforeLoad',
  AFTER_LOAD: 'methods:afterLoad',
  SELECTED: 'methods:selectedDetailsChanged',
  CHANGED: 'methods:selectedDetailsChanged'
}

Then developer will just use

import { PaymentProviderEvents } from '...';

// ...
const onLoaded = () => {
  context.emit(PaymentProviderEvents.LOADED)
}
Fifciu commented 3 years ago

I imagine component will be mounted only after selection: image

VsfShippingProviders.vue

<template>
  <div>
    <VsfShippingProvider 
      v-for="shippingProvider in shippingProviders"
    >
      <template #selected>
        <component
          :key="shippingProvider.id"
          :is="shippingProviderToComponentName(shippingProvider)"
          :data="shippingProvider"
          :finished.sync="isShippingFinished"

          :beforeLoad="config => ({...config, overwritenSmth: 12})"
          @afterLoad="afterLoaded"
          @method:selected="afterSelectedMethod"
          @method:selectedDetailsChanged="afterSelectedDetailsChanged"
        />
      </template>
    </VsfShippingProvider>
    <GoToBillingButton :disabled="!isShippingFinished" />
  </div>
</template>

<script>
export default {
  components: shippingProviderComponents,
  setup () {
    const isShippingFinished = ref(false);
    const onChangeShippingProvider = () => {
      isShippingFinished.value = false;
    }

    return {
      isShippingFinished
    }
  }
}
</script>

Each shipping provider:

Hooks:

// shippingProviderToComponentName.ts
import { upsShippingProviderMapper } from 'ups';
// upsShippingProviderMapper = {
//   componentName: 'UpsShippingProvider',
//   checker: (shippingMethod: SHIPPING_METHOD) => Boolean
// }
import { dhlShippingProviderMapper } from 'dhl';
import { pdpShippingProviderMapper } from 'pdp';
import { inpostShippingProviderMapper } from 'inpost';
import { plainShippingProviderMapper } from '@vue-storefront/commercetools';

const providersMappers = [
  upsShippingProviderMapper,
  dhlShippingProviderMapper,
  pdpShippingProviderMapper,
  inpostShippingProviderMapper,
  plainShippingProviderMapper
];

export const shippingProviderComponents = {
  UpsShippingProviderComponent: () => import('ups/component'),
  DhlShippingProviderComponent: () => import('dhl/component'),
  PdpShippingProviderComponent: () => import('pdp/component'),
  InpostShippingProviderComponent: () => import('inpost/component'),
  PlainShippingProviderComponent: () => import('@vue-storefront/commercetools/component'),
};

export const shippingProviderToComponentName = (shippingProvider: SHIPPING_PROVIDER) => {
  const providerMapper = providersMappers.find(providersMapper => providersMapper.checker(shippingProvider));
  if(!providerMapper) {
    throw new Error(`No matching component for ${shippingProvider.name} shipping method`);
  }
  return providerMapper.componentName;
}
Fifciu commented 3 years ago

Simple draft o shippingProvider

<template>
  <ShippingProvider
    :finished.sync="isShippingFinished"
    :methods:beforeLoad="beforeLoad"
    @methods:afterLoad="onAfterLoad"
    @methods:selected="handleSelectMethod"
    @methods:selectedDetailsChanged="handleChangeMethod"
    @error="onError"
  >
    <template #default="{ name, price }">
      Method {{ name }}, price: {{ price }}
    </template>
  </ShippingProvider>
</template>
methods:beforeLoad handler's signature: ()
methods:afterLoad handler's signature: ({ shippingMethods: SHIPPING_METHODS_RESPONSE })
methods:selected handler's signature: ({ shippingMethod: SHIPPING_METHOD })
bloodf commented 3 years ago

This issue has been closed due to inactivity.

If you want to re-open the issue, please re-create the issue, and post newer information on this problem.