Appleの課金処理とレシートの検証
公開日: 2024-10-24 09:43:32
更新日: 2024-10-24 09:54:04
本日は課金完了後のアップルのレシート検証について書いていきます。
事前準備としてAppleStoreConnectでサンドボックスのテスト用のアカウントが必要になります。
その後iPhoneの実機でのみテスト可能ということで現在のアカウントをログアウトして先ほど作成したサンドボックス用のアカウントを入れます。
※注意!
テスト端末で行う場合はいいのですが、実際に自分で使っている端末を使用すると、私はメインアカウントに戻した際に電話帳が全て消えました。電話帳のBKはきちんと取って、終了後には復旧できるようにしておいた方がいいです。
AppleStoreConnectで商品情報を作成
課金のテストをするにはAppleStoreConnectで商品情報を作成して、ステータスを「提出準備完了」とする必要があります。
そのためにSSを仮の画像でも登録する必要がありました。
商品情報をボタンに設定して、ストアから商品情報を呼び出して購入処理を進める
paymentQueueのDelegateメソッドを記載することでSKPaymentQueue.default().add(payment)の1行でpaymentQueue()が呼ばれて購入が有効かの処理が走ります。
let productIdentifiers = ["com.test.test.svticket20"] // 商品IDを設定
/// 課金アイテム1購入ボタン
@objc func purchaseItem1() {
if SKPaymentQueue.canMakePayments() {
let productRequest = SKProductsRequest(productIdentifiers: Set(productIdentifiers))
productRequest.delegate = self
productRequest.start() //■■■商品情報を参照する
} else {
print("In-app purchases are disabled.")
}
}
/// ■■■SKProductsRequestDelegate: 商品情報を取得
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
if let product = response.products.first {
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment) //■■■paymentQueueを呼ぶ
}
}
購入処理結果を表示
こちらの関数(paymentQueue)は同じViewControllerに書けばいいのですが、監視するような書き方が必要だったので別で書きました。
この書き方によってSKPaymentQueue.default().add(payment)の1行でpaymentQueueが監視実行されるようになります。
import StoreKit
class YourViewController: UIViewController, SKPaymentTransactionObserver {
override func viewDidLoad() {
super.viewDidLoad()
// SKPaymentQueue にオブザーバーを登録
SKPaymentQueue.default().add(self)
}
deinit {
// 画面が閉じられた際にオブザーバーを解除
SKPaymentQueue.default().remove(self)
}
// ■■■SKPaymentTransactionObserver: 購入処理の結果を処理
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchased:
print("Purchase successful")
// レシートの取得・検証処理
if let receiptURL = Bundle.main.appStoreReceiptURL, FileManager.default.fileExists(atPath: receiptURL.path) {
do {
let receiptData = try Data(contentsOf: receiptURL)
let receiptString = receiptData.base64EncodedString(options: [])
print("Receipt: \(receiptString)")
// レシートの検証リクエストを送信
validateReceipt(receiptString: receiptString)
} catch {
print("Failed to retrieve receipt data: \(error.localizedDescription)")
}
}
SKPaymentQueue.default().finishTransaction(transaction)
case .failed:
if let error = transaction.error as NSError? {
print("Purchase failed: \(error.localizedDescription)")
}
SKPaymentQueue.default().finishTransaction(transaction)
case .restored:
print("Purchase restored")
SKPaymentQueue.default().finishTransaction(transaction)
case .deferred:
print("Purchase deferred")
case .purchasing:
print("Purchase in progress")
@unknown default:
print("Unknown transaction state")
}
}
}
}
SKPaymentQueue.default().add(self): これを実行することで、YourViewController(または他の任意のクラス)が SKPaymentQueue のトランザクションを監視し始めます。これにより、購入プロセス中にトランザクションが更新された際に、paymentQueue(_:updatedTransactions:) メソッドが自動的に呼ばれるようになります。
SKPaymentQueue.default().remove(self): deinit でオブザーバーを解除するのは、メモリリークや不要な監視を防ぐためです。これにより、画面が閉じられた際にオブザーバーが正しく解除されます。
レシートの検証用の処理
Appleレシートサーバーにデータを確認して、購入履歴にデータが存在するか?改竄されていないか?を検証するらしい。
/// レシート検証処理
func validateReceipt(receiptString: String) {
// サンドボックス環境または本番環境のURLを指定
let receiptVerificationURL = "https://sandbox.itunes.apple.com/verifyReceipt" //sandbox(テスト用)
// let receiptVerificationURL = "https://buy.itunes.apple.com/verifyReceipt" // 本番環境用
// リクエストボディ(サブスクリプションでない場合は`sharedSecret`を除外)
var requestBody: [String: Any] = [
"receipt-data": receiptString
]
// もしサブスクリプションや自動更新のアイテムを使用しているなら`sharedSecret`を含める
if !sharedSecret.isEmpty {
requestBody["password"] = sharedSecret
}
// HTTPリクエストの作成
guard let url = URL(string: receiptVerificationURL) else { return }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
// JSONデータに変換
do {
let jsonData = try JSONSerialization.data(withJSONObject: requestBody, options: [])
request.httpBody = jsonData
} catch {
print("Failed to serialize JSON: \(error.localizedDescription)")
return
}
// 検証リクエストを送信
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
print("Failed to validate receipt: \(error.localizedDescription)")
return
}
// レスポンスデータを処理
if let data = data {
do {
if let jsonResponse = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
print("Receipt validation response: \(jsonResponse)")
// 検証結果に基づいて処理を実装
if let status = jsonResponse["status"] as? Int {
switch status {
case 0:
print("レシートは有効です。")
case 21000:
print("App Store が提供された JSON オブジェクトを読み取れませんでした。")
case 21001:
print("レシートを認証できませんでした。")
case 21002:
print("receipt-data プロパティ内のデータが不正、または存在しません。")
case 21003:
print("レシートを認証できませんでした。")
case 21004:
print("提供された共有シークレットが、アカウントに登録されている共有シークレットと一致しません。")
case 21005:
print("レシートサーバーは現在利用できません。")
case 21006:
print("このレシートは有効ですが、サブスクリプションの有効期限が切れています。")
case 21007:
print("このレシートはサンドボックス環境のものですが、本番環境に送信されました。")
case 21008:
print("このレシートは本番環境のものですが、サンドボックス環境に送信されました。")
default:
print("レシートの検証に失敗しました。不明なステータス: \(status)")
}
}
}
} catch {
print("Failed to parse JSON response: \(error.localizedDescription)")
}
}
}
task.resume()
}
Appleのレシートの検証後のレスポンス
値はマスクしましたがこちらのような値が返ってきます。
Receipt validation response: ["status": 0, "receipt": {
"adam_id" = 0;
"app_item_id" = 0;
"application_version" = 1;
"bundle_id" = "com.test.test";
"download_id" = 0;
"in_app" = (
{
"in_app_ownership_type" = PURCHASED;
"is_trial_period" = false;
"original_purchase_date" = "2024-10-23 08:21:48 Etc/GMT";
"original_purchase_date_ms" = 115000008000;
"original_purchase_date_pst" = "2024-10-23 01:21:48 America/Los_Angeles";
"original_transaction_id" = 2000000000000000;
"product_id" = "com.musictheater.flightworld.svticket20";
"purchase_date" = "2024-10-23 08:21:48 Etc/GMT";
"purchase_date_ms" = 115000008000;
"purchase_date_pst" = "2024-10-23 01:21:48 America/Los_Angeles";
quantity = 1;
"transaction_id" = 2000000000000000;
}
);
"original_application_version" = "1.0";
"original_purchase_date" = "2013-08-01 07:00:00 Etc/GMT";
"original_purchase_date_ms" = 115000008000;
"original_purchase_date_pst" = "2013-08-01 00:00:00 America/Los_Angeles";
"receipt_creation_date" = "2024-10-23 08:21:48 Etc/GMT";
"receipt_creation_date_ms" = 115000008000;
"receipt_creation_date_pst" = "2024-10-23 01:21:48 America/Los_Angeles";
"receipt_type" = ProductionSandbox;
"request_date" = "2024-10-23 08:21:51 Etc/GMT";
"request_date_ms" = 1729671711211;
"request_date_pst" = "2024-10-23 01:21:51 America/Los_Angeles";
"version_external_identifier" = 0;
}, "environment": Sandbox]
レシートは有効です。
Server registration failed. Transaction rolled back.
こちらのような値が返ってくるので、私はこの値を使って自分のDBへ登録するようにしました。