# 画面設計書 90-サポートページ

## 概要

本ドキュメントは、Ghost会員向けポータル（Portal）のサポートページ（SupportPage）の画面設計書である。寄付/チップ機能を通じてサイトをサポートするための支払い処理画面を定義する。

### 本画面の処理概要

サポートページは、Ghost会員サイトのPortalウィジェット内で、Stripeを通じた寄付/チップ支払いを開始する画面である。

**業務上の目的・背景**：サポート（Tips and Donations）機能は、サイト運営者が会員やビジターから任意の金額で支援を受け取るための機能である。サブスクリプションとは別に、一回限りの寄付を受け付けることで、収益化の選択肢を拡大する。

**画面へのアクセス方法**：サイト上の「Support」ボタン/リンクをクリックして遷移する。直接URL（`#/portal/support`）でのアクセスも可能。

**主要な操作・処理内容**：
1. ページ表示時にStripeチェックアウトセッション作成
2. Stripe決済ページへの自動リダイレクト
3. 決済成功/キャンセル時のリダイレクト処理
4. 機能無効/エラー時のエラー画面表示

**画面遷移**：
- 遷移元：サイトテーマ上のサポートボタン
- 遷移先：Stripe決済ページ、成功画面（support/success）、エラー画面（support/error）

**権限による表示制御**：
- ログイン/未ログインに関わらずアクセス可能
- donations_enabled設定がfalseの場合はエラー表示

## 関連機能

| 機能No | 機能名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 14 | Stripe連携 | 主機能 | Stripe決済処理 |
| 71 | Portal | 補助機能 | Portalウィジェット内でのUI表示 |

## 画面種別

処理中（自動遷移）

## URL/ルーティング

- ハッシュルート：`#/portal/support`
- Pages.jsでの登録キー：`support`
- 成功時リダイレクト先：`#/portal/support/success`（未ログイン）または `?action=support&success=true`（ログイン済み）

## 入出力項目

| 項目名 | 項目ID | 型 | 必須 | 入力/出力 | 説明 |
|--------|--------|-----|------|----------|------|
| なし | - | - | - | - | 本画面に入力項目なし（自動処理） |

## 表示項目

| 項目名 | データソース | 説明 |
|--------|-------------|------|
| ローディング画面 | - | Stripe処理中のローディング表示 |
| エラータイトル | - | "Sorry, that didn't work." |
| エラーメッセージ | error | エラー内容の説明 |

## イベント仕様

### 1-Stripeチェックアウト開始（自動）

**トリガー**：ページ表示時（useEffect）

**処理フロー**：
1. site.donations_enabled を確認
2. falseの場合：DisabledFeatureErrorをセット、ローディング終了
3. trueの場合：checkoutDonation関数を実行
4. api.member.checkoutDonation({successUrl, cancelUrl, personalNote})を呼び出し
5. 成功時：response.urlへリダイレクト（window.location.replace）
6. 失敗時：エラーメッセージをセット、ローディング終了

**API呼び出し**：
- POST `/members/api/create-stripe-checkout-session/`
- ボディ：`{identity, metadata, successUrl, cancelUrl, type: 'donation', personalNote}`

### 2-決済成功後リダイレクト

**トリガー**：Stripe決済完了

**処理フロー**：
- ログイン済み：`{currentUrl}?action=support&success=true`へリダイレクト
- 未ログイン：`{currentUrl}#/portal/support/success`へリダイレクト

### 3-決済キャンセル

**トリガー**：Stripe決済ページでキャンセル

**処理フロー**：
- cancelUrl（現在のURL）へリダイレクト

## データベース更新仕様

### 操作別データベース影響一覧

| 操作（イベント） | 対象テーブル | 操作種別 | 概要 |
|----------------|-------------|---------|------|
| 寄付決済成功 | donations | INSERT | 寄付レコード作成（Stripe Webhook経由） |

### テーブル別更新項目詳細

#### donations（Stripe Webhook経由）

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | member_id | 会員ID（ログイン時）またはnull | - |
| INSERT | email | 決済者のメールアドレス | - |
| INSERT | amount | 寄付金額 | - |
| INSERT | personal_note | パーソナルノート | - |
| INSERT | created_at | 現在日時 | - |

## メッセージ仕様

| メッセージID | 種別 | メッセージ内容 | 表示条件 |
|-------------|------|--------------|---------|
| MSG-001 | エラー | "Sorry, that didn't work." | エラー発生時 |
| MSG-002 | エラー | "There was an error processing your payment. Please try again." | デフォルトエラーメッセージ |
| MSG-003 | エラー | "This site is not accepting donations at the moment." | donations_enabled=false時 |
| MSG-004 | エラー | "This site is not accepting payments at the moment." | DisabledFeatureError時 |
| MSG-005 | エラー | "Something went wrong, please try again later." | 一般エラー時 |
| MSG-006 | プレースホルダー | "Add a personal note" | personalNoteのデフォルト値 |

## 例外処理

| 例外条件 | 処理内容 | 表示メッセージ |
|---------|---------|--------------|
| donations_enabled=false | SupportErrorコンポーネント表示 | "This site is not accepting donations at the moment." |
| DisabledFeatureError | SupportErrorコンポーネント表示 | "This site is not accepting payments at the moment." |
| API通信エラー | SupportErrorコンポーネント表示 | "Something went wrong, please try again later." |
| Stripe未設定 | SupportErrorコンポーネント表示 | エラーメッセージ |

## 備考

- 本画面は基本的にローディング画面のみを表示し、即座にStripe決済ページへリダイレクトする
- エラーが発生した場合のみSupportErrorコンポーネントが表示される
- 決済処理はStripe側で完結し、Webhook経由でGhost側に通知される
- personalNoteは"Add a personal note"がデフォルトで設定される
- Sentryによるエラーログ記録が有効

---

## コードリーディングガイド

本画面を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: データ構造を理解する

サポートページで扱う主要なデータ構造を把握する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | app-context.js | `apps/portal/src/app-context.js` | AppContextの構造（member, site） |

**読解のコツ**: site.donations_enabledが機能の有効/無効を制御。member有無でリダイレクトURLが変わる。

#### Step 2: エントリーポイントを理解する

ページコンポーネントの構造と初期化処理を把握する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | support-page.js | `apps/portal/src/components/pages/support-page.js` | SupportPage関数コンポーネント |
| 2-2 | pages.js | `apps/portal/src/pages.js` | ページルーティング定義 |

**主要処理フロー**:
1. **8-12行**: 関数定義、state初期化（isLoading, error, disabledFeatureError）
2. **14-48行**: useEffectでcheckoutDonation実行
3. **50-67行**: 条件に応じたコンポーネント返却

#### Step 3: チェックアウト処理を理解する

Stripeチェックアウトセッション作成処理を把握する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | support-page.js | `apps/portal/src/components/pages/support-page.js` | useEffect内のcheckoutDonation関数 |

**主要処理フロー**:
- **15-37行**: checkoutDonation内部関数定義
- **16-17行**: successUrl計算（member有無で分岐）
- **22-23行**: api.member.checkoutDonation呼び出し
- **25-27行**: 成功時のリダイレクト
- **28-36行**: エラーハンドリング

#### Step 4: API連携を理解する

バックエンドAPIとの連携処理を把握する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | api.js | `apps/portal/src/utils/api.js` | member.checkoutDonation関数 |

**主要処理フロー**:
- **511-551行**: checkoutDonation関数
- **521-528行**: リクエストボディ構築
- **541-548行**: エラーハンドリング

#### Step 5: エラー画面を理解する

エラー発生時の表示を把握する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | support-error.js | `apps/portal/src/components/pages/support-error.js` | SupportErrorコンポーネント |

**主要処理フロー**:
- **28-61行**: SupportErrorコンポーネント
- **30-32行**: メッセージ設定
- **34-36行**: Sentryへのエラーログ送信

### プログラム呼び出し階層図

```
SupportPage (support-page.js)
    |
    +-- useContext(AppContext)
    |       +-- member, site
    |
    +-- useState()
    |       +-- isLoading (true)
    |       +-- error (null)
    |       +-- disabledFeatureError (null)
    |
    +-- useEffect([])
    |       |
    |       +-- site.donations_enabled === false
    |       |       +-> setDisabledFeatureError("...donations...")
    |       |       +-> setLoading(false)
    |       |
    |       +-- else
    |               +-> checkoutDonation()
    |                       |
    |                       +-- successUrl計算
    |                       |       +-- member ? "?action=support&success=true"
    |                       |       +-- !member ? "#/portal/support/success"
    |                       |
    |                       +-- api.member.checkoutDonation()
    |                       |       |
    |                       |       +-> POST /create-stripe-checkout-session/
    |                       |               body: {identity, metadata, successUrl,
    |                       |                      cancelUrl, type: 'donation',
    |                       |                      personalNote}
    |                       |
    |                       +-- response.url
    |                       |       +-> window.location.replace(response.url)
    |                       |
    |                       +-- error handling
    |                               +-- DisabledFeatureError -> setDisabledFeatureError()
    |                               +-- other -> setError()
    |
    +-- render()
            |
            +-- isLoading ? LoadingPage
            +-- error ? SupportError
            +-- disabledFeatureError ? SupportError
            +-- else ? null
```

### データフロー図

```
[入力]                    [処理]                           [出力]

AppContext
  +-- member -------------> successUrl計算 ----------------> リダイレクトURL
  +-- site.donations_enabled -> 機能有効性チェック ---------> エラー or 続行

ページロード時（自動）
  |
  +-- useEffect() --------> checkoutDonation()
                                |
                                +-> donations_enabled?
                                        |
                                        +-- false -> SupportError
                                        |
                                        +-- true
                                                |
                                                +-> api.member.checkoutDonation()
                                                        |
                                                        +-> POST /create-stripe-checkout-session/
                                                                |
                                                                +-> response.url
                                                                        |
                                                                        +-> window.location.replace()
                                                                                |
                                                                                +-> Stripe決済ページ
                                                                                        |
                                                                                        +-- 成功 -> successUrl
                                                                                        +-- キャンセル -> cancelUrl

Stripe決済完了（Webhook）
  |
  +-> Ghost Backend ------> donations INSERT
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| support-page.js | `apps/portal/src/components/pages/support-page.js` | ソース | メインページコンポーネント |
| support-error.js | `apps/portal/src/components/pages/support-error.js` | ソース | エラー画面コンポーネント |
| support-success.js | `apps/portal/src/components/pages/support-success.js` | ソース | 成功画面コンポーネント |
| pages.js | `apps/portal/src/pages.js` | ソース | ページルーティング定義 |
| app-context.js | `apps/portal/src/app-context.js` | ソース | アプリケーションコンテキスト定義 |
| api.js | `apps/portal/src/utils/api.js` | ソース | API通信処理 |
| loading-page.js | `apps/portal/src/components/pages/loading-page.js` | ソース | ローディング画面コンポーネント |
| close-button.js | `apps/portal/src/components/common/close-button.js` | ソース | 閉じるボタンコンポーネント |
| action-button.js | `apps/portal/src/components/common/action-button.js` | ソース | アクションボタンコンポーネント |
| warning-outline.svg | `apps/portal/src/images/icons/warning-outline.svg` | アセット | 警告アイコン |
| i18n.js | `apps/portal/src/utils/i18n.js` | ソース | 国際化ユーティリティ |
