# 画面設計書 79-サインアップページ

## 概要

本ドキュメントは、Portal（公開会員ポータル）におけるサインアップページの設計書です。

### 本画面の処理概要

この画面は、Ghostサイトへの新規会員登録を行うためのページです。無料会員登録、有料プランの選択、ニュースレター購読設定などを行えます。

**業務上の目的・背景**：Ghost Portalは、サイト訪問者を購読者・会員に変換するための中心的な機能です。本画面は、新規ユーザーの会員登録プロセスを管理し、無料・有料のメンバーシップを提供します。利用規約への同意、プラン選択、ニュースレター設定など、多様なオプションをサポートし、柔軟な会員獲得フローを実現しています。

**画面へのアクセス方法**：Portalウィジェットを開いてサインアップを選択するか、サインインページから「Sign up」リンクをクリックしてアクセスします。すでにログイン済みの場合はアカウントホームページへ自動遷移します。

**主要な操作・処理内容**：
1. 名前・メールアドレスの入力
2. 利用規約への同意（設定されている場合）
3. プランの選択（無料/有料）
4. ニュースレター購読の選択（複数ニュースレターがある場合）
5. サインアップリクエストの送信

**画面遷移**：
- 遷移元：Portalウィジェット、サインインページ、オファーページ
- 遷移先：マジックリンクページ、ニュースレター選択ページ
- 関連遷移：サインインページ

**権限による表示制御**：
- メンバーシップ機能が無効の場合、サインアップフォームは表示されない
- 招待制の場合、招待メッセージのみ表示
- 有料会員のみの場合、無料サインアップが制限される

## 関連機能

| 機能No | 機能名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 8 | メンバー登録 | 主機能 | 新規会員の登録処理 |
| 15 | Tier（プラン）管理 | 補助機能 | 利用可能プランの表示 |
| 71 | Portal | 補助機能 | Portalウィジェット内でのUI表示 |

## 画面種別

入力フォーム（登録）

## URL/ルーティング

- ページ識別子: `signup`
- Portal内パス: `#/portal/signup`
- ルート定義: `apps/portal/src/pages.js`

## 入出力項目

| 項目名 | 入出力 | データ型 | 必須 | 説明 |
|--------|--------|----------|------|------|
| name | 入力 | string | 条件付き | 名前（portal_name設定時のみ） |
| email | 入力 | string | Yes | メールアドレス |
| plan | 入力 | string | Yes | 選択したプランID（'free'または価格ID） |
| phonenumber | 入力 | string | No | 電話番号（スパム対策用隠しフィールド） |
| token | 入力 | string | No | 認証トークン（特殊ケース用） |
| termsCheckboxChecked | 入力 | boolean | 条件付き | 利用規約への同意 |

## 表示項目

### ヘッダー

| 項目名 | データ型 | 説明 |
|--------|----------|------|
| サイトアイコン | image | サイトのアイコン画像 |
| サイト名 | string | サイトのタイトル |

### フォーム

| 項目名 | データ型 | 説明 |
|--------|----------|------|
| Nameフィールド | input | 名前入力フィールド（オプション） |
| Emailフィールド | input | メールアドレス入力フィールド |
| 利用規約チェックボックス | checkbox | 同意チェックボックス（設定時） |
| プラン選択 | ProductsSection | 利用可能なプランの選択UI |
| Sign upボタン | button | サインアップ送信ボタン |
| サインインリンク | link | "Already a member?" + "Sign in" |

### 特殊状態表示

| 状態 | 表示内容 |
|------|---------|
| 招待制 | "This site is invite-only, contact the owner for access." |
| 有料会員のみ | "This site only accepts paid members." |
| メンバーシップ無効 | "Memberships unavailable, contact the owner for access." |
| フリートライアル | トライアル終了後の課金に関する説明 |

## イベント仕様

### 1-フォーム入力

- トリガー: Name/Emailフィールドへの入力
- 処理:
  1. `handleInputChange()` でstateを更新
  2. 対応するstateプロパティに値を設定

### 2-プラン選択

- トリガー: プランカードのクリック
- 処理:
  1. `handleSelectPlan()` で選択を処理
  2. `state.plan` に選択したプランIDを設定
  3. 5ms遅延でReactのチェックボックス同期問題を回避

### 3-利用規約チェック

- トリガー: チェックボックスのクリック
- 処理:
  1. `termsCheckboxChecked` stateを更新
  2. 未チェック時はエラー表示、フォーカス移動

### 4-サインアップ送信

- トリガー: 「Sign up」ボタンクリック、またはプラン選択時の「Continue」クリック
- 処理:
  1. `getFormErrors()` でバリデーション実行（利用規約含む）
  2. エラーがある場合はstate.errorsに設定
  3. 利用規約エラーのみの場合はチェックボックスにスクロール
  4. 複数ニュースレターがある場合は `showNewsletterSelection: true` に設定
  5. 単一ニュースレターの場合は `doAction('signup', {...})` を実行

### 5-ニュースレター選択後サインアップ

- トリガー: ニュースレター選択ページからの戻り
- 処理:
  1. `NewsletterSelectionPage` コンポーネントが表示
  2. 選択完了後に `doAction('signup', {...})` を実行

### 6-サインインへ遷移

- トリガー: 「Sign in」リンククリック
- 処理:
  1. `doAction('switchPage', {page: 'signin'})` を実行
  2. サインインページへ遷移

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

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

| 操作（イベント） | 対象テーブル | 操作種別 | 概要 |
|----------------|-------------|---------|------|
| サインアップ送信 | members | INSERT | 新規メンバーの作成 |
| サインアップ送信 | members_newsletters | INSERT | ニュースレター購読設定 |

※ 実際のデータ作成はGhost Core Members APIで処理

## メッセージ仕様

| 種別 | メッセージ | 表示タイミング |
|------|-----------|---------------|
| ラベル | Name | 名前フィールドラベル（オプション） |
| ラベル | Email | メールフィールドラベル |
| プレースホルダー | Jamie Larson | 名前フィールドプレースホルダー |
| プレースホルダー | jamie@example.com | メールフィールドプレースホルダー |
| ボタン | Sign up | 無料プランのみの場合 |
| ボタン | Continue | 有料プランがある場合 |
| ボタン | Sending... | サインアップ処理中 |
| ボタン | Retry | サインアップ失敗時 |
| 情報 | Already a member? | サインインへの案内 |
| リンク | Sign in | サインインページへのリンク |
| 情報 | This site is invite-only, contact the owner for access. | 招待制サイト |
| 情報 | This site only accepts paid members. | 有料会員のみ |
| 情報 | Memberships unavailable, contact the owner for access. | メンバーシップ無効 |
| 情報 | After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then. | フリートライアル説明 |
| エラー | Please fill in required fields | プラン選択時のフォームエラー |

## 例外処理

| 例外状況 | 対応内容 |
|---------|---------|
| ログイン済み | アカウントホームページへ自動遷移 |
| 招待制サイト | サインアップフォームを非表示、招待メッセージを表示 |
| 有料会員のみ（free query） | 有料会員のみメッセージを表示 |
| メンバーシップ無効 | サインインも不可のメッセージを表示 |
| サインアップ失敗 | 「Retry」ボタンを表示 |
| 利用規約未同意 | チェックボックスにスクロール、エラースタイル適用 |

## 備考

- portal_name設定で名前フィールドの表示/非表示を制御
- portal_signup_terms_htmlで利用規約テキストをカスタマイズ可能
- portal_signup_checkbox_requiredで同意チェックボックスを必須化可能
- ProductsSectionコンポーネントで複雑なプラン選択UIを提供
- SiteTitleBackButtonでニュースレター選択からの戻りを実現
- sanitizeHtmlで利用規約HTMLをサニタイズ

---

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

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

### 推奨読解順序

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

まず、プログラム間で受け渡されるデータ構造を理解することが重要です。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | app-context.js | `apps/portal/src/app-context.js` | AppContextの構造（site, member, action等） |
| 1-2 | helpers.js | `apps/portal/src/utils/helpers.js` | getSiteProducts, getSitePrices等のヘルパー |

**読解のコツ**: `site` オブジェクトには `portal_name`, `portal_signup_terms_html`, `portal_signup_checkbox_required` などの設定が含まれます。

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

処理の起点となるファイル・関数を特定します。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | signup-page.js | `apps/portal/src/components/pages/signup-page.js` | サインアップページの実装 |
| 2-2 | pages.js | `apps/portal/src/pages.js` | ページマッピング |

**主要処理フロー**:
1. **L347-361**: constructor - 初期state設定
2. **L363-378**: componentDidMount/componentDidUpdate - プラン初期化
3. **L395-439**: doSignup - サインアップ処理実行
4. **L495-544**: getInputFields - フォームフィールド定義
5. **L546-587**: renderSignupTerms - 利用規約表示
6. **L631-652**: renderProducts - プラン選択UI
7. **L687-770**: renderForm - フォーム全体のレンダリング

#### Step 3: 関連コンポーネントを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | products-section.js | `apps/portal/src/components/common/products-section.js` | プラン選択コンポーネント |
| 3-2 | newsletter-selection-page.js | `apps/portal/src/components/pages/newsletter-selection-page.js` | ニュースレター選択 |
| 3-3 | input-form.js | `apps/portal/src/components/common/input-form.js` | 入力フォームコンポーネント |

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

```
SignupPage (React Component)
    |
    +-- componentDidMount()
    |       +-- (if member) doAction('switchPage', {page: 'accountHome'})
    |       +-- handleSelectedPlan()
    |
    +-- handleSignup(e) / handleChooseSignup(e, plan)
    |       +-- doSignup()
    |               +-- getFormErrors() (includes terms checkbox)
    |               +-- (if hasMultipleNewsletters) showNewsletterSelection = true
    |               +-- (else) doAction('signup', {name, email, plan, ...})
    |
    +-- renderForm()
    |       +-- isInviteOnly() --> renderInviteOnlyMessage()
    |       +-- isPaidMembersOnly() --> renderPaidMembersOnlyMessage()
    |       +-- !isSignupAllowed() --> renderMembersDisabledMessage()
    |       +-- InputForm (component)
    |       +-- renderSignupTerms()
    |       +-- renderProducts() --> ProductsSection
    |       +-- renderSubmitButton()
    |       +-- renderLoginMessage()
    |
    +-- (if showNewsletterSelection)
            +-- NewsletterSelectionPage (component)
```

### データフロー図

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

ページ読み込み
    |
    +-- componentDidMount() --> member存在? --> accountHome遷移
    |                          |
    |                          +-- handleSelectedPlan() --> state.plan初期化

フォーム入力
    |
    +-- handleInputChange() --> state更新（name/email）

プラン選択
    |
    +-- handleSelectPlan() --> state.plan更新

サインアップ送信
    |
    +-- doSignup() --> getFormErrors()
                          |
                          +-- (errors) --> state.errors更新
                          |                  |
                          |                  +-- (checkbox error only) --> scroll to terms
                          |
                          +-- (valid) --> hasMultipleNewsletters?
                                               |
                                               +-- (yes) --> NewsletterSelectionPage
                                               |
                                               +-- (no) --> doAction('signup')
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| signup-page.js | `apps/portal/src/components/pages/signup-page.js` | ソース | サインアップページコンポーネント |
| pages.js | `apps/portal/src/pages.js` | ソース | ページマッピング定義 |
| app-context.js | `apps/portal/src/app-context.js` | ソース | Reactコンテキスト定義 |
| products-section.js | `apps/portal/src/components/common/products-section.js` | ソース | プラン選択コンポーネント |
| newsletter-selection-page.js | `apps/portal/src/components/pages/newsletter-selection-page.js` | ソース | ニュースレター選択ページ |
| input-form.js | `apps/portal/src/components/common/input-form.js` | ソース | 入力フォームコンポーネント |
| action-button.js | `apps/portal/src/components/common/action-button.js` | ソース | アクションボタンコンポーネント |
| close-button.js | `apps/portal/src/components/common/close-button.js` | ソース | 閉じるボタンコンポーネント |
| site-title-back-button.js | `apps/portal/src/components/common/site-title-back-button.js` | ソース | 戻るボタンコンポーネント |
| form.js | `apps/portal/src/utils/form.js` | ソース | フォームバリデーション |
| helpers.js | `apps/portal/src/utils/helpers.js` | ソース | ヘルパー関数 |
| sanitize-html.js | `apps/portal/src/utils/sanitize-html.js` | ソース | HTMLサニタイズ |
