Composable Storefront で Stripe を利用して簡易な決済を実装してみる - 実装編

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

今回は「[Composable Storefront で Stripe を利用して簡易な決済を実装してみる - 内容編](https://qiita.com/syunyama/items/16e11bf2715f64dd24b9)」の続編で実装編としてどのように Composable Storefront に簡易な決済を Stripe で実装したかを紹介します。


## 実装したフロー

![Stripe × Composable (1) (1).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2110894/b517e02f-9ea3-9556-2860-badcea941223.jpeg)

## 実装したサンプルコード

https://github.com/syunyama/sample_stripe

## PWA Kit に実装した内容

今回は [PWA Kit の拡張テンプレート](https://qiita.com/syunyama/items/b97057c4d2e5073c6eee)を利用しています。拡張のために追加したコードは Github で確認できます。

https://github.com/syunyama/sample_stripe/compare/8702ac1..1ce4c40?diff=split&w=

PWA Kit (Composable Storefront) でポイントとなる実装は下記の点です。

### ポイント 1 - React Stripe.js のインストールと利用

下記コマンドでパッケージをインストールします。詳細は「[React Stripe.js リファレンス](https://stripe.com/docs/stripe-js/react?locale=ja-JP)」を参照してください。

```sh
npm install --save @stripe/react-stripe-js @stripe/stripe-js
```

実際に利用している部分は[こちら](https://github.com/syunyama/sample_stripe/blob/1ce4c403204b0b7c7e30f2dda68e238f1cf47720/pwa-kit/sample-stripe/overrides/app/pages/checkout/index.jsx#L185)です。

```jsx:pwa-kit/sample-stripe/overrides/app/pages/checkout/index.jsx
==OMITTED==
const [stripePromise, setStripePromise] = useState(null)
useEffect(() => {
    setStripePromise(
        loadStripe(
            '{Your API key}'
        )
    )
}, [])

==OMITTED==

return (
    <CheckoutProvider>
        <Elements stripe={stripePromise}>
            <Checkout />
        </Elements>
    </CheckoutProvider>
)
==OMITTED==
```





### ポイント 2 - Create Stripe payment method

チェックアウトの確認画面に遷移する際に 呼ばれる `onPaymentSubmit` 内で [stripe.createPaymentMethod を呼んで Stripe の Payment Method を作成](https://github.com/syunyama/sf_pwakit_sample_stripe/blob/1ce4c403204b0b7c7e30f2dda68e238f1cf47720/pwa-kit/sample-stripe/overrides/app/pages/checkout/partials/payment.jsx#L86)しています。 

```jsx:pwa-kit/sample-stripe/overrides/app/pages/checkout/partials/payment.jsx
==OMITTED==
const result = await stripe.createPaymentMethod({
    type: 'card',
    card: elements.getElement(CardElement)
})
if (result.error) {
    showToast({
        title: result.error.message,
        status: 'error'
    })
    return
}
==OMITTED==
```

### ポイント 3 - Add CC payment instrument for basket with Stripe payment method id

上記で作成された Payment Method ID は `c_stripePyamentMethodId` というプロパティで [B2C Commerce の Payment Instrument に追加](https://github.com/syunyama/sf_pwakit_sample_stripe/blob/1ce4c403204b0b7c7e30f2dda68e238f1cf47720/pwa-kit/sample-stripe/overrides/app/pages/checkout/partials/payment.jsx#L100)しています。実際に保存している部分は後に紹介する Hooks 部分です。
API 呼び出し時に Stripe の Payment Intent の情報や保存するか否かの情報が返却されますがその情報は State として [stripeObj](https://github.com/syunyama/sf_pwakit_sample_stripe/blob/1ce4c403204b0b7c7e30f2dda68e238f1cf47720/pwa-kit/sample-stripe/overrides/app/pages/checkout/index.jsx#L43) に[保存](https://github.com/syunyama/sf_pwakit_sample_stripe/blob/1ce4c403204b0b7c7e30f2dda68e238f1cf47720/pwa-kit/sample-stripe/overrides/app/pages/checkout/partials/payment.jsx#L116)しています。また[画面が再度リロードされた際に保存する部分](https://github.com/syunyama/sf_pwakit_sample_stripe/blob/1ce4c403204b0b7c7e30f2dda68e238f1cf47720/pwa-kit/sample-stripe/overrides/app/pages/checkout/partials/payment.jsx#L60)も作成しています。

```jsx:pwa-kit/sample-stripe/overrides/app/pages/checkout/partials/payment.jsx
==OMITTED==
const paymentMethod = result.paymentMethod
const paymentInstrument = {
    paymentMethodId: 'STRIPE',
    c_stripePyamentMethodId: paymentMethod.id,
    c_willSaved: stripeObj.willSaved,
    paymentCard: {
        maskedNumber: `*********${paymentMethod.card.last4}`,
        cardType: getPaymentInstrumentCardType(paymentMethod.card.brand),
        expirationMonth: parseInt(paymentMethod.card.exp_month),
        expirationYear: parseInt(paymentMethod.card.exp_year)
    }
}
const basketResult = await addPaymentInstrumentToBasket({
    parameters: {basketId: basket?.basketId},
    body: paymentInstrument
})
const instrument = basketResult.paymentInstruments.find(
    (item) => item.paymentMethodId === 'STRIPE'
)
setStripeObj({
    stripeIntentId: basket.c_stripeIntentId,
    stripeClientSecret: basket.c_stripeClientSecret,
    stripePyamentMethodId: instrument.c_stripePyamentMethodId,
    willSaved: instrument.c_willSaved
})
==OMITTED==
```

:::note warn
 `setStripeObj` は [React コンテクスト](https://ja.react.dev/learn/passing-data-deeply-with-context)を利用したアプリケーション全体的に統一感があると思いますが、今回はわかりやすさを重視してコンポーネントのプロパティとして渡しています。
:::

### ポイント 4 - Confirm Stripe payment intent

作成された 上記で作成された Payment Intent を元に認証を行うために注文を確定されるときに呼び出される `submitOrder` 関数内で [stripe.confirmCardPayment を呼び出して](https://github.com/syunyama/sf_pwakit_sample_stripe/blob/1ce4c403204b0b7c7e30f2dda68e238f1cf47720/pwa-kit/sample-stripe/overrides/app/pages/checkout/index.jsx#L63)います。

```jsx:pwa-kit/sample-stripe/overrides/app/pages/checkout/index.jsx
==OMITTED==
const result = await stripe.confirmCardPayment(stripeObj.stripeClientSecret, options)
if (result.error) {
    throw new Error(result.error.message)
}
==OMITTED==
```

### ポイント 5 - CSP の設定

`js.stripe.com` からの JS の読み込みや iframe の読み込みを許可します。実際に利用している部分は[こちら](https://github.com/syunyama/sf_pwakit_sample_stripe/blob/1ce4c403204b0b7c7e30f2dda68e238f1cf47720/pwa-kit/sample-stripe/overrides/app/ssr.js#L43)です。

```js:pwa-kit/sample-stripe/overrides/app/ssr.js
==OMITTED==
app.use(
  helmet({
    contentSecurityPolicy: {
      useDefaults: true,
      directives: {
        "img-src": [
          // Default source for product images - replace with your CDN
          "*.commercecloud.salesforce.com",
        ],
        "script-src": [
          // Used by the service worker in /worker/main.js
          "storage.googleapis.com",
          /****** Sample Stripe Added Start ******/
          "js.stripe.com",
          /****** Sample Stripe Added End ******/
        ],
        "connect-src": [
          // Connect to Einstein APIs
          "api.cquotient.com",
        ],
        /****** Sample Stripe Added Start ******/
        "frame-src": ["js.stripe.com"],
        /****** Sample Stripe Added End ******/
      },
    },
  })
);
==OMITTED==
```



## SCAPI Hooks に実装した内容 

[SCAPI Hooks](https://qiita.com/syunyama/items/7cea968880505ec303db#scapiocapi-hooks) では [addPaymentInstrumentToBasket](https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=addPaymentInstrumentToBasket) が呼ばれた際に下記の実装を行なっています。詳細は [Github のソースコード](https://github.com/syunyama/sf_pwakit_sample_stripe/blob/main/cartridges/sample_custom_stripe/cartridge/scripts/hooks/basketPaymentHooks.js)をご覧ください。

* B2C Commerce の顧客と紐づく Stripe の Customer がまだない場合の Customer の作成
* `c_stripePyamentMethodId` パラメータを利用した Payment Intent の作成


:::note
他にも[サンプルとなる Hooks](https://github.com/SalesforceCommerceCloud/sfcc-hooks-collection/tree/main/commerce-cloud-code/plugin_hooktacular/cartridge/scripts) が公開されているので参考にしてみてください。
:::

## B2C Commerce の設定

[マイグレーションファイル](https://help.salesforce.com/s/articleView?id=cc.b2c_site_import_export.htm&type=5)を利用して下記の B2C Commerce の設定を行なっています。


### 設定 1 - システムオブジェクトへのカスタムフィールドの追加

[ソースコード](https://github.com/syunyama/sf_pwakit_sample_stripe/blob/main/migrations/sample_custom_stripe/meta/system-objecttype-extensions.xml)

* Profile オブジェクトに `stripeCustomerId` フィールドの追加
* OrderPaymentInstrument オブジェクトに `stripeIntentId` , `stripeClientSecret` , `stripePaymentMethodId` , `willSaved` フィールドの追加
* Basket オブジェクトに `stripeIntentId` , `stripeClientSecret`  フィールドの追加

### 設定 2 - 支払い方法とプロセッサー

[ソースコード](https://github.com/syunyama/sf_pwakit_sample_stripe/blob/main/migrations/sample_custom_stripe/sites/RefArch/payment-methods.xml)

* `STRIPE` という ID を持つ支払い方法を設定しています。

### 設定 3 - サービス

[ソースコード](https://github.com/syunyama/sf_pwakit_sample_stripe/blob/main/migrations/sample_custom_stripe/services.xml)

* Stripe の REST API を利用するためのサービスを作成しています。


:::note
 マイグレーションには以前紹介した b2c-tools が便利です。「[Salesforce B2C Commerce で利用できる b2c-tools について](https://qiita.com/syunyama/items/bf0071e0eccbbceb2730)」をご参照ください。
:::


## 実際に利用する際の注意

* [PWA Kit](https://github.com/syunyama/sf_pwakit_sample_stripe/blob/1ce4c403204b0b7c7e30f2dda68e238f1cf47720/pwa-kit/sample-stripe/overrides/app/pages/checkout/index.jsx#L189) の `{Your API key}` の値を [Stripe の API Key (公開可能キー)](https://stripe.com/docs/keys?locale=ja-JP#obtain-api-keys) に置き換えてください。 
* [サービスのユーザ ID](https://github.com/syunyama/sf_pwakit_sample_stripe/blob/main/migrations/sample_custom_stripe/services.xml) の `{Stripe API Key}` を [Stripe の API Key (シークレットキー)](https://stripe.com/docs/keys?locale=ja-JP#obtain-api-keys) に置き換える必要があります。 
* [PWA Kit の config フォルダ](https://github.com/syunyama/sf_pwakit_sample_stripe/tree/main/pwa-kit/sample-stripe/config)に [default.js](https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/configuration-options.html) を置いていないのでご注意ください。
* 買い物かご削除・更新時の Stripe Payment Intent の削除・更新はしていません。（価格の更新に関しては [Calculate Hook](https://developer.salesforce.com/docs/commerce/commerce-api/guide/extensibility_via_hooks.html#calculate-hook) を利用して、削除の場合は `dw.ocapi.shop.basket.beforeDELETE` Hook を利用すればいいかと思います。 ）