個人事業主がmacOSアプリを直接配布するまでの道のり③: ライセンス機能の実装と、ついに販売開始まで

これは「個人事業主が Mac App Store を経由せず、自分の Web サイトから macOS アプリを直接配布する」という体験記の3回目です。

道のり①②で「Apple が認める署名済み・公証済みアプリの DMG」までは作れる状態になりました。

しかし、それだけでは 「無料で配布するアプリ」 の世界に留まります。今回はその先、「お金をいただいて使ってもらうアプリ」 にするためのライセンス機能の実装、そして最終リリースまでの道のりを書きます。

長いですが、indie developer として indie アプリを売り始めるところまでの記録として、誰かの役に立てば嬉しいです。

目次

何を作りたかったか

私が作っているのは「電子帳簿リネーマー」という macOS アプリで、PDF領収書のファイル名を電子帳簿保存法対応の形式に自動変換するツールです。

販売モデルは決まっていました。

  • 無料お試し版: 25ファイルまで使える
  • 製品版ライセンス: ¥2,980 の買い切り、ファイル処理数無制限

サブスクリプションではなく、買い切り。月額課金は一切なし。これは私自身が個人事業主として「毎月の固定費は増やしたくない」と思っているからです。

そして、Mac App Store を経由せず、自分の Web サイトから直接販売する。これは Apple の30%手数料を避けたいというよりも、「自分でビジネスをコントロールできる状態」にしたかったからです。

決済プラットフォームの選定

買い切りライセンスを Web から販売する方法はいくつかあります。

  • Stripe: 一番有名、自由度高い、でも自前で領収書・税務処理が必要
  • Gumroad: 簡単、でも手数料がやや高め
  • Paddle: Merchant of Record で税務処理込み、ただし日本での実績は少ない
  • Polar.sh: 比較的新しい、Merchant of Record で税務処理込み、開発者向け機能が充実

色々検討した結果、Polar.sh を選びました。決め手は3つ。

  1. License Key の発行・管理機能が標準で備わっている(自前で実装する必要なし)
  2. Merchant of Record として日本の消費税対応もしてくれる
  3. 手数料が4%程度 と比較的良心的

特に1つ目が大きい。普通、ライセンス販売には「ライセンスキー発行サーバー」を自前で立てる必要があるのですが、Polar.sh は API 経由でライセンス検証もできる仕組みを提供してくれます。

ライセンス機能の設計

Polar.sh と連携してライセンス機能を実装する上で、私のアプリで必要な機能を整理しました。

1. 起動時: 保存済みライセンスを読み出す
2. PDF処理時: 無料版なら使用回数を消費(残り回数を表示)
3. ライセンスキー入力時: Polar API で検証
4. 認証成功時: ライセンス情報を保存、Pro版へ
5. 認証失敗時: エラーメッセージ表示
6. ライセンス解除: 削除して無料版に戻る

これを実装するため、ライセンス機能を4つのファイルに分けて作りました。

lib/license/
├── license_models.dart      # データ型(LicenseStatus, LicenseInfo)
├── polar_api_client.dart    # Polar API との通信
├── license_storage.dart     # ライセンス情報の永続化
└── license_service.dart     # 上記をまとめるサービス層

UI は license_dialog.dart として別に作りました。

設計で悩んだこと: 永続化方法

「ライセンス情報をどこに保存するか」 これは結構悩みました。

選択肢は2つ。

選択肢A: macOS Keychain(暗号化された安全な保存先)

flutter_secure_storage パッケージを使うと、macOS の Keychain にデータを保存できます。Keychain は暗号化されていて、Mac へのログインで保護されているので、セキュリティ的には最強です。

選択肢B: shared_preferences(平文だが手軽)

shared_preferences はアプリのデータフォルダに平文で JSON 保存します。暗号化はされませんが、サンドボックスで保護されているので、他のアプリからは見えません。

最初は 選択肢A(Keychain) を選びました。「ライセンスキーは大事なデータだから、暗号化された Keychain に保存するべき」と思ったのです。

これが後で、思いもよらない地獄の入り口になりました。

実装 1日目: ライセンス機能のコア実装

最初の1日で、ライセンス機能のコアを実装しました。

  • LicenseStorage で flutter_secure_storage を使ってライセンス情報を Keychain に保存
  • PolarApiClient で Polar の Customer Portal API を呼び出して検証
  • 30日に1回、自動で再検証
  • 無料版で25回まで使える、26回目で購入導線を表示

ここまでは順調でした。デバッグ環境では完璧に動作しました。

✅ ライセンスキー入力 → 認証成功 → Pro版表示
✅ 無料版25回制限 → 26回目で購入ダイアログ表示
✅ アプリ再起動後もライセンス情報維持

「これで完成だ。あとは Release ビルドして、Notarization(公証)して、DMG にして、別Macで動作確認して終わり」と思っていました。

実装 2日目: 別Macでアプリが起動しない

Release ビルドを作って、Notarization も通って、DMG も完成。AirDrop で別Mac(MacBook Air)に送って、ダブルクリックで開く。

そこで悲劇が始まりました。

アプリが起動しないのです。「この App は壊れているため開けません。ゴミ箱に入れる必要があります。」というエラーメッセージ。

Mac mini では動くのに、なぜ別Macでは動かないのか?

調べると、codesign で再署名するときの証明書とプロビジョニングプロファイルの問題でした。

罠1: Provisioning Profile が「特定の Mac でしか動かない」鍵だった

私が最初に使っていた証明書は「Apple Development」で、これは「開発用の特定の Mac でしかアプリを動かせない」という制限がついています。

別Macで動かすには「Developer ID Application」という別の証明書で署名し直す必要があります。

Xcode 経由で「Apple Development」で署名されたアプリの中には、Provisioning Profile (.provisionprofile) というファイルが埋め込まれています。これは「特定の Mac でしか動かない」鍵に対応する許可証のようなものです。

ターミナルで codesign --force --sign "Developer ID Application: ..." app.app と再署名しただけでは、この Provisioning Profile は残ったままです。鍵は変わったのに、許可証が古いままという矛盾状態になります。

これが原因で「No matching profile found」エラーが出て、別Macで起動できませんでした。

罠2: keychain-access-groups の罠

Provisioning Profile を削除しても、まだ動きませんでした。

別のエラー: keychain-access-groups という設定で、Provisioning Profile が必要だと言われるのです。

これは「複数のアプリで Keychain データを共有する」ための機能で、flutter_secure_storage を使っていると、自動でこの設定が入ります。複数アプリで共有するには「同じ開発者が作った」証明が必要で、その証明は Provisioning Profile から取得します。

つまり、

  • Provisioning Profile があると: 矛盾エラー
  • Provisioning Profile がないと: keychain-access-groups エラー

八方塞がり。

大きな方針転換: Keychain を使うのをやめる

数時間悩んだ末、思い切って方針転換しました。

「そもそも、ライセンスキーを Keychain に保存する必要があるのか?」

冷静に考えてみると、

  • ライセンスキーは Polar API で誰でも検証可能な公開情報
  • 使用回数の数字が漏れても実害なし

つまり、暗号化する必要はそもそもなかったのです。「大事なデータだから暗号化された Keychain に」と最初に決めたのは、過剰な思い込みでした。

決断: flutter_secure_storage をやめて、shared_preferences に切り替える。

// Before: flutter_secure_storage(Keychain)
final storage = FlutterSecureStorage(macOptions: ...);
await storage.write(key: 'license', value: jsonString);

// After: shared_preferences(アプリのデータフォルダに平文)
final prefs = await SharedPreferences.getInstance();
await prefs.setString('license', jsonString);

メソッド名・戻り値は同じインターフェースで包み直したので、上位の LicenseService のコードは変えずに済みました。

そして、entitlements ファイルから keychain-access-groups を削除。Provisioning Profile も使わない。シンプルな構成に。

<!-- Release.entitlements (最終版) -->
<dict>
  <key>com.apple.security.app-sandbox</key>
  <true/>
  <key>com.apple.security.files.user-selected.read-write</key>
  <true/>
  <key>com.apple.security.files.bookmarks.app-scope</key>
  <true/>
  <key>com.apple.security.network.client</key>
  <true/>
</dict>

たったこれだけ。

5代目DMGで、ついに別Macで動いた

クリーンな entitlements で再ビルド、再署名、再 Notarization、再 DMG 化。

5回目のビルドにして、ついに別Mac(MacBook Air)でアプリが正常に起動しました。

✅ アプリが「壊れている」エラーなしで起動
✅ 「無料版 (残り25回)」表示
✅ PDFをドラッグ&ドロップ → 処理成功
✅ 使用回数のカウントダウン正常動作

この瞬間の達成感、本当にすごかったです。

学び: indie developer が知っておくべきこと

この2日間の試行錯誤で得た学び。

  1. 「Apple Development」と「Developer ID Application」は別物
  • 開発用と配布用で証明書が違う。混在させると地獄。
  1. Developer ID 配布では、Provisioning Profile は不要(削除すべき)
  • 残っていると「特定 Mac でしか動かない」状態になる。
  1. keychain-access-groups は Mac App Store 配布向けの機能
  • Web から直接配布する場合、これがあるとトラブルの種になる。
  1. 単体アプリでデータ保存するなら shared_preferences で十分
  • Keychain は本当に「他アプリと共有したい秘密」がある場合のみ。
  1. 「Mac mini で動くから別Macでも動くはず」は油断大敵
  • 必ずまっさらな別Macで動作確認すること。

これらは、世界中の indie developer が同じ罠でハマっています。Stack Overflow や Reddit で「No matching profile found」と検索すると、似た質問が山ほど出てきます。

LP に DMG を配置、販売開始

別Macでの動作確認が取れた後、エックスサーバーの管理画面から DMG をアップロード。LP の「無料ダウンロード」ボタンに DMG の URL を設定。

<a href="electronic_bookkeeping_renamer_v1.0.0.dmg" download>
  無料ダウンロード
</a>

「ライセンスを購入する」ボタンには Polar の Checkout URL を設定。

<a href="https://buy.polar.sh/polar_cl_XXX" target="_blank">
  ライセンスを購入する
</a>

これで、世界中の人が

  1. LP を訪れる
  2. 「無料ダウンロード」で DMG を取得
  3. 25ファイル試してみる
  4. 気に入ったら「ライセンスを購入する」で Polar で決済
  5. 受信したライセンスキーをアプリに入力 → Pro版へ

という流れで使い始められる状態になりました。v1.0 リリース完了です。

自分で買ってみた話

翌朝、コーヒー片手にもうひとつのことを試しました。

「自分でクレジットカードを使って、自分のアプリを買ってみる」

これは indie developer の正攻法の動作確認方法です。本番環境を 100%エンドツーエンドで検証 するため、いろいろな決済プラットフォームの開発者向けドキュメントでも推奨されています。

  • LP の「ライセンスを購入する」ボタンを押す
  • Polar のチェックアウト画面が開く
  • クレジットカード情報を入力
  • 三井住友カードの3Dセキュア認証(本人確認)が走る
  • 決済完了、LP に戻る
  • Polar からサンキューメールが届く
  • ライセンスキーをコピー
  • アプリでライセンスキーを入力
  • 認証成功、Pro版表示に切替
  • 25ファイル制限が解除される

すべてが想定通りに動きました。Polar の管理画面を見ると、「Revenue グラフ」が初日に小さく跳ね上がっています。

¥2,980 が、いくつかの手数料(Polar 約4% + 為替手数料 + 固定費)を引かれて、$18.65 という形で私の Available Balance に乗りました。

「小さいけれど、確かな足跡」。

「自分でも安心して買えるアプリ」と「お客様体験を実際に体験した上で売れるアプリ」、この感覚は、今後の開発者人生で何度でも思い出したい価値ある体験になりました。

次は何をするか

v1.0 をリリースしましたが、これで終わりではありません。むしろここからが本番です。

  • 認識可能な取引先の拡充
  • 認識精度の向上(特殊なフォーマットのPDFへの対応)
  • ユーザーからのフィードバックに基づく機能追加
  • Windows版の検討

そして何より、同じ悩みを抱える個人事業主の方々に届けること。これが一番大事な仕事です。

おわりに

3回のシリーズで「個人事業主がmacOSアプリを Mac App Store を経由せず、Web から直接配布して販売する」という道のりを書いてきました。

技術的には決して簡単な道のりではありませんでした。証明書、署名、公証、ライセンス機能、決済プラットフォーム、Web 配置…覚えることがたくさんあり、罠もたくさんあります。

でも、それを乗り越えてきた今、「indie developer として完成形のプロダクトを世界に届けられる」状態になれたことが、何よりの財産です。

同じ道を歩み始めようとしている方々への、小さな道標になれば嬉しいです。


🔗 電子帳簿リネーマー公式サイト: https://sinqwell.net/electronic_bookkeeping_renamer/

🐦 X: @sinqwell_dev

目次