Salesforce B2B / D2C Commerce のチェックアウトのカスタムコンポーネントを作成してみる

※ これから記載する事項は、私が所属する会社とは一切関係のない事柄です。


今回は v59 (Winter ‘24) でチェックアウトのコンポーネントの作り方が少し変わったので紹介したいと思います。（[ヘルプ](https://developer.salesforce.com/docs/atlas.en-us.b2b_b2c_comm_dev.meta/b2b_b2c_comm_dev/b2b_b2c_comm_create_checkout_component.htm)）

:::note warn
- 今回は支払いコンポーネントについては取り扱いません。詳細は[ヘルプ](https://developer.salesforce.com/docs/atlas.en-us.b2b_b2c_comm_dev.meta/b2b_b2c_comm_dev/b2b_b2c_comm_integrate_payment_lwc.htm)をご覧ください。
- 今回は利用できる関数の一部のみを利用しているので、他の関数については[インターフェース](https://developer.salesforce.com/docs/atlas.en-us.b2b_b2c_comm_dev.meta/b2b_b2c_comm_dev/b2b_b2c_comm_checkout_checkoutsavable.htm)をご覧ください。
:::



## 以前と何が変わったのか？

* コンポーネントの階層
    * [v58](https://developer.salesforce.com/docs/atlas.en-us.244.0.b2b_b2c_comm_dev.meta/b2b_b2c_comm_dev/b2b_b2c_comm_checkout_component_hierarchy.htm) → [v59](https://developer.salesforce.com/docs/atlas.en-us.246.0.b2b_b2c_comm_dev.meta/b2b_b2c_comm_dev/b2b_b2c_comm_checkout_component_hierarchy.htm)
* コンポーネントを作る際のインターフェース
    * [v58](https://developer.salesforce.com/docs/atlas.en-us.244.0.b2b_b2c_comm_dev.meta/b2b_b2c_comm_dev/b2b_b2c_comm_checkout_checkoutsavable.htm) → [v59](https://developer.salesforce.com/docs/atlas.en-us.246.0.b2b_b2c_comm_dev.meta/b2b_b2c_comm_dev/b2b_b2c_comm_checkout_checkoutsavable.htm)
* コンポーネントを操作する際のフロー
    * [v58](https://developer.salesforce.com/docs/atlas.en-us.244.0.b2b_b2c_comm_dev.meta/b2b_b2c_comm_dev/b2b_b2c_comm_checkout_component_communication.htm) → [v59](https://developer.salesforce.com/docs/atlas.en-us.246.0.b2b_b2c_comm_dev.meta/b2b_b2c_comm_dev/b2b_b2c_comm_checkout_component_communication.htm)




## 今回作ってみるコンポーネント

今回は二つのコンポーネントを作成したいと思います。

![sample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2110894/87a14e32-7781-e9ea-f88e-211c6aad10f1.gif)


### 1. 入力ボックスからカートオブジェクト（WebCart）のカスタムフィールド（SampleText__c）を編集するコンポーネント（以下、Sample Custom Field）

WebCart オブジェクトに 「**SampleText__c**」 というフィールドを追加して、メールアドレスを値として更新するコンポーネントです。入力した値がメールアドレスの形式じゃないとエラーが発生します。
入力ボックスを利用した際のイベント処理と Apex コントローラを通して値を更新するサンプルです。


### 2. チェックアウトの Data Provider の値を更新して購入完了ボタンの表示・非表示を切り替えるコンポーネント （以下、Sample Hide Place Order）

チェックボックスを用意しチェックをすると右上の「**Place Order**」ボタンが非表示になります。これは実際ではあまりないですが、これを応用して入力状況に応じてボタンを押せないようにするような UI を作成することができます。
またこれは Data Provider の値の変更の仕方のサンプルであり、これを応用すれば `dispatchUpdateAsync()` を利用して配送先やメールアドレスなどの標準データの変更も行うことが可能です。
更新リクエストできる値は[コード内の CheckoutFormRequest](https://git.soma.salesforce.com/communities/ui-commerce-components/blob/246/packages/commerce-components/src/lwc/commerce_unified_checkout/checkoutApiInternal/formStore.ts#L205) をご覧ください。



## ソースコード

### 1. Sample Custom Field

```html:sampleCustomFieldInput.html
<template>
  <template if:false={isSummarized}>
    <lightning-input
      type="email"
      label="テスト"
      onchange={handleInput}
      value={value}
    ></lightning-input>
  </template>
  <template if:true={isSummarized}>
    <label>テスト：{value}</label>
  </template>
</template>
```




```javascript:sampleCustomFieldInput.js
import { CheckoutComponentBase } from 'commerce/checkoutApi';
import { api, wire } from 'lwc';
import { CartSummaryAdapter } from 'commerce/cartApi';
import updateValue from '@salesforce/apex/SampleCustomFieldContoroller.updateValue';
import getValue from '@salesforce/apex/SampleCustomFieldContoroller.getValue';

export default class SampleCustomFieldInput extends CheckoutComponentBase {
    
    isSummarized = true
    value
    _cartId

    async _initialize() {
        try {
            const data = await getValue({ cartId: this._cartId });
            this.value = data.SampleText__c;
            this.dispatchRequestAspect({ summarizable: false, uneditable: false });
        } catch (e) {
            console.error(e);
        }
    }

    @wire(CartSummaryAdapter)
    async cart({ data, error }) {
        if (data) {
            this._cartId = data.cartId;
            this._initialize();
        } else if (error) {
            console.error(error);
        }
    }

    handleInput(event) {
        this.value = event.detail.value;
        this.checkValidity();
    }

    checkValidity() {
        const el = this.template.querySelector('lightning-input');
        el.reportValidity();
        return el.checkValidity();
    }

    // インターフェースの実装
    setAspect(newAspect) {
        this.isSummarized = newAspect.summary;
    }

    // インターフェースの実装
    async stageAction(checkoutStage) {
        switch (checkoutStage) {
            case 'REPORT_VALIDITY_SAVE':
                // セクション完了時に更新
                if (!this.checkValidity()) {
                    return false
                }
                try {
                    await updateValue({ cartId: this._cartId, value: this.value });
                    return true
                } catch (e) {
                    this.dispatchUpdateErrorAsync({
                        groupId: 'TestError',
                        type: '/commerce/errors/checkout-failure',
                        exception: 'Test error occures.',
                    });
                    return false
                }
            default:
                return true;
        }
    }

}
```


```xml:sampleCustomFieldInput.js-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>58.0</apiVersion>
    <isExposed>true</isExposed>
    <masterLabel>Sample Custom Field</masterLabel>
    <targets>
        <target>lightningCommunity__Page</target>
        <target>lightningCommunity__Default</target>
    </targets>
</LightningComponentBundle>
```





```apex:SampleCustomFieldContoroller.cls
public with sharing class SampleCustomFieldContoroller {
  @AuraEnabled(cacheable=false)
  public static WebCart getValue(String cartId) {
    return [SELECT Id, SampleText__c FROM WebCart WHERE Id = :cartId LIMIT 1];
  }

  @AuraEnabled(cacheable=false)
  public static WebCart updateValue(String cartId, String value) {
    WebCart cart = [
      SELECT Id, SampleText__c
      FROM WebCart
      WHERE Id = :cartId
      LIMIT 1
    ];
    cart.SampleText__c = value;
    update cart;
    return cart;
  }
}
```




### 2. Sample Hide Place Order




```html:sampleHidePlaceOrderBtn.html
<template>
    <div>
        <input
            type="checkbox"
            id="testhidecheckoutbtn"
            checked={checked}
            onchange={handleChange}
        ></input>
        <label for="testhidecheckoutbtn">
            <lightning-formatted-rich-text
                value="チェックアウトボタンを隠す"
            ></lightning-formatted-rich-text>
        </label>
    </div>
</template>
```

```javascript:sampleHidePlaceOrderBtn.js
import { CheckoutComponentBase } from 'commerce/checkoutApi';
import { api, track } from 'lwc';

export default class SampleHidePlaceOrderBtn extends CheckoutComponentBase {

    @track
    _checkoutDetails

    @api
    get checkoutDetails() {
        return this._checkoutDetails;
    }

    set checkoutDetails(value) {
        this._checkoutDetails = value;
    }

    get checked() {
        return this._checkoutDetails.display.hidePlaceOrderButton
    }
    
    async handleChange(event) {
        // 入力時に更新
        await this.dispatchUpdateAsync({
            display: {
                hidePlaceOrderButton: event.target.checked,
            },
        });
        this.dispatchCommit();
    }

    // インターフェースの実装
    async stageAction(checkoutStage) {
        switch (checkoutStage) {
            case 'CHECK_VALIDITY_UPDATE':
                // dispatchCommit()　が呼び出された際に「CHECK_VALIDITY_UPDATE」が渡される
                return true;
            case 'REPORT_VALIDITY_SAVE':
                // dispatchCommit()　が呼び出された際に「REPORT_VALIDITY_SAVE」が渡される
                return true;
            default:
                return true;
        }
    }

}
```



```xml:sampleHidePlaceOrderBtn.js-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>58.0</apiVersion>
    <isExposed>true</isExposed>
    <masterLabel>Sample Hide Place Order</masterLabel>
    <targets>
        <target>lightningCommunity__Page</target>
        <target>lightningCommunity__Default</target>
    </targets>
    <targetConfigs>
        <targetConfig targets="lightningCommunity__Default">
            <property name="checkoutDetails" label="Checkout Binding Expression" type="String"
                default="{!Checkout.Details}" />
        </targetConfig>
    </targetConfigs>
</LightningComponentBundle>
```





## ソースコードのポイント



### 共通

* コンポーネントは [CheckoutComponentBase](https://git.soma.salesforce.com/communities/ui-commerce-components/blob/246/packages/commerce-components/src/lwc/commerce_unified_checkout/checkoutOrchestration/checkoutComponentBase.ts) を継承します。CheckoutComponentBase は [CheckoutComponent インターフェース](https://developer.salesforce.com/docs/atlas.en-us.b2b_b2c_comm_dev.meta/b2b_b2c_comm_dev/b2b_b2c_comm_checkout_checkoutsavable.htm)を実装しています。
* 主に CheckoutComponent インターフェースの  `stageAction()` と `setAspect()` 関数を実装します。



### Sample Custom Field

* カートの情報（今回はカート ID）は [CartSummaryAdapter](https://developer.salesforce.com/docs/atlas.en-us.246.0.b2b_b2c_comm_dev.meta/b2b_b2c_comm_dev/b2b_b2c_comm_display_lwc_apis.htm) を `@wire` で呼び出して取得しています。
* `setAspect()` チェックアウトが 1 ページレイアウトではなく、アコーディオンレイアウト（[ヘルプ](https://help.salesforce.com/s/articleView?id=sf.comm_components.htm&type=5)）の時に入力ボックスではなく、値を表示する際に利用します。（下記画像）
    * ![スクリーンショット 2023-10-30 13.50.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2110894/88ccabf4-5454-776f-d280-174d852d3d10.png)

* 入力ボックスのバリデーション（`checkValidity()`）はリアルタイムで実行されます。
* Apex を通した保存（`updateValue()`）は「Place Order」ボタンを押したとき、またはアコーディオンレイアウトの場合はアクションボタンを押したときに発生する `REPORT_VALIDITY_SAVE` アクション時に実行されます。（[アクションの発生タイミング](https://developer.salesforce.com/docs/atlas.en-us.b2b_b2c_comm_dev.meta/b2b_b2c_comm_dev/b2b_b2c_comm_checkout_component_communication.htm)）
* `SampleText__c` フィールドを取得・更新する Apex を作成して、コンポーネント内でコールしています。



### Sample Hide Place Order

* コンポーネントへのデフォルトプロパティとして設定している `{!Checkout.Details}` という変数でチェックアウトの情報を取得しています。このオブジェクトは `@track` で変更をウォッチしています。
* `dispatchUpdateAsync()` で Data Provider の `hidePlaceOrderButton` を編集しています。
* 今回は `hidePlaceOrderButton` を編集しただけですが、[CheckoutFormRequest](https://git.soma.salesforce.com/communities/ui-commerce-components/blob/246/packages/commerce-components/src/lwc/commerce_unified_checkout/checkoutApiInternal/formStore.ts#L205) 内の `defaultDeliveryGroup` などを修正すれば配送先などを修正できます。
* 今回は利用していませんが dispatchCommit を呼び出すと `CHECK_VALIDITY_UPDATE` と `REPORT_VALIDITY_SAVE` アクションが発生します。両方で 「**true**」 を返せば配送先やメールアドレスなどが永続化されます。（[フロー](https://developer.salesforce.com/docs/atlas.en-us.b2b_b2c_comm_dev.meta/b2b_b2c_comm_dev/b2b_b2c_comm_checkout_component_communication.htm)）