UI 拡張機能の仕組み
UI 拡張機能システムと、Stripeダッシュボードを拡張する方法についてご紹介します。
Stripe Apps の UI 拡張機能により、 TypeScript と React を使用して独自の UI を Stripe 製品に表示できます。React での開発経験がある場合は、これらのツールに馴染みがあるはずです。ただし、これらは別の Web ページに埋め込まれた安全なサンドボックス内で実行されるため、いくつかの点が標準のブラウザーベースの React アプリケーションとは異なります。
概要
UI Extensions は TypeScript で記述され、React を使用し、Stripe の UI ツールキットで UI を作成します。その他の React 環境とは異なり、UI Extensions は、任意の HTML をサポートしていません。代わりに、Stripe が提供する UI コンポーネントのみを使用します。UI 拡張機能の構造には、いくつかの主要なディレクトリーとファイルが含まれています。
stripe-app.
: アプリマニフェスト。アプリが Stripe と対話する方法を記述します。これには、必要な権限、UI 拡張機能が存在するかどうか、存在する場合はその拡張機能が Stripe の UI のどこに表示されるかなどが含まれます。json package.
: NPM パッケージのメタデータ。UI 拡張機能は通常の NPM パッケージです。npm または yarn を使用して依存関係を管理することができます。json src
: UI 拡張機能の実際の TypeScript ソースコード。デフォルトでは、CLI は、汎用ビューをsrc/views
に配置し、対応するエントリーをstripe-app.
に配置します。json
UI 拡張機能の開発には、Stripe CLI アプリプラグインを使用します。CLI は、アプリを正しい構造で初期化して、アプリマニフェストを設定し、開発サーバーを実行して、Stripe に送信するためにアプリを適切にバンドルします。
UI 拡張機能を開発する
- アプリ開発者としてビューを作成します。このビューは、特定のビューポートが画面に表示されるときは常に表示されるように登録された React コンポーネントです。たとえば、あるビューを、ユーザーが請求書の詳細ページを表示するたびに表示されるようにするには、ビューポートの
stripe.
に登録します。dashboard. invoice. detail - アプリをアップロードする準備ができたら、CLI コマンドにより、コードがバンドルされて Stripe にアップロードされ、Stripe の CDN でホストされます。
- アプリの UI 拡張機能が初期化されると、Stripe は、サンドボックス化された iframe にアプリのコードをダウンロードします。
- ユーザーが特定のビューポートを持つページに移動する場合 (例:
/invoices/inv_
):1283 - Stripe は、ビューポートによって提供されたコンテキストでサンドボックス内の UI 拡張機能のビューを定義します。
- Stripe がビューをダッシュボードに渡すと、ユーザーに表示されます。
- ユーザーが UI 拡張機能を操作すると (ボタンをクリックするなど)、UI 拡張機能のサンドボックス内のイベントハンドラーがイベントを受信して、ビューを更新できます。
ビューとビューポート
UI をアプリのユーザーに表示するには、React ビューを作成してビューポートに登録します。
ビューは、アプリがエクスポートする React コンポーネントです。ビューポートは、ビューが表示される場所を示す識別子です。アプリがアップロードされると、アプリによってエクスポートされたすべてのビューが、関連付けられているビューポートに登録されます。
stripe apps add view
を実行すると、ビューは自動的にビューポートに登録します。これにより、アプリマニフェストにエントリーが追加されます。
{ //... 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 拡張機能は、UI の更新を非同期に Stripe ダッシュボードに送信して表示する、不可視のサンドボックス化された iframe で実行されます。1 つのサンドボックスで、同時に複数のビューに対応できます。
サンドボックスとビューのライフサイクルは以下のようになります。
- ダッシュボードが、UI 拡張機能のサンドボックスを読み込みます。これは、ダッシュボードが読み込まれてから、ユーザーがアプリを開くまでの間に発生します。
- ビューを表示する必要がある場合、ダッシュボードは、サンドボックスが初期化されるまで待機し、その後マウントする正しいビューをサンドボックスに指示して、適切なコンテキストで渡します。
- ユーザーがビューを閉じると (たとえば、アプリのドロワーを閉じる場合)、ビューのマウントが解除されます。マウントが解除されると、ビューは DOM とサンドボックス化された React ツリーから削除されます。
- サンドボックスは、リソース使用量に応じて、実行されたままになるか、シャットダウンされることになります。ダッシュボードは、サンドボックスが終了する前に useEffect とその他のクリーンアップハンドラーが実行されるよう最善を尽くします。
Stripe Apps の UI 拡張機能のライフサイクル
サンドボックスの制限事項
UI 拡張機能のコードは一意のサンドボックス環境で実行されるため、Stripe Apps の UI 拡張機能は、フルブラウザーのコンテキストで実行される通常の React アプリが行うすべてを実行できるわけではありません。
Stripe Apps と通常の React アプリの主な相違点
- Stripe Apps は、DOM に直接アクセスできません。ダッシュボードから不可視の別個の DOM を持つ iframe 内で実行されます。
- ダッシュボードがすべてのデータをシリアライズしてアプリにプロキシー送信します。UI ツールキットのコンポーネントはシリアライズ可能なデータのみを受け付けます。
- ダッシュボードはすべての「プロパティー」もシリアライズしてアプリにプロキシー送信するため、UI ツールキットのコンポーネントに渡される、またはこれによってトリガーされる関数は、非同期型になります。
React と JavaScript の制限事項
以下の制約は、アプリの開発時に React と JavaScript で何を実行できるかに影響します。React ツリーは、Stripe ダッシュボードのホスト環境がデシリアライズして評価するまで DOM にレンダリングされません。アプリの DOM が更新され、ダッシュボードの React のインスタンスがデータ入力を管理します。
グローバルな document オブジェクトと window オブジェクトは制限されている
UI 拡張コードが実行されている DOM 環境は、サンドボックス化された iframe によってロックダウンされます。そのため、localStorage、indexedDB、BroadcastChannel などの上位の API を使用できません。サンドボックス化された iframe は null
オリジンを持つため、同一オリジンポリシーを適用している DOM API は正常に機能しません。
React の ref プロパティがサポートされない
UI コンポーネントは、React の ref
プロパティーをサポートしていません。これは、React ツリーがシリアライズされて Stripe ダッシュボードに渡され、レンダリングされるためです。コンポーネントが最終的にレンダリングされる DOM は、サンドボックス化されたアプリコードからアクセスできません。
アプリでは React のバージョンを管理できない
各 Stripe アプリで生成されるデフォルトの package.
ファイルには、react
の dependency
エントリーがありません。Stripe アプリの package.
ファイルに特定のバージョンの React を追加しても、アプリが表示される React バージョンは管理されません。タイプチェックとユニットテストのみを実行します。Stripe ダッシュボードは、そのバージョンの React (現在のバージョン 17.0.2) を使用して、すべてのアプリを表示します。互換性を確保するため、Stripe から指示された場合にのみ変更してください。
インタラクションに非制御コンポーネントを使用する
ダッシュボードはすべてのデータ入力をシリアライズしてアプリにプロキシー送信します。この結果、React の制御コンポーネントの使用時に入力遅延が発生します。この遅延は、ユーザーに認識され、その間にユーザーが入力した文字を上書きする可能性があります。また、先頭でテキストを編集しようとすると、カーソルがテキスト入力の末尾にスキップします。
アプリの遅延を減らすために、非制御の方法でユーザーの入力を使用します。
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 コンポーネントに当てはまります。拡張機能は分離された環境で実行されますが、UI コンポーネントはダッシュボードに直接表示されます。SDK は、UI ツールキットコンポーネントを表示するようにダッシュボードに指示し、その結果、次の制限事項が生じます。
コンポーネントはイベントの伝播を止められない
イベントハンドラーは非同期で呼び出されるため、アプリのイベントハンドラーが呼び出されるまでにイベントはすでに伝播しています。そのため、アプリは、イベントの伝播やバブリングを止めることができません。
コンポーネントはシリアライズ可能なデータタイプのみをプロパティとして受け付ける
UI コンポーネントはシリアライズ可能なデータタイプのみを受け付けます。Map
または Set
など、シリアライズできないデータタイプをプロパティーとして UI ツールキットのコンポーネントに渡すと、エラーが返されます。
プロパティにはシンプルな型、関数、または React イベントのみを使用してください。サポートされている型は以下のとおりです。
- 文字列、数字、
true
、false
、null
、およびundefined
- キーと値がすべてシンプルな型のオブジェクト
- 値がすべてシンプルな型の配列
- 関数。ただし、プロキシー送信されると非同期になります。引数として渡されるすべての関数や返される関数にも、型の制限が適用されます。
- React イベント
コンポーネントはレンダリング関数をサポートしない
React は同期的にレンダリングしますが、UI コンポーネントに渡される関数は、ダッシュボードがアプリにプロキシー送信した後で非同期になります。UI コンポーネントに渡されるマークアップを生成する関数では、React がその結果を使用するまでにレンダリングが完了しません。結果的に、レンダリング関数はどの UI コンポーネントでも受け付けられません。
したがって、次のパターンは機能しません。
// This doesn't work ❌ <ItemProvider> {(data) => ( <Item data={data} /> )} </ItemProvider>
// This doesn't work ❌ <Item renderFooter={() => <div>footer</div>} />
JSX は単一ノードとして子以外のプロパティにのみ渡すことが可能
UI コンポーネントは、次のような単一の React エレメントを取るプロパティーをサポートします。
// This will work ✅ <Item footer={<div>footer</div>} />
より複雑な JSX データ構造はサポートされません。ただし、次の例外があります。
// This doesn't work ❌ <Item footer={[<div>one</div>, <div>two</div>]} />
// This doesn't work ❌ <Item footer={{ one: <div>one</div>, two: <div>two</div> }} />
複数の React エレメントを UI コンポーネントに渡す必要がある場合は、次のようにフラグメント内にラップします。
// This works ✅ <Item footer={ <> <div>one</div> <div>two</div> </> }/>
同様の制約が children
にあてはまります。JSX を含む配列とオブジェクトはサポートされませんが、複数の React エレメントは許可されます。
// This works ✅ <Item> <div>one</div> <div>two</div> </Item>
NPM パッケージのインストール
Stripe Apps にサードパーティーの NPM パッケージを追加する場合には制限はありません。必要に応じて自由にパッケージをインストールしてください。ただし、UI Extensions のサンドボックスの制限事項により、パッケージが期待どおりに機能しない場合があります。
lodash
は DOM アクセスを必要としないため、lodash
のようなユーティリティライブラリを使用してもかまいません。
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
のようなフォームライブラリは機能しません。
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} /> ); };