npm package: @feelflow/ffid-sdk
Description: FeelFlow ID Platform SDK for React/Next.js applications
Author: FeelFlow
Latest version: 2.3.0

# @feelflow/ffid-sdk

[![npm version](https://img.shields.io/npm/v/@feelflow/ffid-sdk.svg)](https://www.npmjs.com/package/@feelflow/ffid-sdk)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

FeelFlow ID Platform SDK — React/Next.js 向け + サーバーサイドモジュールはフレームワーク非依存。

**5行のコードでFFID認証を導入！**

## インストール

```bash
npm install @feelflow/ffid-sdk
# or
yarn add @feelflow/ffid-sdk
# or
pnpm add @feelflow/ffid-sdk
```

## 統合ガイド

サービスを FFID Platform に統合する詳細なガイドは **[Integration Guide](./docs/INTEGRATION_GUIDE.md)** を参照してください。OAuth フロー、フロントエンド/バックエンド実装パターン、セキュリティチェックリスト、アンチパターン集を網羅しています。

## クイックスタート

### 1. プロバイダーを設定（5行で完了！）

```tsx
// app/layout.tsx
import { FFIDProvider } from '@feelflow/ffid-sdk'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ja">
      <body>
        <FFIDProvider serviceCode="chatbot">{children}</FFIDProvider>
      </body>
    </html>
  )
}
```

### 2. 認証情報を使用

```tsx
import { useFFID, useSubscription } from '@feelflow/ffid-sdk'

function Dashboard() {
  const { user, isAuthenticated, login, logout } = useFFID()
  const { isActive, planCode } = useSubscription()

  if (!isAuthenticated) {
    return <button onClick={login}>ログイン</button>
  }

  return (
    <div>
      <p>Welcome, {user.displayName ?? user.email}!</p>
      <p>プラン: {planCode}</p>
      <button onClick={logout}>ログアウト</button>
    </div>
  )
}
```

## UIコンポーネント

```tsx
import {
  FFIDLoginButton,
  FFIDUserMenu,
  FFIDOrganizationSwitcher,
  FFIDSubscriptionBadge,
} from '@feelflow/ffid-sdk/components'

function Header() {
  return (
    <header>
      <FFIDLoginButton>ログイン</FFIDLoginButton>
      <FFIDOrganizationSwitcher />
      <FFIDSubscriptionBadge />
      <FFIDUserMenu />
    </header>
  )
}
```

### コンポーネントのスタイリング（classNames パターン）

各UIコンポーネントは [Radix UI スタイルパターン](https://www.radix-ui.com/primitives/docs/guides/styling) に従った `classNames` プロップをサポートしています。これにより、コンポーネントの各パーツに個別のクラスを適用できます。

#### FFIDUserMenu

```tsx
<FFIDUserMenu
  classNames={{
    container: 'relative',          // ラッパー要素
    button: 'focus:ring-2',         // アバターボタン（トリガー）
    avatar: 'rounded-full',         // アバター画像/フォールバック
    menu: 'shadow-lg',              // ドロップダウンメニュー
    userInfo: 'border-b',           // ユーザー情報セクション
    menuItem: 'hover:bg-gray-100',  // カスタムメニュー項目
    logout: 'text-red-600',         // ログアウトボタン
  }}
/>
```

#### FFIDOrganizationSwitcher

```tsx
<FFIDOrganizationSwitcher
  classNames={{
    container: 'relative',           // ラッパー要素
    button: 'border rounded',        // トリガーボタン
    dropdown: 'shadow-md',           // ドロップダウンメニュー
    option: 'px-4 py-2',             // 各組織オプション
    optionSelected: 'bg-blue-50',    // 選択中の組織（optionに加えて適用）
  }}
/>
```

#### FFIDSubscriptionBadge

```tsx
<FFIDSubscriptionBadge
  classNames={{
    badge: 'font-semibold',  // バッジspan要素
  }}
/>
```

> **Note**: `FFIDLoginButton` はシンプルな単一要素コンポーネントのため、標準の `className` プロップのみをサポートしています。

## API リファレンス

### FFIDProvider

アプリケーション全体をラップするプロバイダーコンポーネント。

```tsx
<FFIDProvider
  serviceCode="chatbot"     // 必須: サービスコード
  apiBaseUrl="..."          // オプション: カスタムAPIエンドポイント
  debug={true}              // オプション: デバッグログ有効化（非推奨、loggerを使用）
  logger={customLogger}     // オプション: カスタムロガー（下記参照）
  refreshInterval={300000}  // オプション: セッション更新間隔（ms）
  onAuthStateChange={(user) => {}}  // オプション: 認証状態変更時コールバック
  onError={(error) => {}}   // オプション: エラー時コールバック
>
  {children}
</FFIDProvider>
```

### カスタムロガー

SDKのデバッグ出力をカスタマイズできます。デフォルトではログは出力されません（サイレント）。

```tsx
import type { FFIDLogger } from '@feelflow/ffid-sdk'
import pino from 'pino' // または winston, bunyan 等

// アプリケーションのロガーインスタンス
const appLogger = pino({ level: 'debug' })

// FFID SDK用にラップ
const ffidLogger: FFIDLogger = {
  debug: (...args) => appLogger.debug({ sdk: 'ffid' }, ...args),
  info: (...args) => appLogger.info({ sdk: 'ffid' }, ...args),
  warn: (...args) => appLogger.warn({ sdk: 'ffid' }, ...args),
  error: (...args) => appLogger.error({ sdk: 'ffid' }, ...args),
}

// 使用例
<FFIDProvider serviceCode="chatbot" logger={ffidLogger}>
  {children}
</FFIDProvider>
```

**ロガー優先順位:**

1. `logger` が指定されている場合 → カスタムロガーを使用
2. `debug: true` で `logger` なし → `console` を使用（後方互換性）
3. 両方なし → サイレント（no-op）

### useFFID()

ユーザー・組織情報を取得するフック。

```tsx
const {
  user,               // FFIDUser | null - 現在のユーザー
  organizations,      // FFIDOrganization[] - 所属組織一覧
  currentOrganization, // FFIDOrganization | null - 現在の組織
  isLoading,          // boolean - ロード中
  isAuthenticated,    // boolean - 認証済み
  login,              // () => void - ログインページへリダイレクト
  logout,             // () => Promise<void> - ログアウト
  switchOrganization, // (id: string) => void - 組織切り替え
  refresh,            // () => Promise<void> - セッション更新
} = useFFID()
```

### useSubscription()

契約情報を取得するフック。

```tsx
const {
  subscription,  // FFIDSubscription | null - 現在のサブスクリプション
  planCode,      // string | null - プランコード
  isActive,      // boolean - アクティブ契約
  isTrialing,    // boolean - トライアル中
  isCanceled,    // boolean - 解約済み
  hasPlan,       // (plans: string | string[]) => boolean - プラン確認
  hasAccess,     // () => boolean - アクセス権確認
} = useSubscription()
```

### withSubscription()

サブスクリプション確認HOC。

```tsx
const PremiumFeature = withSubscription(MyComponent, {
  plans: ['pro', 'enterprise'],
  fallback: <UpgradePrompt />,
  loading: <Spinner />,
})
```

## 型定義

```typescript
interface FFIDUser {
  id: string
  email: string
  displayName: string | null
  avatarUrl: string | null
  locale: string | null
  timezone: string | null
  createdAt: string
}

interface FFIDOrganization {
  id: string
  name: string
  slug: string
  role: 'owner' | 'admin' | 'member'
  status: 'active' | 'invited' | 'suspended'
}

interface FFIDSubscription {
  id: string
  serviceCode: string
  serviceName: string
  planCode: string
  planName: string
  status: 'trialing' | 'active' | 'past_due' | 'canceled' | 'paused'
  currentPeriodEnd: string | null
}
```

### OAuth userinfo の契約要約

token mode では SDK は `/api/v1/oauth/userinfo` を呼び出し、基本プロフィールに加えてサービス契約の要約を受け取ります。
この要約により、追加 API を呼ばずにプラン判定や UI 分岐を行えます。

```ts
interface FFIDOAuthUserInfoSubscription {
  subscriptionId: string | null
  status: 'trialing' | 'active' | 'past_due' | 'canceled' | 'paused' | null
  planCode: string | null
  seatModel: 'organization' | null
  memberRole: 'owner' | 'admin' | 'member' | 'viewer' | null
  organizationId: string | null
}
```

`seatModel` はシートモデル識別用であり、organization と role は userinfo の解決済み組織文脈として扱います。

## React 以外の環境で使う

本 SDK は React/Next.js 向けに設計されていますが、一部のモジュールはフレームワーク非依存で利用できます。

### サーバーサイドモジュール（React 依存なし）

以下の subpath exports は React に一切依存しません。Node.js、Deno、Bun 等で即座に利用できます。

```typescript
// 利用規約・法的文書
import { createFFIDLegalClient } from '@feelflow/ffid-sdk/legal'

// Agency（代理店）管理
import { createFFIDAgencyClient } from '@feelflow/ffid-sdk/agency'

// お知らせ取得
import { createFFIDAnnouncementsClient } from '@feelflow/ffid-sdk/announcements'

// Webhook 署名検証・ハンドラー（※ Node.js crypto が必要）
import { createFFIDWebhookHandler, verifyWebhookSignature } from '@feelflow/ffid-sdk/webhooks'
```

> **Note**: `webhooks` モジュールは Node.js の `crypto` モジュールと `Buffer` を使用します。Cloudflare Workers で利用する場合は [`nodejs_compat` 互換フラグ](https://developers.cloudflare.com/workers/runtime-apis/nodejs/)を有効にしてください。

これらのモジュールは独立した subpath export として公開されているため、メインエントリ (`@feelflow/ffid-sdk`) を経由せず、React の依存が伝播しません。

### `createFFIDClient` を非 React 環境で使う

非 React 環境では、可能な限り上記の subpath exports（`/legal`、`/webhooks` 等）の個別クライアントを使用してください。メインエントリの `createFFIDClient` を使う必要がある場合、`createFFIDClient` 自体は React を使用しませんが、メインエントリに含まれるため **bundler 環境では React を external に指定する**必要があります。

#### Cloudflare Workers

ビルドコマンドで `--external` を指定するか、カスタム esbuild 設定で `external` を設定してください。

```bash
# wrangler のビルドコマンド例
esbuild src/index.ts --bundle --format=esm --external:react --external:react-dom
```

#### Vue / Nuxt（Vite）

```typescript
// vite.config.ts
export default defineConfig({
  build: {
    rollupOptions: {
      external: ['react', 'react-dom'],
    },
  },
})
```

#### esbuild

```bash
esbuild src/index.ts --bundle --external:react --external:react-dom
```

#### webpack

```javascript
// webpack.config.js
module.exports = {
  externals: {
    react: 'react',
    'react-dom': 'react-dom',
  },
}
```

> **Note**: サーバーサイドで bundler を使わずに実行する場合、**subpath exports（`@feelflow/ffid-sdk/legal` 等）を使用すれば** external 設定なしで React がインストールされていなくても動作します。メインエントリ（`@feelflow/ffid-sdk`）を ESM で import する場合は、モジュールグラフが静的に解決されるため React が必要です。

### `peerDependencies` は optional です

SDK の `package.json` で `react` / `react-dom` は `optional: true` に設定済みです（利用者側での設定は不要）。React をインストールしなくても `npm install` 時に warning は発生しません。

```json
// SDK の package.json に設定済み（参考）
{
  "peerDependenciesMeta": {
    "react": { "optional": true },
    "react-dom": { "optional": true }
  }
}
```

## 環境変数

オプションで環境変数を使用してデフォルト設定を上書きできます：

```bash
NEXT_PUBLIC_FFID_API_URL=https://id.feelflow.net
NEXT_PUBLIC_FFID_SERVICE_CODE=chatbot
```

## ライセンス

MIT