wheatandcatの開発ブログ

技術系記事を投稿してます

Polarでライセンスキー発行の仕組みを実装する

概要

現在 Mac アプリを開発中。
有料アプリとして販売する想定なので、課金とライセンスキー管理の仕組みを調べたところ、Polar を使うのが良さそうだったので紹介する。

Polarとは

polar.sh

  • 開発者向けの課金・ライセンス管理をまとめて提供するプラットフォーム
  • API 周りがかなり充実しており、ライセンスキーや関連情報を自前で管理する必要がない
  • サンドボックス環境が用意されている
  • 他サービスでは販売前に審査が必要なケースもあるが、Polar は売上が発生するまでは審査なしで進められるのでスムーズ

やりたいこと

  • ソフトウェア利用の年間ライセンスキーを販売
  • 販売する商品は「1年間有効なライセンスキー」
  • ライセンスキーの認証
  • ライセンスキーの期限や認証デバイス数の管理

実際にどのくらい自前実装が必要なのかと思いつつ進めたが、必要だったのは Polar API をライセンス判定処理に組み込む部分だけ だった。

実装

Polarのサンドボックス環境に入る

polar.sh

サンドボックス環境を使うことで、実際の決済を発生させずに動作確認ができる。

Polar のアカウントメニューを開き、Go to Sandbox をクリックするとサンドボックス環境に入れる。

商品を作成

New Product をクリックし、以下の設定で商品を作成する。

  • Recurring subscription で Every 1 year
  • 料金は 1,000 JPY
  • Create Benefit をクリックして License Keys を選択し、以下を設定
    • Expires 1 year
    • Limit Activations 3

販売リンクを作成

Create Checkout Link をクリックして販売リンクを作成する。

  • Label: ライセンスキー
  • Products: 先ほど作成した商品を選択

これで販売リンクが作成される。アクセスすると以下のような画面が表示される。

検証用のライセンスキーを発行

  • サンドボックス環境では商品を購入しても実際の決済は発生しない
  • 上記の販売リンクから適当に値を入力して購入する
  • 購入後、以下の画面にライセンスキーが表示される

ライセンスキーとデバイスを紐づけて有効化

Polar の customer-portal/license-keys/activate API を使って実装する。
- https://polar.sh/docs/api-reference/customer-portal/license-keys/activate

Mac アプリ起動時に以下の画面を表示し、ライセンスキーを入力させる。

Swift のコードは以下の通り。

class LicenseManager {
   (略)
    func saveLicenseKey(_ key: String) {
        keychainSave(key: keychainLicenseKey, value: key)
    }

    func saveActivationID(_ id: String) {
        keychainSave(key: keychainActivationIDKey, value: id)
    }

    /// ライセンスキーを認証してアクティベーションIDを保存する
    func activate(licenseKey: String) async throws {
        let url = URL(string: "\(baseURL)/license-keys/activate")!
        var req = URLRequest(url: url)
        req.httpMethod = "POST"
        req.setValue("application/json", forHTTPHeaderField: "Content-Type")

        let body = ActivateRequest(
            key: licenseKey,
            organizationID: organizationID,
            label: deviceName(),
            meta: ["device_id": deviceID()]
        )
        req.httpBody = try JSONEncoder().encode(body)

        let (data, response) = try await URLSession.shared.data(for: req)
        guard let http = response as? HTTPURLResponse else {
            throw LicenseError.networkError
        }
        guard http.statusCode == 200 else {
            let msg = (try? JSONDecoder().decode(PolarAPIError.self, from: data))?.message ?? "Unknown error"
            throw LicenseError.serverError(msg)
        }

        let result = try JSONDecoder().decode(ActivateResponse.self, from: data)
        saveLicenseKey(licenseKey)
        saveActivationID(result.id)
    }
}

ライセンスキーの認証

Polar の customer-portal/license-keys/validate API を使って実装する。
- https://polar.sh/docs/api-reference/customer-portal/license-keys/validate

Swift のコードは以下の通り。

class LicenseManager {
   (略)

    func loadLicenseKey() -> String? {
        keychainLoad(key: keychainLicenseKey)
    }

    func loadActivationID() -> String? {
        keychainLoad(key: keychainActivationIDKey)
    }

    /// 起動時にライセンスキーとアクティベーションIDを検証する
    func validate() async throws {
        guard let licenseKey = loadLicenseKey() else {
            throw LicenseError.serverError("ライセンスキーが見つかりません")
        }

        let url = URL(string: "\(baseURL)/license-keys/validate")!
        var req = URLRequest(url: url)
        req.httpMethod = "POST"
        req.setValue("application/json", forHTTPHeaderField: "Content-Type")

        let body = ValidateRequest(
            key: licenseKey,
            organizationID: organizationID,
            activationID: loadActivationID()
        )
        req.httpBody = try JSONEncoder().encode(body)

        let (data, response) = try await URLSession.shared.data(for: req)
        guard let http = response as? HTTPURLResponse else {
            throw LicenseError.networkError
        }
        guard http.statusCode == 200 else {
            let msg = (try? JSONDecoder().decode(PolarAPIError.self, from: data))?.message ?? "ライセンスが無効です"
            throw LicenseError.serverError(msg)
        }

        let result = try JSONDecoder().decode(ValidateResponse.self, from: data)
        guard result.status == "granted" else {
            throw LicenseError.serverError("ライセンスが無効または失効しています")
        }
    }
}

これでライセンスキー発行と認証の仕組みは一通り実装できた。

まとめ

  • 初めてライセンスキー決済まわりを実装したが、Polar を使うと想像以上にシンプルに進められた
  • Polar API にはクライアント向けの API も用意されているので、サーバーを自前実装しなくてもライセンス管理を組み込めるのはかなり便利だった