Stripe Appsで、Stripeの顧客・決済情報を更新するフォームを作成する方法



この記事は、[Stripe Apps を25日間紹介し続ける Advent Calendar 2022](https://qiita.com/advent-calendar/2022/stripe-apps) 9日目の記事です。

![スクリーンショット 2022-11-24 17.56.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/e0261865-0418-daa2-423a-5f4d70558b1b.png)

ここまでの記事で、「UIコンポーネントの配置方法」「フォーム入力などのイベントのハンドル方法」「Stripe APIの呼び出し方法」を紹介しました。

https://qiita.com/hideokamoto/items/5b025e95c704d3070e88

https://qiita.com/hideokamoto/items/294a047241b09777b6eb

https://qiita.com/hideokamoto/items/f5354f5b9ad686e42319

今回は、この３つを組み合わせて、アプリに顧客情報を更新するフォームを構築してみましょう。

## Stripe Appsアプリで、フォームを開発する方法

簡単なサンプルとして、「テストケースに応じた、テスト用クレジットカードを登録する」アプリを作りましょう。

ドキュメントに紹介されている、テスト用のクレジットカード番号を利用して、「特定のテストケースのためのカード情報」をワンクリックで設定できるようにします。

https://stripe.com/docs/testing

:::note warn
テスト用カード番号を利用するため、本番環境ではご利用いただけません。
:::

### Step1: 利用するStripe APIの権限を設定する

まずは利用するAPIの権限を設定しましょう。

ここでは、顧客情報の読み込み・更新と決済方法の追加を行います。

そのため、Stripe CLIでそれぞれの権限を設定しましょう。

```bash
$ stripe apps grant permission customer_write "for demo application"
$ stripe apps grant permission payment_method_write  "for demo application"
```

それぞれの権限名は、ドキュメントからご確認ください。

https://stripe.com/docs/stripe-apps/reference/permissions#object

### Step2: フォームのUIを実装する

続いてテストケースに対応したカード情報を入力するフォームを作りましょう。

`src/views/App.tsx`に以下のコードを追加します。

```tsx
import { Box, ContextView, FormFieldGroup, Radio } from "@stripe/ui-extension-sdk/ui"
import type { ExtensionContextValue } from "@stripe/ui-extension-sdk/context"
import BrandIcon from "./brand_icon.svg"
import { FC } from 'react'

type TestCard = {
  label: string;
  cardNumber: string;
}

/**
 * @see https://stripe.com/docs/testing
 **/
const testCardTypes: Array<TestCard> = [{
  label: "VISA",
  cardNumber: "4242424242424242"
}, {
  label: "disputes",
  cardNumber: "4000000000002685"
}, {
  label: "Declied card",
  cardNumber: "4000000000000341"
}]

const TestCardForm:FC<{ customerId?: string }> = ({ customerId }) => {
  if (!customerId) return null
  return (
    <Box css={{
      gap: 'medium',
      stack: 'y',
    }}>
      <FormFieldGroup
        legend="Testing card type"
        layout="column"
      >
        {testCardTypes.map(card => (
          <Radio
            key={card.cardNumber}
            name="cardType"
            label={card.label}
            onChange={(e) => {
              console.log(e.target.checked)
            }}
          />
        ))}
      </FormFieldGroup>
      <Box
        css={{
          alignSelfX: 'end'
        }}
      >
        <Button
          type="primary"
          onPress={(e) => {
            console.log(e)
          }}
        >
          Save
        </Button>
      </Box>
    </Box>
  )
}

const App = ({ environment }: ExtensionContextValue) => {
  return (
    <ContextView
      title="Test card register"
      brandColor="#F6F8FA" // replace this with your brand color
      brandIcon={BrandIcon} // replace this with your brand icon
    >
      <TestCardForm customerId={environment.objectContext?.id} />    
    </ContextView>
  );
};

export default App;
```

保存すると、`testCardTypes`で定義したカードの種類を選ぶ画面が表示されます。

![スクリーンショット 2022-12-06 14.16.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/70c0f8a3-aa14-1b26-dfb2-d45526f0c228.png)


### Step3: useStateで、フォームに入力された情報をハンドルする

画面が用意できましたので、選択したテスト用カードを保存する仕組みを作りましょう。

まず`useState`を利用して、選択中のカード情報をstateに保存します。

```diff_typescript
-import { FC } from 'react'
+import { FC, useState } from 'react'

...


const TestCardForm:FC<{ customerId?: string }> = ({ customerId }) => {
+  const [testCard, setTestCard] = useState<TestCard | null>(null)
  if (!customerId) return null
  return (
    <Box css={{
      gap: 'medium',
      stack: 'y',
    }}>
      <FormFieldGroup
        legend="Testing card type"
        layout="column"
      >
        {testCardTypes.map(card => (
          <Radio
            key={card.cardNumber}
            name="cardType"
            label={card.label}
            onChange={(e) => {
-              console.log(e.target.checked)
+              setTestCard(card)
            }}
          />
        ))}
      </FormFieldGroup>
+      <Box>{JSON.stringify(testCard, null, 2)}</Box>

...
```

デバッグのため、`useState`にセットされた値を見るための要素を追加しています。

次の画像のように、選択したカード情報が表示されていればOKdesu.

![スクリーンショット 2022-12-06 14.22.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/d6120bb2-bd61-a788-e585-7b31ba76368a.png)

### Step4: Buttonのイベントハンドラを使って、カード情報を保存する

選択したカードを、顧客の決済手段として登録する処理を作りましょう。

`form`の`onSubmit`に相当するイベントが2022/12時点ではないため、`Button`の`onPress`イベントで発火します。

```diff_typescript
import { FC, useState} from 'react'
+import { showToast } from '@stripe/ui-extension-sdk/utils';
+import { createHttpClient, STRIPE_API_KEY } from '@stripe/ui-extension-sdk/http_client'
+import Stripe from 'stripe'
+const stripe = new Stripe(STRIPE_API_KEY, {
+  httpClient: createHttpClient(),
+  apiVersion: '2022-11-15',
+})

...


-      <Box>{JSON.stringify(testCard, null, 2)}</Box>
      <Box
        css={{
          alignSelfX: 'end'
        }}
      >
        <Button
          type="primary"
-          onPress={(e) => {
+          onPress={async (e) => {
-            console.log(e)
+            if (!testCard) return
+            const today = new Date()
+            const paymentMethod = await stripe.paymentMethods.create({
+              type: 'card',
+              card: {
+                number: testCard?.cardNumber,
+                exp_month: today.getMonth() + 1,
+                exp_year: today.getFullYear() + 1,
+                cvc: '123',
+              }
+            })
+            await stripe.paymentMethods.attach(paymentMethod.id, {
+              customer: customerId
+            })
+            await showToast("Saved the test card", {
+              type: "success"
+            })
          }}
        >
          Save
        </Button>
      </Box>
    </Box>
  )
}
```

:::note info
ユーザーがカード情報を入力する際は、本来SetupIntentとStripe ElementsかCheckoutを利用します。

https://stripe.com/docs/payments/setup-intents?locale=ja-JP

ですが今回は3DSの認証や有効期限のバリデーションなどは不要ですので、Payment Method APIで直接登録しています。
:::

`Save`ボタンをクリックして、画面下に成功メッセージが表示されれば成功です。

![スクリーンショット 2022-12-06 14.47.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/c3ff6562-04a2-9ffd-d1d1-324212312fe0.png)

### Step5: ダッシュボードの顧客情報を更新する

Stripe SDKを利用して、顧客情報などの更新ができました。

しかしよく見ると、ダッシュボード側の顧客情報が更新されていません。

アプリ側の変更をダッシュボードにも反映させるために、数行コードを追加しましょう。

```diff_typescript
-import { showToast } from '@stripe/ui-extension-sdk/utils';
+import { useRefreshDashboardData, showToast } from '@stripe/ui-extension-sdk/utils';


...



const TestCardForm:FC<{ customerId?: string }> = ({ customerId }) => {
  const [testCard, setTestCard] = useState<TestCard | null>(null)
+  const refreshDashboardData = useRefreshDashboardData()


...

            await stripe.paymentMethods.attach(paymentMethod.id, {
              customer: customerId
            })
+            await refreshDashboardData()
            await showToast("Saved the test card", {
              type: "success"
            })

```

これで保存成功後に、ダッシュボードが再読み込みされます。

## おわりに

このように、Stripe Appsのアプリを作ることで、Stripe上の様々なデータにアクセスしたり、情報を更新できます。

　請求業務や顧客サポートなどで、複数のページを行ったり来たりして行う作業がある場合、今回のサンプルを応用して、カスタムワークフローを構築できます。



## Stripe Appsひとりアドベントカレンダー 2022

今年ベータリリースされたばかりのStripe Appsは、まだ日本語の情報が多くありません。

そこでQiita Advent Calendar 2022にて、毎日Stripe Appsについての情報を投稿します。

https://qiita.com/advent-calendar/2022/stripe-apps

ノーコードで利用する方法や、開発するためのTipsなども紹介予定ですので、ぜひ購読をお願いします。