# UI 拡張機能の仕組み

UI 拡張機能システムと、Stripeダッシュボードを拡張する方法についてご紹介します。

Stripe アプリの UI 拡張機能 ([TypeScript](https://www.typescriptlang.org/) と [React](https://reactjs.org/)) を使用すると、Stripe プロダクトに独自の UI をレンダリングできるようになります。これらのツールは、React での開発経験を持つ人であればすぐにご利用いただけると思います。ただし、これらは別のウェブページに埋め込まれる[サンドボックス化された安全な iframe](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox) 内で実行されるため、標準の React ブラウザーアプリケーションとはいくつかの点で異なります。

## 概要

- [Intro to React (React の概要) (英語)](https://reactjs.org/tutorial/tutorial.html)
- [Get started with TypeScript (TypeScript の使用の開始) (英語)](https://www.typescriptlang.org/docs/)
- [Stripe の UI コンポーネント](https://docs.stripe.com/stripe-apps/components.md)

UI Extensions は TypeScript で記述され、React を使用し、[Stripe の UI ツールキット](https://docs.stripe.com/stripe-apps/components.md)で UI を作成します。その他の React 環境とは異なり、UI Extensions は、任意の HTML をサポートしていません。代わりに、Stripe が提供する UI コンポーネントのみを使用します。UI 拡張機能の構造には、いくつかの主要なディレクトリーとファイルが含まれています。

- `stripe-app.json`: *アプリマニフェスト* (In a Stripe App, the app manifest is a stripe-app.json file in your app's root directory. It defines your app's ID, views, permissions, and other essential properties)。アプリが Stripe と対話する方法を記述します。これには、必要な権限、UI 拡張機能が存在するかどうか、存在する場合はその拡張機能が Stripe の UI のどこに表示されるかなどが含まれます。
- `package.json`: NPM パッケージのメタデータ。UI 拡張機能は通常の [NPM パッケージ](https://docs.npmjs.com/about-packages-and-modules)です。[npm](https://docs.npmjs.com/cli) または [yarn](https://yarnpkg.com/) を使用して依存関係を管理することができます。
- `src`: UI 拡張機能の実際の TypeScript ソースコード。デフォルトでは、CLI は、汎用ビューを `src/views` に配置し、対応するエントリーを `stripe-app.json` に配置します。

UI 拡張機能の開発には、Stripe CLI アプリプラグインを使用します。CLI は、アプリを正しい構造で初期化して、アプリマニフェストを設定し、開発サーバーを実行して、Stripe に送信するためにアプリを適切にバンドルします。

### UI 拡張機能を開発する

- アプリ開発者としてビューを作成します。このビューは、特定の[ビューポート](https://docs.stripe.com/stripe-apps/reference/viewports.md)が画面に表示されるときは常に表示されるように登録された React コンポーネントです。たとえば、あるビューを、ユーザーが請求書の詳細ページを表示するたびに表示されるようにするには、ビューポートの `stripe.dashboard.invoice.detail` に登録します。
- アプリをアップロードする準備ができたら、CLI コマンドにより、コードがバンドルされて Stripe にアップロードされ、Stripe の CDN でホストされます。
- アプリの UI 拡張機能が初期化されると、Stripe は、サンドボックス化された iframe にアプリのコードをダウンロードします。
- ユーザーが特定のビューポートを持つページに移動する場合 (例: `/invoices/inv_1283`):
  - Stripe は、ビューポートに表示されるコンテキストを使用して、iframe 内の UI 拡張機能のビューを定義します。
  - Stripe がビューをダッシュボードに渡すと、ユーザーに表示されます。
  - ユーザーが UI 拡張機能を操作 (ボタンのクリックなど) すると、UI 拡張機能の iframe のイベントハンドラがイベントを受信し、ビューを更新します。
![Stripe アプリの UI 拡張機能のシステム図](https://b.stripecdn.com/docs-statics-srv/assets/extensions-structure.d6d4d0512ef2194eda5c7ffd214e894e.jpg)

## ビューとビューポート

UI をアプリのユーザーに表示するには、React ビューを作成してビューポートに登録します。

ビューは、アプリがエクスポートする React コンポーネントです。ビューポートは、ビューが表示される場所を示す識別子です。アプリがアップロードされると、アプリによってエクスポートされたすべてのビューが、関連付けられているビューポートに登録されます。

`stripe apps add view` を実行すると、ビューは自動的にビューポートに登録します。これにより、*アプリマニフェスト* (In a Stripe App, the app manifest is a stripe-app.json file in your app's root directory. It defines your app's ID, views, permissions, and other essential properties)にエントリーが追加されます。

```json
{
  //... other manifest properties
  "ui_extension": {
    "views": [
      {
          "viewport": "stripe.dashboard.invoice.detail", // See all valid values at stripe.com/docs/stripe-apps/reference/viewports
          "component": "NameOfComponent" // This is provided by you
      }
      // ... additional views
    ]
  }
}
```

## UI 拡張機能のライフサイクル

UI 拡張機能は、非表示のサンドボックス化された iframe で実行され、UI の更新は Stripe ダッシュボードに非同期で送信されます。サンドボックス化された単一の iframe で、複数のビューに同時に対応できます。

サンドボックス化された iframe と、それによって表示されるビューのライフサイクルは次のように機能します。

- ダッシュボードは、ダッシュボードが読み込まれてからユーザーがアプリを開くまでの間に発生する UI 拡張機能の iframe を読み込みます。
- ビューを表示する必要がある場合、ダッシュボードはサンドボックス化された iframe が初期化されるのを待ってから正しいビューをマウントするように指示し、[適切なコンテキスト](https://docs.stripe.com/stripe-apps/reference/extensions-sdk-api.md#props)に渡します。
- ユーザーがビューを閉じると (たとえば、アプリのドロワーを閉じる場合)、ビューのマウントが解除されます。マウントが解除されると、ビューは DOM とサンドボックス化された React ツリーから削除されます。
- サンドボックス化された iframe は、リソースの使用状況に応じて、実行状態のままになるか、シャットダウンされます。ダッシュボードは iframe を終了する前に [useEffect](https://reactjs.org/docs/hooks-effect.html) やその他のクリーンアップハンドラを実行できるように最善を尽くすことのみ保証しています。
![Stripe Apps の UI 拡張機能のライフサイクル](https://b.stripecdn.com/docs-statics-srv/assets/extensions-lifecycle.0fef55cf88daa063de38151596ad17e5.jpg)

Stripe Apps の UI 拡張機能のライフサイクル

## サンドボックス化された iframe の制限事項

UI 拡張機能のコードは一意のサンドボックス環境で実行されるため、Stripe Apps の UI 拡張機能は、フルブラウザーのコンテキストで実行される通常の React アプリが行うすべてを実行できるわけではありません。

### Stripe Apps と通常の React アプリの主な相違点

1. Stripe Apps は、DOM に直接アクセスできません。ダッシュボードから不可視の別個の DOM を持つ iframe 内で実行されます。
2. ダッシュボードがすべてのデータをシリアライズしてアプリにプロキシー送信します。UI ツールキットのコンポーネントはシリアライズ可能なデータのみを受け付けます。
3. ダッシュボードはすべての「プロパティー」もシリアライズしてアプリにプロキシー送信するため、UI ツールキットのコンポーネントに渡される、またはこれによってトリガーされる関数は、非同期型になります。

### React と JavaScript の制限事項

以下の制約は、アプリの開発時に React と JavaScript で何を実行できるかに影響します。React ツリーは、Stripe ダッシュボードのホスト環境がデシリアライズして評価するまで DOM にレンダリングされません。アプリの DOM が更新され、ダッシュボードの React のインスタンスがデータ入力を管理します。

#### グローバルな document オブジェクトと window オブジェクトは制限されている

UI 拡張コードが実行されている DOM 環境は、[サンドボックス化された iframe](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox)によってロックダウンされます。そのため、[localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage)、[indexedDB](https://developer.mozilla.org/en-US/docs/web/api/indexeddb)、[BroadcastChannel](https://developer.mozilla.org/en-US/docs/web/api/broadcastchannel) などの最上位の API は使用できません。[同一生成元ポリシー](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy)に依存する DOM API は、サンドボックス化された iframe の生成元が `null` であるため、正常には機能しません。

#### React の ref プロパティがサポートされない

UI コンポーネントは、React ツリーがシリアル化され、レンダリングのために Stripe ダッシュボードに渡されるため、React `ref` プロパティをサポートしていません。コンポーネントが最終的にレンダリングされる DOM は、サンドボックス化された iframe で実行されているアプリコードからアクセスできません。

#### アプリでは React のバージョンを管理できない

各 Stripe アプリで生成されるデフォルトの `package.json` ファイルには、`react` の `dependency` エントリーがありません。Stripe アプリの `package.json` ファイルに特定のバージョンの React を追加しても、アプリが表示される React バージョンは管理されません。タイプチェックとユニットテストのみを実行します。Stripe ダッシュボードは、そのバージョンの React (現在のバージョン 17.0.2) を使用して、すべてのアプリを表示します。互換性を確保するため、Stripe から指示された場合にのみ変更してください。

#### インタラクションに非制御コンポーネントを使用する

ダッシュボードはすべてのデータ入力をシリアライズしてアプリにプロキシー送信します。この結果、React の[制御コンポーネント](https://reactjs.org/docs/forms.html#controlled-components)の使用時に入力遅延が発生します。この遅延は、ユーザーに認識され、その間にユーザーが入力した文字を上書きする可能性があります。また、先頭でテキストを編集しようとすると、カーソルがテキスト入力の末尾にスキップします。

アプリの遅延を減らすために、非制御の方法でユーザーの入力を使用します。

```jsx
import {useState} from 'react';
import {TextArea} from '@stripe/ui-extension-sdk/ui';

const App = () => {
  const defaultValue = 'Initial TextArea value';
  const [text, setText] = useState(defaultValue);
  return (
    <>
      <TextArea
        label="Message"
        // This doesn't work ❌
        // Attempting to edit text at the beginning skips the cursor to the end
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <TextArea
        label="Message"
        // This will work ✅
        defaultValue={defaultValue}
        onChange={e => setText(e.target.value)}
      />
    </>
  );
};
```

### UI コンポーネントの制約

以下の制約は、[UI コンポーネント](https://docs.stripe.com/stripe-apps/components.md)に当てはまります。拡張機能は分離された環境で実行されますが、UI コンポーネントはダッシュボードに直接表示されます。SDK は、UI ツールキットコンポーネントを表示するようにダッシュボードに指示し、その結果、次の制限事項が生じます。

#### コンポーネントはイベントの伝播を止められない

イベントハンドラーは非同期で呼び出されるため、アプリのイベントハンドラーが呼び出されるまでにイベントはすでに伝播しています。そのため、アプリは、イベントの伝播やバブリングを止めることができません。

#### コンポーネントはシリアライズ可能なデータタイプのみをプロパティとして受け付ける

UI コンポーネントはシリアライズ可能なデータタイプのみを受け付けます。`Map` または `Set` など、シリアライズできないデータタイプをプロパティーとして UI ツールキットのコンポーネントに渡すと、エラーが返されます。

プロパティにはシンプルな型、関数、または React イベントのみを使用してください。サポートされている型は以下のとおりです。

- 文字列、数字、`true`、`false`、`null`、および `undefined`
- キーと値がすべてシンプルな型のオブジェクト
- 値がすべてシンプルな型の配列
- 関数。ただし、プロキシー送信されると非同期になります。引数として渡されるすべての関数や返される関数にも、型の制限が適用されます。
- React イベント

> このシリアライズ可能なデータ型の制限は、`@stripe/ui-extension-sdk/ui` からインポートされたコンポーネント (`Box`、`Img`、`Button` など) に *のみ* 適用されます。独自に作成したカスタムコンポーネントでは、SDK コンポーネントに直接値を渡さない限り、任意のデータ型をプロパティ (深くネストされたオブジェクト、循環参照または複合インスタンスを含む) として受け入れることができます。

```jsx
import { Img } from "@stripe/ui-extension-sdk/ui";

// Object with circular reference
const circularObj = { name: 'example' };
circularObj.self = circularObj;

// ✅ OK: Passing `circularObj` to your own component
function Card({ src, data }) {
  // ❌ NOT OK: Passing `data` to an SDK component
  return <Img src={src} width={100} height={100} data={data} />;
}

function App() {
  // ✅ OK: Custom component can receive complex objects
  return <Card src="https://example.com/image.jpg" data={circularObj} />;
}
```

#### コンポーネントはレンダリング関数をサポートしない

React は同期的にレンダリングしますが、UI コンポーネントに渡される関数は、ダッシュボードがアプリにプロキシー送信した後で非同期になります。UI コンポーネントに渡されるマークアップを生成する関数では、React がその結果を使用するまでにレンダリングが完了しません。結果的に、レンダリング関数はどの UI コンポーネントでも受け付けられません。

したがって、次のパターンは機能しません。

```jsx
// This doesn't work ❌
<ItemProvider>
  {(data) => (
    <Item data={data} />
  )}
</ItemProvider>
```

```jsx
// This doesn't work ❌
<Item renderFooter={() => <div>footer</div>} />
```

#### JSX は単一ノードとして子以外のプロパティにのみ渡すことが可能

UI コンポーネントは、次のような単一の React エレメントを取るプロパティーをサポートします。

```jsx
// This will work ✅
<Item footer={<div>footer</div>} />
```

より複雑な JSX データ構造はサポートされません。ただし、次の例外があります。

```jsx
// This doesn't work ❌
<Item footer={[<div>one</div>, <div>two</div>]} />
```

```jsx
// This doesn't work ❌
<Item footer={{
  one: <div>one</div>,
  two: <div>two</div>
}} />
```

複数の React エレメントを UI コンポーネントに渡す必要がある場合は、次のようにフラグメント内にラップします。

```jsx
// This works ✅
<Item footer={
  <>
    <div>one</div>
    <div>two</div>
  </>
}/>
```

同様の制約が `children` にあてはまります。JSX を含む配列とオブジェクトはサポートされませんが、複数の React エレメントは許可されます。

```jsx
// This works ✅
<Item>
  <div>one</div>
  <div>two</div>
</Item>
```

## NPM パッケージのインストール

サードパーティーの NPM パッケージを Stripe アプリに追加する場合の制限はありません。お客様が適切だと思うパッケージを自由にインストールしてください。ただし、UI 拡張機能には[サンドボックス化された iframe の制限事項](https://docs.stripe.com/stripe-apps/how-ui-extensions-work.md#sandboxed-iframe-limitations)があるため、すべてのパッケージが正常に機能するわけではありません。

`lodash` は DOM アクセスを必要としないため、`lodash` のようなユーティリティライブラリを使用してもかまいません。

```jsx
import { Box, Button } from "@stripe/ui-extension-sdk/ui";
import { useState } from "react";
import kebabCase from "lodash/kebabCase";

const text = "A note to the user";

const App = () => {
  const [isKebabCase, setIsKebabCase] = useState(false);
  return (
    <>
      {/* This will work ✅ */}
      <Box>{isKebabCase ? kebabCase(text) : text}</Box>
      <Button
        onPress={() => {
          setIsKebabCase(!isKebabCase);
        }}
      >
        Toggle kebab-case
      </Button>
    </>
  );
};
```

`react-hook-form` は Ref を使用してフォームのステータスを管理するため、`react-hook-form` のようなフォームライブラリは機能しません。

```jsx
import { TextField } from "@stripe/ui-extension-sdk/ui";
import { useForm } from "react-hook-form";

const App = () => {
  const { register } = useForm();
  const { onChange, name, ref } = register("firstName");
  return (
    <TextField
      label="First name"
      placeholder="Enter your name"
      name={name}
      onChange={onChange}
      // This doesn't work ❌
      ref={ref}
    />
  );
};
```

## See also

- [ビューの構築とテスト](https://docs.stripe.com/stripe-apps/build-ui.md)
- [アプリの設計](https://docs.stripe.com/stripe-apps/design.md)
- [アプリのスタイル設定](https://docs.stripe.com/stripe-apps/style.md)
- [配布オプション](https://docs.stripe.com/stripe-apps/distribution-options.md)
