iOSエンジニアのつぶやき

毎朝8:30に iOS 関連の技術について1つぶやいています。まれに釣りについてつぶやく可能性があります。

Stripe Connect について少し調べる

個人で開発しているアプリで、CtoC の決済を実現するために Stripe Connect について少しだけ調べたのでまとめておきます👷‍♀️ (実際に利用したらまた記事書きます)

f:id:yum_fishing:20201201002102p:plain

アカウントのタイプ

Stripe Connect を導入するにあたり、アカウントタイプというものをビジネスシーンに合わせて3種類の中から選ぶことができるようです。

Standard

こちらのアカウントタイプを選択した場合は、売り手が Stripe のフォーム上でアカウントの作成やダッシュボードによる決済の管理をすることができます。これによりプラットフォーム(サービスの提供者)は、OAuth の Stripe 組み込みを実装するだけで、CtoC の決済処理を開始することができるようです👀

Custom

Custom アカウントは、登録フローや決済のダッシュボード管理などをプラットフォームがカスタマイズして作成できるアカウントタイプです。また、Standard と違い送金などのタイミングもプラットフォーム側でコントロールすることができるため、より柔軟な決済フローを構築することができます。

Express

Express は、Standard と Custom アカウントの中間のようなアカウントのタイプで、Custom のように決済の流れを制御しつつ Stripe から提供されているダッシュボードや登録フローを使用することで、Stripe とプラットフォームのブランドの調和をとることができます。

Standard Account + iOS

Standard Account + iOS での決済フローの手順を簡単に見ていきましょう。

大まかな手順は下記のようになります。4. については、Connect に限らず Stripe による決済での共通的な処理が多いので割愛します(また明日書くかもしれません)。

  1. *Connected Account の作成(サーバ)
  2. Account Links の作成(サーバ)
  3. 取得したリンクにユーザをリダイレクトする(クライアント)
  4. 一般ユーザ支払いをハンドリング

1. Connected Account の作成

/v1/accounts API を使用して、Standard のアカウントを作成します。

API: https://stripe.com/docs/api/accounts/create

// Set your secret key. Remember to switch to your live secret key in production!
// See your keys here: https://dashboard.stripe.com/account/apikeys
const stripe = require('stripe')('sk_test_4eC39HqLyjWDarjtT1zdp7dc');

const account = await stripe.accounts.create({
  type: 'standard',
});

また、Connected Account を作成するのに必要な、情報を事前に入力している場合は、それらの情報をパラメータに渡すことで、Stripe プラットフォーム上でのフォーム入力の際に、それらの情報をユーザ再入力する必要がなくなるそうです。ですので、極力事前にユーザから取得した情報はパラメータに付与することで少しでも、フローの離脱率を下げることに貢献できそうです🧑‍🔧

2. Account Links の作成

このプロセスは、ConnectOnboarding などの Stripe がホストするアプリケーションにアクセスするための URL を取得するために必要です。

API: https://stripe.com/docs/api/account_links

// Set your secret key. Remember to switch to your live secret key in production!
// See your keys here: https://dashboard.stripe.com/account/apikeys
const stripe = require('stripe')('sk_test_4eC39HqLyjWDarjtT1zdp7dc');

const accountLinks = await stripe.accountLinks.create({
  account: 'acct_1032D82eZvKYlo2C',
  refresh_url: 'https://example.com/reauth',
  return_url: 'https://example.com/return',
  type: 'account_onboarding',
});

これらのリクエストには、下記のパラメータが必要になります。

  • account
    • Account の ID
  • type
    • account_onboardingaccount_update がありますが、Standard Account の場合、指定できるのは account_onboarding のみになります。
  • return_url
    • ユーザが ConnectOnboarding フローを完了した時に、この URL へのリダイレクトを発行します。
  • refresh_url
    • ConnectOnboarding フローで次のようなアクションが発生した場合に、この URL にリダイレクトされます。
      • リンクの有効期限が切れた場合
      • ユーザがページを更新するか、ブラウザで前後をクリック
      • プラットフォームがアカウントにアクセスできなくなった場合
      • アカウントが拒否された場合

また、return_url でリダイレクトされた場合でも、ConnectOnboarding フローが正常に完了したとは限らないので、次のいずれかを実行して、アカウントの details_submitted パラメータの状態を確認します。

  • account.updated webhooks を監視
  • Accounts API でアカウント情報を取得

また、iOS の場合これら return_urlrefresh_url にユニバーサルリンクを設定することでよりシームレスに iOS アプリにリダイレクトすることができます。

※ 本番環境(Live Mode)では HTTPS しか使用できないので注意が必要

3. 取得したリンクにユーザをリダイレクトする

前の手順で取得した ConnectOnboarding フローのためのリンクにユーザをリダイレクトします。下記が Stripe Doc の Swift サンプルコードです。

import UIKit
import SafariServices

class ConnectOnboardViewController: UIViewController {

    // ...

    override func viewDidLoad() {
        super.viewDidLoad()

        let connectWithStripeButton = UIButton(type: .system)
        connectWithStripeButton.setTitle("Connect with Stripe", for: .normal)
        connectWithStripeButton.addTarget(self, action: #selector(didSelectConnectWithStripe), for: .touchUpInside)
        view.addSubview(connectWithStripeButton)

        // ...
    }

    @objc
    func didSelectConnectWithStripe() {
        if let url = URL(string: Settings.BackendAPIBaseURL)?.appendingPathComponent("onboard-user") {
          var request = URLRequest(url: url)
          request.httpMethod = "POST"
          let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
              guard let data = data,
                  let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String : Any],
                  let accountURLString = json["url"] as? String,
                  let accountURL = URL(string: accountURLString) else {
                      // handle error
              }

              let safariViewController = SFSafariViewController(url: url)
              safariViewController.delegate = self

              DispatchQueue.main.async {
                  self.present(safariViewController, animated: true, completion: nil)
              }
          }
        }
    }

    // ...
}

extension ConnectOnboardViewController: SFSafariViewControllerDelegate {
    func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
        // the user may have closed the SFSafariViewController instance before a redirect
        // occurred. Sync with your backend to confirm the correct state
    }
}

用語

Connected Account: アプリを使用してサービスを提供する販売者のアカウント

参考

その他の記事

yamato8010.hatenablog.com

yamato8010.hatenablog.com

yamato8010.hatenablog.com