ちょっとサブスクリプションを確認したら、サーバーを通すチェックが必要とか
firebase使えるけどユーザーが増えると高額になるとか…vpsで自前で用意するのも止まると困るので。。。と考えていたら!?
StoreKit2 は、アプリだけで完結できるということを確認!よしやりましょう!
StoreKit2
https://developer.apple.com/jp/videos/play/wwdc2021/10114/
StoreKit2 + SwiftUI
https://developer.apple.com/videos/play/wwdc2023/10013/
早速やってみましょう!
App Store Connect の設定
まず、App Store Connect でサブスクリプションを登録します。
- App Store Connect にログイン
- 対象アプリを選択
- 収益化→サブスクリプションから
サブスクリプショングループを作成 - グループにサブスクリプションを作成(配信先、金額など設定
1ヶ月 $0.99など - ローカライズ(名前や説明を入力)
という感じで保存しました。
請求の猶予期間
全てのユーザー、更新のユーザー、sandboxのみなど選択できるようです
3日、16日 , 28日 から選択できるようです、更新時にカード期限切れなどの場合猶予を持たせたりするのに良いようです
なしでも大丈夫です。
無料トライアルなどは、猶予ではなく普通に無料で回数制限で使ってもらうなどで良いかと
Sandboxテスター


sandboxテスターに入れると 「5分ごとの月次更新」など確認できます!
実機テストなのですが
設定 > App Store > SANDBOXアカウント
これを変えるだけでOKでした!
購入後、購入履歴を削除して、サブスクリプションを解除してみる
appstoreのsandboxアカウントをチェックして「購入履歴を削除」します。すぐには消えない?

購入済みアイテムが購入できるアイテムになければOKです^^v
購入中断テスト?
appstoreconnectのsandboxアカウントを購入中断

これはうまく中断できていないかも?普通に購入できているような
ひとまず置いておいて
レシート検証と通知
レシート検証
transaction内のレシート検証を行うことが出来ます。
verifiedされたtransactionなので通常はOKなのですが、それをサーバー側でも検証すると不正検知できたりということのようです。
verifiedされているのに、サーバー経由でappstoreに確認したら不正だった。ということもあるかもしれません。
過去にverifiedしたtransactionデータを再利用したり。などあるそうで。
不正データだったとしても検知するだけではなく、それをどう処理するかのロジックをアプリ側で対応しないといけません、不正なので機能停止。など
storekit1ではレシート検証が必須だったようです、2では推奨のようです。
レシート検証は非同期で行うだけなので、即時購入停止などはできません。transactionはverifiedされているので一旦transactionはfinishして検証結果に合わせてアイテム付与などを行うということですね
ゲームなどはサーバー側でユーザーのアイテムデータを管理することも多いと思うのでその辺りはまた重要になってきそうです。
今そこまで重要度は高くないですが、とりあえずレシート検証も入れてみます。
通知
appstoreに登録するとサーバーに購入関連通知を受け取ることができます。
このユーザーはアイテム使ったのに課金がキャンセルされている!など?あるのかな
通知とレシート検証を検証してみたい。などなど
通知を受け取るのは簡単なので受け取って保存してみましょう
App Store Server Notifications V2

通知を受け取るには?
- App Store Connect での設定:
- App Store Connect にログインし、アプリを選択します。
- 「App の情報」>「App Store Server Notifications V2」で、通知を受け取るサーバーの URL を設定します。

プロダクションサーバーとsandboxサーバーのURLを入れます!
- サーバー側の実装:
- Apple から送信される通知を受け取るための API エンドポイントをサーバーに実装します。
- 受信した通知の JSON ペイロードを検証し、必要な情報を抽出します。
- トランザクションの情報をデータベースなどに保存し、管理します。
- 通知の検証:
- Appleから受け取った通知は、JWS署名されています。
- Appleの公開鍵を使って署名を検証し、通知の信頼性を確認する必要があります。
今回はGoでエンドポイントを作ってみます!
ちょっとテーマが大きいのでGoで作る部分は別記事を投稿します
続く。
通知メモ

公開鍵が公開されてないので検証ができない?
証明書チェーン検証と署名検証
App Store Server Notification (v2) の signedPayload
を処理
signedPayload
(JWS)を受け取る- ヘッダーの
x5c
から証明書チェーンを検証 - リーフ証明書の公開鍵で JWS 署名を検証
- ペイロード(JSON)を安全に取り出す
Apple PKI – AppleRootCA-G3.pem
は以下からダウンロード:
📎 https://www.apple.com/certificateauthority/
→ 「Apple Root CA – G3」を選んで .cer
を .pem
に変換(または直接PEM形式にして保存)
# DER (.cer) → PEM 変換 openssl x509 -inform der -in AppleRootCA-G3.cer -out AppleRootCA-G3.pem $ openssl x509 -inform der -in AppleRootCA-G3.cer -out AppleRootCA-G3.pem $ ls AppleRootCA-G3.cer AppleRootCA-G3.pem
github.com/golang-jwt/jwt/v5をインストール
$ go get github.com/golang-jwt/jwt/v5
go: added github.com/golang-jwt/jwt/v5 v5.2.2
nginx -> go で Appleからの通知取得に成功しました!
type=SUBSCRIBED, subtype=INITIAL_BUY サブスク初回購入ですね^^
2025/04/08 05:45:29 Received notification: type=SUBSCRIBED, subtype=INITIAL_BUY [GIN] 2025/04/08 - 05:45:29 | 200 | 20.633415ms | 17.58.253.22 | POST
DID_RENEW 更新通知も来ました
2025/04/08 05:50:05 Received notification: type=DID_RENEW, subtype= [GIN] 2025/04/08 - 05:50:05 | 200 | 6.27891ms | 17.58.253.7 | POST "/notifications_sdbx"
httpsや使用するport は 17.0.0.0/8 に制限して良いですね ( appleの
他のものも使う場合は、nginxで限定しても良いですね
user向けのAPIとは別が良いかと思いますが、
両方同じサーバーの場合は appleからの通知用のuriには以下を入れました。
allow 17.0.0.0/8; deny all;
購入時のチェックも – レシート検証?
検証して不正だったらアプリの課金状態を解消する(apple側ではなく、アプリ側の対応のみ
みたいな。自分のアプリは、課金で何かパワーを持つことはないのですが、ゲームなどアイテム課金が重要な場合は、サーバーとの整合性等重要ですね。
今回は transactionが不正?だったらアプリ内にフラグを立てて、サーバー側にも記録すると。
app_account_tokenでユーザーを識別できるので
uuid / keychainで課金状況をチェックしてみました↓

UUIDを app account tokenに反映させる
let result = try await StoreKit.Product.products(for: [product.id]).first?.purchase(options: [.appAccountToken(uuidManager.userUUID)])
transaction finishの後に下のデータを送信してデータベースに入れました。
let payload: [String: Any] = [ "bundleId":bundleID, "bundleVersion":buildVersion, "transactionId": String(transaction.id), "originalTransactionId": String(transaction.originalID), "productId": transaction.productID, "storefront": storefront, "purchaseDate": transaction.purchaseDate.timeIntervalSince1970, "expirationDate": transaction.expirationDate?.timeIntervalSince1970 ?? 0, "environment": environment, "appAccountToken": transaction.appAccountToken?.uuidString.lowercased() ?? "" ]
await transaction.finish() Task { await sendTransactionToServer(transaction) }
sendTransactionToServerは送るだけなのでTaskで非同期で awaitを外すだけでも良いようですが
Task内でエラー処理などする場合に良いようです
クライアント(ユーザー)からのデータとapple通知両方確認することができました◎
ユーザーからのデータは、SHA256でハッシュ値を作って改ざん無ければくらいをチェックしているだけです。
ipaddressも保存しておきます。
nginxからは
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; --- appleからの通知は appleのipだけ許可しています location /api/ { allow 17.0.0.0/8; deny all; }
func getClientIP(c *gin.Context) string { // X-Real-IP ヘッダー if ip := c.GetHeader("X-Real-IP"); ip != "" { return ip } // X-Forwarded-For にフォールバック if ip := c.GetHeader("X-Forwarded-For"); ip != "" { ips := strings.Split(ip, ",") return strings.TrimSpace(ips[0]) // 最初のIPが元々のクライアントの可能性 } ip, _, err := net.SplitHostPort(c.Request.RemoteAddr) if err != nil { return c.Request.RemoteAddr // fallback } return ip }
こんな感じでipを取得して保存しました^^
これで何らかの異常があればチェックできます。
今回はサブスクリプションなので
アプリでも課金済みを定期的にチェックしているので通常問題ないです。
アイテム課金でサーバーとの整合性が必要な場合、ここから丁寧にチェック入れないとですね
コメント