# 画面設計書 86-マジックリンクページ

## 概要

本ドキュメントは、Ghost会員向けポータル（Portal）のマジックリンクページ（MagicLinkPage）の画面設計書である。サインイン/サインアップ後にマジックリンクメール送信完了を通知し、ワンタイムコード入力を受け付ける画面を定義する。

### 本画面の処理概要

マジックリンクページは、Ghost会員サイトのPortalウィジェット内で、パスワードレス認証のメール送信完了を通知し、オプションでワンタイムコード（OTC）による認証を可能にする画面である。

**業務上の目的・背景**：パスワードレス認証は、ユーザーがパスワードを覚える必要がなく、セキュリティとユーザビリティを両立する認証方式である。マジックリンクメールを送信後、ユーザーにメール確認を促すこの画面は、認証フローの重要な中間ステップである。さらに、ワンタイムコード入力機能により、メールクライアントを開かずに即座にログインできる利便性も提供する。

**画面へのアクセス方法**：サインインページまたはサインアップページでメールアドレスを送信後、自動的にこの画面に遷移する。直接URL（`#/portal/magiclink`）でのアクセスも可能だが、通常は認証フローの一部として表示される。

**主要な操作・処理内容**：
1. マジックリンクメール送信完了メッセージの表示
2. ワンタイムコード（OTC）入力フォームの表示（OTC機能有効時）
3. OTCの検証と認証完了処理
4. メールクライアントを開くボタン（Sniper Links機能有効時）
5. ポップアップを閉じる操作

**画面遷移**：
- 遷移元：サインインページ（signin）、サインアップページ（signup）
- 遷移先：認証成功後のリダイレクト先、閉じる（ポップアップ終了）

**権限による表示制御**：
- 未認証状態でアクセス可能
- OTC機能の有効/無効に応じて入力フォームの表示が変化
- Sniper Links機能の有効/無効に応じてボタン表示が変化

## 関連機能

| 機能No | 機能名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 9 | メンバー認証 | 主機能 | マジックリンク送信完了の表示 |
| 71 | Portal | 補助機能 | Portalウィジェット内でのUI表示 |

## 画面種別

完了・入力

## URL/ルーティング

- ハッシュルート：`#/portal/magiclink`
- Pages.jsでの登録キー：`magiclink`

## 入出力項目

| 項目名 | 項目ID | 型 | 必須 | 入力/出力 | 説明 |
|--------|--------|-----|------|----------|------|
| ワンタイムコード | otc | string | ○（OTC機能有効時） | 入力 | 6桁の数字コード |

## 表示項目

| 項目名 | データソース | 説明 |
|--------|-------------|------|
| ページタイトル | - | "Now check your email!" 固定 |
| 説明文 | pageData.email, lastPage, otcRef | メール送信先と確認方法の説明 |
| OTC入力フォーム | otcRef | ワンタイムコード入力欄（OTC有効時） |
| 封筒アイコン | - | メール確認を促すアイコン |
| メールを開くボタン | sniperLinks | メールクライアント起動ボタン |

## イベント仕様

### 1-ワンタイムコード入力

**トリガー**：OTC入力フォームへの入力

**処理フロー**：
1. handleInputChange で入力値を検証（数字のみ許可）
2. state.otc に値を保存
3. 6桁入力完了時：自動でdoVerifyOTC()を実行

**バリデーション**：
- 数字のみ許可（正規表現: `/[^0-9]/g`）
- 最大6文字
- 入力完了時の自動送信

### 2-ワンタイムコード検証

**トリガー**：「Continue」ボタンクリック、またはEnterキー押下、または6桁入力完了

**処理フロー**：
1. 入力値の存在確認（空の場合はエラー「Enter code above」）
2. エラーなしの場合：`doAction('verifyOTC', {otc, otcRef, redirect})` 実行
3. 検証成功時：リダイレクト先へ遷移
4. 検証失敗時：エラーメッセージ表示

**API呼び出し**：
- POST `/members/api/verify-otc/`

### 3-ポップアップを閉じる

**トリガー**：「Close」ボタンクリック（OTC無効時）、または CloseButton クリック

**処理フロー**：
1. `doAction('closePopup')` 実行
2. ポップアップを閉じる

### 4-メールを開く（Sniper Links）

**トリガー**：「Open email」ボタンクリック

**処理フロー**：
1. デバイスに応じたSniper Linksを使用
   - Android Chrome: sniperLinks.android
   - その他: sniperLinks.desktop
2. メールクライアントを起動

### 5-サインインに戻る

**トリガー**：「Back to Log in」リンククリック

**処理フロー**：
1. `doAction('switchPage', {page: 'signin'})` 実行
2. サインインページへ遷移

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

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

| 操作（イベント） | 対象テーブル | 操作種別 | 概要 |
|----------------|-------------|---------|------|
| OTC検証成功 | members_login_sessions | INSERT | ログインセッション作成 |

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

#### members_login_sessions

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | member_id | 認証された会員ID | OTC検証成功時 |
| INSERT | created_at | 現在日時 | - |

## メッセージ仕様

| メッセージID | 種別 | メッセージ内容 | 表示条件 |
|-------------|------|--------------|---------|
| MSG-001 | タイトル | "Now check your email!" | 常時 |
| MSG-002 | 説明 | "An email has been sent to {email}. Click the link inside or enter your code below." | OTC有効時（signin） |
| MSG-003 | 説明 | "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder." | OTC無効時（signin） |
| MSG-004 | 説明 | "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!" | signup時 |
| MSG-005 | ボタン | "Continue" | OTCフォーム送信ボタン |
| MSG-006 | ボタン | "Verifying..." | OTC検証中 |
| MSG-007 | ボタン | "Close" | OTC無効時の閉じるボタン |
| MSG-008 | ボタン | "Open email" | Sniper Links有効時 |
| MSG-009 | エラー | "Enter code above" | OTC未入力時 |
| MSG-010 | リンク | "Back to Log in" | サインインへ戻るリンク |
| MSG-011 | プレースホルダー | "------" | OTC入力欄のプレースホルダー |

## 例外処理

| 例外条件 | 処理内容 | 表示メッセージ |
|---------|---------|--------------|
| OTC未入力 | エラーメッセージ表示 | "Enter code above" |
| OTC検証失敗 | エラーメッセージ表示、リトライ可能 | サーバーからのエラーメッセージ |
| API通信エラー | エラー状態、リトライボタン表示 | - |

## 備考

- OTC入力フォームは `otcRef` が存在する場合のみ表示される
- OTC入力は数字のみ許可され、6桁入力時に自動送信される
- Sniper Links は `site.labs?.sniperlinks` が有効な場合に表示
- Android Chrome の場合は Android 用の Sniper Links を使用
- メールアドレスが pageData に含まれない場合は "your inbox" と表示

---

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

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

### 推奨読解順序

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

マジックリンクページで扱う主要なデータ構造を把握する。

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

**読解のコツ**: otcRefがOTC機能の有効/無効を判定する鍵。pageData.emailにメール送信先が含まれる。

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

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

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

**主要処理フロー**:
1. **80-89行**: クラスコンポーネント定義、stateの初期化
2. **126-146行**: renderFormHeaderでタイトルと説明文表示
3. **301-311行**: renderでOTCフォーム or 閉じるボタンの表示分岐

#### Step 3: メッセージ生成ロジックを理解する

lastPageとotcRefに応じた説明文の生成を把握する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | magic-link-page.js | `apps/portal/src/components/pages/magic-link-page.js` | getDescriptionConfig、getTranslatedDescription |

**主要処理フロー**:
- **97-105行**: getDescriptionConfigでメッセージテンプレート定義
- **115-124行**: getTranslatedDescriptionでlastPage/otcRefに応じたメッセージ選択

#### Step 4: OTC入力・検証処理を理解する

OTC入力フォームと検証処理を把握する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | magic-link-page.js | `apps/portal/src/components/pages/magic-link-page.js` | renderOTCForm、handleInputChange、doVerifyOTC |

**主要処理フロー**:
- **242-299行**: renderOTCFormでOTC入力UIレンダリング
- **220-240行**: handleInputChangeで入力値検証と自動送信
- **198-218行**: doVerifyOTCで検証アクション実行

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

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

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

**主要処理フロー**:
- **328-355行**: verifyOTCでPOSTリクエスト送信

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

```
MagicLinkPage (magic-link-page.js)
    │
    ├─ constructor()
    │      └─ state: {otc: '', errors: {}, isFocused: false}
    │
    ├─ renderFormHeader()
    │      ├─ getTranslatedDescription()
    │      │      └─ getDescriptionConfig()
    │      │
    │      └─ EnvelopeIcon + タイトル + 説明文
    │
    ├─ render()
    │      ├─ CloseButton
    │      ├─ renderFormHeader()
    │      │
    │      └─ showOTCForm (otcRef有無で分岐)
    │             ├─ true → renderOTCForm()
    │             └─ false → renderCloseButton()
    │
    ├─ renderOTCForm()
    │      ├─ input (OTC入力欄)
    │      │      └─ onChange → handleInputChange()
    │      │             └─ 6桁入力時 → doVerifyOTC()
    │      │
    │      └─ ActionButton (Continue)
    │             └─ onClick → handleSubmit()
    │                    └─ doVerifyOTC()
    │
    └─ doVerifyOTC()
           ├─ バリデーション (空チェック)
           └─ doAction('verifyOTC', {otc, otcRef, redirect})
                  └─ api.member.verifyOTC()
```

### データフロー図

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

AppContext
  ├─ otcRef ───────────▶ OTCフォーム表示判定 ─────────▶ フォーム or 閉じるボタン
  ├─ pageData.email ───▶ getTranslatedDescription() ──▶ 説明文
  ├─ lastPage ─────────▶ メッセージ選択 ──────────────▶ signin/signup用メッセージ
  └─ sniperLinks ──────▶ renderCloseButton() ─────────▶ Open email or Close

ユーザー入力
  │
  ├─ OTC入力 ──────────▶ handleInputChange()
  │                           │
  │                           ├─ 数字以外除去
  │                           ├─ state.otc更新
  │                           │
  │                           └─ 6桁完了時 → doVerifyOTC()
  │
  └─ Continueボタン ───▶ handleSubmit()
                              │
                              └─ doVerifyOTC()
                                     │
                                     ├─ バリデーション
                                     │
                                     └─ doAction('verifyOTC')
                                            │
                                            └─ api.member.verifyOTC()
                                                   │
                                                   └─▶ 認証成功 → リダイレクト
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| magic-link-page.js | `apps/portal/src/components/pages/magic-link-page.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通信処理 |
| action-button.js | `apps/portal/src/components/common/action-button.js` | ソース | アクションボタンコンポーネント |
| close-button.js | `apps/portal/src/components/common/close-button.js` | ソース | 閉じるボタンコンポーネント |
| sniper-link-button.js | `apps/portal/src/components/common/sniper-link-button.js` | ソース | Sniper Linksボタンコンポーネント |
| envelope.svg | `apps/portal/src/images/icons/envelope.svg` | アセット | 封筒アイコン |
| is-android-chrome.js | `apps/portal/src/utils/is-android-chrome.js` | ソース | Android Chrome判定ユーティリティ |
| i18n.js | `apps/portal/src/utils/i18n.js` | ソース | 国際化ユーティリティ |
