技術記事投稿サービスに誰でも有料記事を投稿できる機能を作った

僕は趣味で[Crieit](https://crieit.net/)というQiitaのような感じのもっとゆるいサービスを運営しています。そちらで有料記事の販売機能を開発しました。ユーザーが自由に記事を販売することが出来るようになっています。開発にはStripeという決済サービスのStripe Connectという機能を使っています。

実際の作り方や考える必要があった事などをまとめていきます。

## Stripe Connectとは

まずStripeというのは簡単にアプリケーションに決済機能を導入することができるサービスです。そしてStripeにはStripe Connectという機能があり、ECサイトや今回の技術記事のように、サービス内でユーザーに商品を販売する機能を提供することが可能になります。

サービス運営者は手数料を設定することで収益を得ることが出来ます。例えばStripe Connectを使わずに振り込みで運用する場合は一度全ての商品の支払いはサービス運営者のところに振り込んでもらい、手数料を引いた分を各販売ユーザーに振り込みする、という手間と費用がかかってしまいます。

Stripe Connectを使うとそうする必要がなく、Stripe側が費用を分配してくれるため安心してユーザーに販売機能を提供することができるようになります。

ちなみにStripe Connect自体はこちらの記事にて詳しく解説されていますので、実際に利用したい方は一読しておくと良いでしょう。

[Stripe Connect 101](https://qiita.com/y_toku/items/7bfa42793801dfc5415d)

## 実装した環境

下記の環境で実装しました。

* Laravel5.8
* Vue.js2.5

Stripeのドキュメントを見るとわかりますが、主要な言語に対応した言語のSDKが用意されていますのでPHPに限らず多くのシステムで利用することが出来ます。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136506/ba749b81-dfb3-12ed-1bbe-4c50489b92ae.png)

## Stripeアカウントの準備

何にしろまずはStripeアカウントが必要となりますので登録しましょう。そして管理画面でConnectを有効にします。「テストデータの表示中」をオンにしておけば本番ではなく全てサンドボックスとしての動作になりますので事前にオンにしておきます。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136506/e315befd-e5c3-9c5b-6a6b-e498c7570509.png)

有効化する際におそらくビジネス名やなんだかんだの入力をする必要があると思いますので必要なところを入力します。

あとは開発者のAPIキーメニューで公開可能キーとシークレットキー、設定のConnect画面でクライアントIDが発行されるため、その３つをメモしておきましょう。StripeのAPIを利用する時に必要です。

そして商品の販売者にはOAuthを利用してStripeアカウントでログインしてもらう必要があります。そのためアプリケーションに戻ってくるためのリダイレクトURLを登録しておく必要がありますのでそれを追加しておきます。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136506/40725995-c00d-4c2d-7c13-ebe813b34a2a.png)

### 販売者もStripeユーザー

前述の通りサービス運営者はStripeアカウントが必要ですが、サービス内で商品を販売するユーザーもStripeアカウントが必要となります。これはOAuthログインしてもらう際に登録してもらう形となります。既にStripeアカウントを持っている場合はそちらを利用してもらうこともできるようです。

要するに販売者は、サービス内で管理してもらう販売者ユーザーという形ではなく、販売者自体が独立したStripeアカウントを持った一事業オーナーとなり、手数料部分だけサービスと連携しているだけという形になるということです。これは事前に説明しておく必要があるでしょう。連携時の登録方法も分かりづらいため説明がないと離脱する可能性もかなり高いと思います。

僕のサービスでも下記のように解説を入れています。

[Stripeアカウントの連結手順 - Crieit](https://crieit.net/posts/stripe-connect-registration)

## 実際の開発方法

LaravelとVue.jsを使って開発していますのでそれを例に説明していきます。

### API利用のための設定

まずは公開可能キーとシークレットキー、クライアントIDを設定します。

```env:.env
STRIPE_KEY=
STRIPE_SECRET=
STRIPE_CLIENT_ID=
```

```php:config/services.php
    'stripe' => [
        'model' => App\Models\User::class,
        'key' => env('STRIPE_KEY'),
        'secret' => env('STRIPE_SECRET'),
        'client_id' => env('STRIPE_CLIENT_ID'),
    ],
```

### 販売ユーザーにログインしてもらう

あとは販売ユーザーにStripe認証をしてもらいます。こんな感じでURLを作ってリダイレクトするだけです。

```php
    public function authenticate(string $state): string
    {
        $query = [
            'response_type' => 'code',
            'client_id' => config('services.stripe.client_id'),
            'scope' => 'read_write',
            'redirect_uri' => url('login/stripe/callback'),
            'state' => $state,
        ];
        return 'https://connect.stripe.com/oauth/authorize?' . http_build_query($query);
    }
```

redirect_uriはStripe側に登録したものと同じものです。stateはStripeから戻ってきた時に渡される値です。セッション等に保存したものを渡し、戻ってきた時に照合して確認できます。

次にStripe側から戻ってきた時の処理です。

```php
    public function handleStripeCallback(Request $request)
    {
        if (session('stripeState') != $request->input('state')) {
            abort(422);
        }

        $request->session()->forget('stripeState');

        if ($request->input('error')) {
            return redirect('show-error');
        }

        $code = $request->input('code');
        $client = new \GuzzleHttp\Client;
        $response = $client->request('post', 'https://connect.stripe.com/oauth/token', [
            'form_params' => [
                'client_secret' => config('services.stripe.secret'),
                'code' => $code,
                'grant_type' => 'authorization_code',
            ],
        ]);
        $stripeUser = json_decode($response->getBody()->getContents());

        // $stripeUser->stripe_user_id をユーザーデータに保存しておく
    }
```

これで販売者ユーザーの準備は完了です。あとは決済処理を実装すれば実際に販売してもらうことが可能です。この時点で販売準備が整っていないとならないため、認証をしてもらう前にでも特定商取引法に基づく表記を登録しておいてもらい、登録した人だけ認証できるようにしておく等しましょう。

## 購入処理

次に一般ユーザーが購入できるようにします。購入情報などを保存する必要があるため、ユーザー登録をしていない場合はユーザー登録を促します。また、Stripeに送る情報としてメールアドレスが必要ですので、これも予め登録していないユーザーがいたら登録を促します。問題なければ購入ボタンを表示します。

### 決済の基本的な仕組み

基本的に、クレジットカード情報などをサーバーに通す事はできません。独自に保存などを行う場合は定められたセキュリティ基準に従う必要があるため、個人や小さな企業には不可能です。そのためJavaScriptで直接決済サービス側に決済情報を送り、代わりに決済情報のトークンを受け取り、サーバー側ではそのトークンを使って決済サービスと連携して決済を行う形が一般的に容易な形です。

### トークンを取得する

まずはJavaScript側でトークンを取得します。まずはStripeのライブラリを読み込みます。

```html
<script src="https://js.stripe.com/v3/"></script>
```

カード情報の入力欄を表示するためのコンテナを用意します。

```html
<div id="card-element"></div>
```

あとは入力欄を初期化します。apiKeyは一般公開キーです。

```javascript
  mounted() {
    this.stripe = window.Stripe(this.apiKey)
    const elements = this.stripe.elements()
    this.card = elements.create('card')
    this.card.mount('#card-element')
    this.card.addEventListener('change', this.onCardChanged.bind(this))
  }
```

changeイベントはエラーが出た場合にエラーを受け取って適宜必要なメッセージ等を表示することが出来ます。

```javascript
  onCardChanged(event) {
    this.isCardEmpty = event.empty
    if (event.error) {
      this.error = event.error.message
    } else {
      this.error = ''
    }
  }
```

あとは決済実行ボタンが押されたらトークンを作成してサーバーに送信します。

```javascript
  async submit() {
    const data = {}

    const result = await this.stripe.createToken(this.card)
    if (result.error) {
      this.error = result.error.message
      return
    }
    data.token = result.token.id

    const response = await axios.post(this.url, data)
  }
```

### トークンから顧客情報の作成

毎回トークンを使っても良いのですが、トークンを使ってStripeのCustomerデータを作成することができ、以後はそちらで決済することも出来ます。

```php
$stripeCustomer = \Stripe\Customer::create([
    'source' => $token,
    'email' => $email,
]);

// $stripeCustomer->idは購入ユーザーデータに紐づけておく
```

このあたりのカスタマーIDにしろ何にしろ、漏れるとまずいと思いますので表には出さないようにしましょう。「フロントの人が楽になるだろうからユーザーデータを毎回丸々JSONで渡してlocalStroageに保存しときゃいいじゃん？」とかはやめましょう。保存するテーブルも分けておくのが良いと思います。

次に、カスタマーIDはトークンではないのでこれで購入はできません。この場合は販売者IDと連携させたトークンをその都度作る必要があります。

```php
    $token = \Stripe\Token::create(
        ['customer' => $customer->id],
        ['stripe_account' => $saleUser->stripe_id]
    );
```

あとは実際に購入処理を行います。手数料もここで渡します。

```php
    $data = [
        'amount' => $post->price,
        'currency' => 'jpy',
        'source' => $token->id,
        'description' => $post->name,
        'application_fee_amount' => floor($post->price * config('data.stripe.chargeFeeRatio') / 100),
    ];

    $charge = \Stripe\Charge::create($data, ['stripe_account' => $saleUser->stripe_id]);
```

あとは購入ログを保存しておきましょう。ログを元に購入済みかどうかを判断し、購入済みのユーザーにだけ有料記事部分を見せるようにします。

### データの保護

基本的に何かの間違いで漏洩してしまわないよう、有料記事データもStripe関連のデータもすべて別テーブルに保存するようにしました。間違ってユーザーや記事の全データをJSONでフロント側に渡してしまったとしても何も漏洩しません。

購入済みかどうかもデータをそのまま渡すのではなく、boolean型にして渡せば安心です。

## その他

今回は記事の購入だけでしたが、サブスクリプションなどを提供する場合には契約が継続中かを確認する必要もあります。契約は販売者のStripeアカウントで自由にキャンセルなども出来てしまいます。そのため、その状態とシステムを連動させる必要があります。

Webhookで変更状態を受け取ることも出来ますし、月更新であれば毎月どこかで確認するなどでも良いでしょう。ちなみにWebhookは送信した詳細をStripeのダッシュボードで見ることなども出来るため便利です。

## まとめ

以上がStripe Connectを利用した有料記事販売機能の作り方です。実装自体はそんなに難しくはないです。Stripeのマニュアルにも細かいパラメータは載っているのでそのあたりを参考にして必要な機能を実装していくことは出来ます。

ただし安全に実装するためのシステム構成を考えたり、問題のある手順が無いかを確認したりなど、結合テストは入念に行う必要があるためぼちぼち大変です。あとは説明など、開発以外のところも色々と準備が必要です。

とはいえ決済会社との連携もなくガツガツ進めていけるため分かってしまえばすぐ作ることが出来ます。ぜひ機会があれば試してみてください。

下記は僕のサービスの有料記事機能紹介ページです。

[有料記事を販売できるようになりました](https://crieit.net/posts/dbaf3ad259cbec4bc20c2c9f2538e4ba)

なにか参考になるところがあったらぜひLGTMをお願いします！