iOSDC Japan 2021 登壇資料
https://fortee.jp/iosdc-japan-2021/proposal/66fe1017-b80b-4f9f-b5c9-dad5e77f4227
https://live.nicovideo.jp/watch/lv333544335#04:00:30
StoreKitͷ͜Ε·Ͱͱ͜Ε͔Βuzzu @ iOSDC JAPAN 2021
View Slide
ࣗݾհɾ@uzzu ɾCookpad Inc. ٕज़෦ Ϣʔβܾࡁج൫άϧʔϓ ɾݞॻAndroidΤϯδχΞɺ࠷ۙRails ɾલ৬εϚʔτϑΥϯήʔϜ։ൃ ɾझຯVR
IAPؔ࿈ͷ͓ࣄ(1/2)https://speakerdeck.com/uzzu/xin-gui-apurikai-fa-wozhi-eruyuzajue-ji-ji-pan
IAPؔ࿈ͷ͓ࣄ(2/2)https://speakerdeck.com/uzzu/xin-gui-apurikai-fa-wozhi-eruyuzajue-ji-ji-pan
IAPؔ࿈ͷ͓ࣄ(2/2)https://speakerdeck.com/uzzu/xin-gui-apurikai-fa-wozhi-eruyuzajue-ji-ji-panできた!(iOS クックパッド 2021/03)
🎊StoreKit2🎊WWDC21
ΞδΣϯμɾΞϓϦ՝࣮ۚͷ֓ཁ ɾStoreKitͱStoreKit2ͷҧ͍(ߪೖɾ෮ݩػೳΛ͍ͳ͕Β) ɾStoreKit2Ͱ৽ͨʹͰ͖Δࣄ ɾ·ͱΊ
͞ͳ͍ࣄ● Promoting IAPɺSubscription Offerɺ Family Sharing SubscriptionͳͲͷOptionalͳܾࡁػೳ● ΞϓϦ՝ۚҎ֎ͷStoreKitͷػೳ(SKOverlay)● App Store Server APIͷৄࡉ● async/awaitͷৄࡉ● StoreKitͱStoreKit2͕ڞଘͨ͠߹ͷڍಈ
ҙࣄ߲ ⚠ɾStoreKit2ݱࡏsandbox՝ࣦۚഊ͢Δ(iOS15 beta8) ɹStoreKit TestingΛར༻͢ΔࣄͰಈ࡞͠·͢ ɹhttps://developer.apple.com/forums/thread/681904ɾStoreKit Testing ར༻ʹ͓͍ͯXcode 13 beta 5࣌Ͱ ɹچStoreKitͱͷڠௐಈ࡞ͷ֬ೝ͕औΕͳ͍ ɹ=> ڞଘͨ͠߹ͷಈ࡞͕Ͳ͏ͳΔͷ͔ࡉ͔͍ॴݱঢ়͔Βͳ͍ ɾ͑ΔΑ͏ʹͳΔͷ iOS15 ʙ (async/await backport ૣ͘དྷͯ͘Εʙ) ɾಈ͘Α͏ʹͳͬͨΒ࠶ݕূ͠·͠ΐ͏
ΞϓϦ՝ۚͷ࣮ͷ֓ཁ(1/2)ɾΞϓϦͰσδλϧίϯςϯπͷൢചఆظߪಡػೳΛ ɹఏڙ͢ΔࡍʹΞϓϦ՝ۚͷ࣮͕ඞཁɾΞϓϦ՝ۚͷλΠϓ4छ ɹফܕΞΠςϜ(Consumable) ɹඇফܕΞΠςϜ(Non-Consumable) ɹࣗಈߋ৽αϒεΫϦϓγϣϯ(Auto-Renewable Subscription) ɹඇࣗಈߋ৽αϒεΫϦϓγϣϯ(Non-Renewing Subscription)
ΞϓϦ՝ۚͷ࣮ͷ֓ཁ(1/2)ɾͦΕͧΕఏڙ͢Δҝʹ࣮ඞਢͳػೳ ɹߪೖ ɹ෮ݩ ɾͦͷଞɺඞਢͰͳ͍͕ඞཁʹԠ࣮ͯ͡ΛٻΊΒΕΔػೳ ɹPromoting IAP .. App Store ΞϓϦ্ͰͷͷϓϩϞʔγϣϯɺߪೖ ɹSubscription Offer .. ҙͷϢʔβʹՁ֨Ͱఏڙ͢Δ ɹFamily Sharing Subscription .. ϑΝϛϦʔڞ༗ͷػೳͰఆظߪೖΛڞ༗͢Δ
ΞδΣϯμɾΞϓϦ՝࣮ۚͷ֓ཁ ɾStoreKitͱStoreKit2ͷҧ͍(ߪೖɾ෮ݩػೳΛ͍ͳ͕Β) ɾStoreKit2Ͱ৽ͨʹͰ͖Δࣄ ɾؔ࿈ͯ͠AppStore Server API
ߪೖɾ෮ݩͷઆ໌ʹೖΔલʹ…ॳظԽʹ͍ͭͯɾچStoreKitΞϓϦ՝ۚͷ࣮ʹඞཁͳ ɹ֤छ௨Λड͚औΔҝͷObserverͷ࣮ͱొॲཧ ɹ͕ඞཁɻຊεϥΠυͰ͜ΕΛศ্ٓॳظԽͱݺͿɾొॲཧ .. SKPaymentTransactionObserverΛ࣮ͨ͠ ɹΫϥεͷΠϯελϯεΛSKPaymentQueueʹొ
class AppDelegate: UIResponder, UIApplicationDelegate {private var iapObserver: SKPaymentTransactionObserver = IAPObserver()func application(_ application: UIApplication,didFinishLaunchingWithOptions launchOptions:[UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {SKPaymentQueue.default().add(iapObserver)return true}func applicationWillTerminate(_ application: UIApplication) {SKPaymentQueue.default().remove(iapObserver)}}
ߪೖɾ෮ݩͷઆ໌ʹೖΔલʹ…ॳظԽʹ͍ͭͯॳظԽʹ·ͭΘΔτϥϒϧ ɹɾΞϓϦىಈ࣌ͷॳظԽॲཧ(Ϣʔβೝূetc…)͕ ɹɹऴΘͬͯͳ͍ঢ়ଶͰObserverॲཧͯ͠͠·͏ ɹɾ௨͕͍ͭདྷͯྑ͍Α͏ͳ࣮ߟྀ͕͞Ε͍ͯͳ͍ ɹɾSKPaymentQueueͷremove/addΛ͓ͯ͠Βͣظͨ͠௨͕དྷͳ͍ ɹɾ3rd party ϥΠϒϥϦ͕ObserverొΛ͓ͯ͠Γ ɹɹࣗΞϓϦͷObserverʹظͨ͠௨͕དྷͳ͍
ߪೖɾ෮ݩͷઆ໌ʹೖΔલʹ…ॳظԽʹ͍ͭͯStoreKit 2ॳظԽෆཁ🎉🎉🎉
購⼊処理フロー購⼊ボタンを押す注⽂番号を発⾏商品情報取得 決済 レシート送信 + 注⽂を有効にして 購⼊した商品を 付与するレシート検証 最新レシート 取得購⼊情報を 更新
購⼊処理フロー/商品情報の取得 .. 旧StoreKitの場合public typealias ProductsRequestResult = Resultpublic typealias ProductsRequestCompletion = (ProductsRequestResult) -> Voidpublic class ProductsRequest: NSObject, SKProductsRequestDelegate {private var completion: ProductsRequestCompletion?private var request: SKProductsRequestinit(_ productIDs: Set) {self.request = SKProductsRequest(productIdentifiers: productIDs)}public func send(completion: @escaping ProductsRequestCompletion) {self.completion = completionself.request.delegate = selfself.request.start()}public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {if let completion = completion {self.completion = nilcompletion(.success(response))}}public func request(_ request: SKRequest, didFailWithError error: Error) {if let completion = completion {self.completion = nilcompletion(.failure(error))}}}
購⼊処理フロー/商品情報の取得 .. 旧StoreKitの場合public typealias ProductsRequestResult = Resultpublic typealias ProductsRequestCompletion = (ProductsRequestResult) -> Voidpublic class ProductsRequest: NSObject, SKProductsRequestDelegate {private var completion: ProductsRequestCompletion?private var request: SKProductsRequestinit(_ productIDs: Set) {self.request = SKProductsRequest(productIdentifiers: productIDs)}public func send(completion: @escaping ProductsRequestCompletion) {self.completion = completionself.request.delegate = selfself.request.start()}public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {if let completion = completion {self.completion = nilcompletion(.success(response))}}public func request(_ request: SKRequest, didFailWithError error: Error) {if let completion = completion {self.completion = nilcompletion(.failure(error))}}}ɹɾdelegate method͕ݺΕΔεϨουอূ͞Ε͍ͯͳ͍ ɹɹ=>ϝΠϯεϨουͰݺΕΔલఏͰ͍Δͱ ɹɹɹޙଓॲཧ͕Ϋϥογϡ͢Δ(iOS 13.1ʙ)ɹɾλΠϓ͕͔Βͳ͍ ɹɹ=>productIdentifierͷ໋໊ΛϧʔϧԽ͢Δɺରࡦ͕ඞཁɹɾใऔಘ͢Δ͚ͩͳͷʹ ɹɹίʔυྔ͕ଟ͍(هࡌͷίʔυͰ࣮ࡍΓͳ͍)
購⼊処理フロー/商品情報の取得 .. StoreKit2の場合let products = try await Product.products(for: ["productId"])ͪΖΜλΠϓऔಘՄೳ (Product.type)🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳
購⼊処理フロー/決済 .. 旧StoreKitの場合let payment = SKMutablePayment(product: product)// 様々オプションを指定SKPaymentQueue.default().add(payment)↓ SKPaymentTransactionObserverの以下のdelegateが呼ばれるfunc paymentQueue( _ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {// :}↓SKPaymentTransaction.transactionState == .purchasing でやってくる↓SKPaymentTransactionObserverの同じdelegateが呼ばれる↓IAP決済ダイアログが出てくる。ユーザが決済を⾏う↓SKPaymentTransaction.transactionState == .purchased でやってくる↓レシート送信、商品付与↓商品付与後、SKPaymentQueue.default().finishTransaction(transaction)
↓商品付与後、購⼊処理フロー/決済 .. 旧StoreKitの場合let payment = SKMutablePayment(product: product)// 様々オプションを指定SKPaymentQueue.default().add(payment)↓ SKPaymentTransactionObserverの以下のdelegateが呼ばれるfunc paymentQueue( _ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {// :}↓SKPaymentTransaction.transactionState == .purchasing でやってくる↓SKPaymentTransactionObserverの同じdelegateが呼ばれる↓IAP決済ダイアログが出てくる。ユーザが決済を⾏う↓SKPaymentTransaction.transactionState == .purchased でやってくる↓レシート送信、商品付与SKPaymentQueue.default().finishTransaction(transaction)ɹɾupdatedTransactionsʹ ɹɹfinish͞Ε͍ͯͳ͍શͯͷTransaction͕ͬͯ͘Δ ɹɹ=>ݱࡏߪೖதͷ͕Կ͔ΛͲ͔͜͠ΒͰ͓࣋ͬͯ͘ ɹɹɹ ඞཁ͕͋Δ ɹɾAsk To Buy(ϖΞϨϯλϧίϯτϩʔϧػೳ)ͷΑ͏ʹ ɹɹܾࡁྃ௨͕όοΫάϥϯυ͔Βͬͯ͘Δέʔε ɹɹ͕͋ΔͷͰɺͦͷߟྀඞཁɹɾͪΖΜɺλΠϓશͯͷTransaction͕ͬͯ͘Δ ɹɹ=>λΠϓΛෳѻ͏Α͏ʹͳͬͨΒ ɹɹɹ Observer͕େมͳࣄʹͳΔ ɹɾ্هΛ౿·্͑ͨͰObserverͱ(Rx|Combine)ͱܨ͗͜Ή ɹɹͷͰ͖ͳ͘ͳ͍͕େม
購⼊処理フロー/決済 .. StoreKit2の場合let result = try await product.purchase()🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉Transaction#finish() Εͣʹ
購⼊処理フロー/レシート検証、最新レシート取得 .. 旧StoreKitの場合let base64EncodedReceipt: String?if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {base64EncodedReceipt = try? Data(contentsOf: appStoreReceiptURL,options: .alwaysMapped).base64EncodedString()} else {base64EncodedReceipt = ""}↓注⽂番号と合わせてサーバに送信↓POST https://buy.itunes.apple.com/verifyReceipt↓最新レシート情報を元に商品付与、注⽂を有効にする↓後続処理へ…
購⼊処理フロー/レシート検証、最新レシート取得 .. 旧StoreKitの場合let base64EncodedReceipt: String?if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {base64EncodedReceipt = try? Data(contentsOf: appStoreReceiptURL,options: .alwaysMapped).base64EncodedString()} else {base64EncodedReceipt = ""}↓注⽂番号と合わせてサーバに送信↓POST https://buy.itunes.apple.com/verifyReceipt↓最新レシート情報を元に商品付与、注⽂を有効にする↓後続処理へ…ɹɾϨγʔτݕূࣗମΞϓϦ୯ମͰͰ͖Δ͕ ɹɹ؆қͳͷͳͷͰجຊతʹΘͳ͍ ɹɾաڈͷಉΞϓϦͰͷߪೖใ͕શؚͯ·ΕΔҝ ɹɹϖΠϩʔυαΠζ͕૿Ճ ɹɹࣗಈߋ৽αϒεΫϦϓγϣϯͳΒߋ৽݄૿͑Δ ɹɾϖΠϩʔυ͕େ͖͘ͳΔ΄ͲɺverifyReceiptͷ ɹɹϨεϙϯε͘ͳΔɻΑ͘500Λు͘ɹɹ=>exclude_old_transactions ͳΦϓγϣϯͰ ɹɹɹߴԽ͕ਤΕΔ͕ ɹɹɹݹ͍ߪೖใඞཁͳϢʔεέʔε ɹɹɹ࣮ӡ༻্΅ͪ΅ͪ͋Δ
購⼊処理フロー/レシート検証、最新レシート取得 .. StoreKit2の場合JWS化!レシート検証でサーバ間通信不要 巨⼤なレシート送らなくてもOK(代わりにJWS) AppStore Server APIで個別にTransaction単位で問合せ可能に🍻🍻🍻🍻🍻🍻🍻🍻🍻🍻🍻🍻🍻🍻🍻🍻🍻🍻JWSがどれくらいのサイズになるだろうか…🤔
෮ݩͱ…ʁɾIAPܾࡁߦΘΕ͍ͯΔ͕͕༩͞Ε͍ͯͳ͍ ɹϢʔβͷٹࡁાஔ ɹ- ߪೖΤϥʔ͕ൃੜͨ͠··ܾࡁը໘͔Βͯ͠͠·ͬͨ ɹ- ػछมߋޙʹݩʑར༻͍ͯͨ͠ঢ়ଶʹ͍ͨ͠ ɹ- ߪೖࡁΈͷΛෳͰར༻͍ͨ͠ɾܾࡁใΛ෮ݩ͢Δػೳ͕ఏڙ͞Ε͍ͯΔͷͰ ɹͦΕΛར༻ͯ͠෮ݩॲཧΛߦ͏
෮ݩͱ…ʁɾফܕΞΠςϜͷ߹ ɹ=> ະॲཧͳܾࡁใ͕͋ΕɺͦΕΛߪೖॲཧʹԊͬͯॲཧͯ͠Λ༩ ɾඇফܕΞΠςϜɺࣗಈߋ৽αϒεΫϦϓγϣϯ ɹඇࣗಈߋ৽αϒεΫϦϓγϣϯͷ߹ ɹ=> ະॲཧͳܾࡁใ͕͋ΕɺͦΕΛߪೖॲཧʹԊͬͯॲཧͯ͠Λ༩ ɹ=> ॲཧࡁΈͰ͋Ε ɹɹϩάΠϯ͍ͯ͠ΔϢʔβΛݩʑར༻͍ͯͨ͠ϢʔβʹΓସ͑Δ ɹɹ·ͨ ݩʑར༻͍ͯͨ͠Ϣʔβʹ౷߹ͭͭ͠ϩάΠϯϢʔβΛΓସ͑Δ ɹɹ·ͨ ߪೖใʹ߹ΘͤͯݱࡏͷϢʔβʹΛ͚ସ͑Δ ɹɹͳͲɺཁ݅ʹ͋Θͤͯ…
復元処理フロー復元操作をする or ⾃動で復元レシート送信 + 購⼊情報を 復元する為の あれこれ 決済情報を 復元するレシート検証 最新レシート 取得購⼊情報を 復元する為の あれこれ
復元処理フロー/決済情報の復元 .. 旧StoreKitの場合SKPaymentQueue.default().restoreCompletedTransactions()↓ SKPaymentTransactionObserverの以下のdelegateが呼ばれるfunc paymentQueue( _ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {// :}↓SKPaymentTransaction.transactionState == .restored でやってくる↓アプリとして必要な復元処理を実施↓復元処理を実施後SKPaymentQueue.default().finishTransaction(transaction)↓ SKPaymentTransactionObserverの以下のdelegateが呼ばれるfunc paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {// :}
復元処理フロー/決済情報の復元 .. 旧StoreKitの場合SKPaymentQueue.default().restoreCompletedTransactions()↓ SKPaymentTransactionObserverの以下のdelegateが呼ばれるfunc paymentQueue( _ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {// :}↓SKPaymentTransaction.transactionState == .restored でやってくる↓アプリとして必要な復元処理を実施↓復元処理を実施後SKPaymentQueue.default().finishTransaction(transaction)↓ SKPaymentTransactionObserverの以下のdelegateが呼ばれるfunc paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {// :}ɹɾ·ͨupdatedTransactions… ɹɹ=>ߪೖॲཧͱ෮ݩॲཧผͷͳͷͰ ɹɹɹͪΌΜͱtransactionStateΛݟΔ ɹɾະॲཧͳܾࡁͷ߹ߪೖॲཧͱಉͷॲཧΛ࣮ࢪ͢Δ ɹɹඞཁ͕͋ΔɹɾͪΖΜɺλΠϓશͯͷTransaction͕ͬͯ͘Δ ɹɾRxɺCombineɺο…
復元処理フロー/決済情報の復元 .. StoreKit2の場合let transaction = await product.latestTransactionlet result = await product.currentEntitlementlet result = await Transaction.latest(for: "productId")let results = await Transaction.currentEntitlements// etc…👏👏👏👏👏👏👏👏👏👏👏👏SKReceiptRefreshRequest૬ͷͷ await AppStore.sync()
StoreKit2Ͱ৽ͨʹͰ͖ΔࣄɾTransactionͷৄࡉ(unified_receipt૬)Λ ɹ༰қʹࢀরͰ͖Δ ɹچStoreKitͰग़དྷ͕ͨ… ɹɹηΩϡΞͱݴ͍͍Γํͩͬͨ ɹɹܕ҆શʹࢀর͢ΔҝʹखલͰؤுͬͯσίʔυ͢Δ ɹɹඞཁ͕͋ͬͨ
StoreKit2Ͱ৽ͨʹͰ͖ΔࣄɾฦۚϦΫΤετΛ࣮Ͱ͖Δ ɹ返⾦リクエストボタンを押す返⾦リクエスト返⾦リクエスト通知購⼊情報を 提出返⾦と判断されれば 返⾦
ɾiOS15ະຬ ɹ=>async/await backport ͕དྷͨΒมΘΔ͔… ɾApple School ManagerApple Business Manager্Ͱ ɹVolume Purchase ProgramΛར༻͍ͯ͠Δ߹ ɾΞϓϦͷ༧ൢചػೳΛར༻͢Δ߹ ɾաڈʹ༗ྉΞϓϦ͕ͩͬͨϑϦʔϛΞϜϞσϧʹ ɹҠߦͨ͠ΞϓϦͷ߹ɺ·ͨͦͷٯͷ߹ ɹ=>StoreKit2ʹ·ͩAPI (preorder_date) ͕ଘࡏ͠ͳ͍StoreKit2͕͑ͳ͍έʔε
·ͱΊ● ͨͩͰܾ͑͞ࡁॲཧෳࡶͳॴʹ چStoreKitͷSKPaymentTransactionObserverΛத৺ͱͨ͠ઃܭ͕ ͞Βʹෆඞཁͳෳࡶ͞ΛੜΜͰ͠·͍ͬͯͨ - ߪೖɺ෮ݩॲཧ࣮ʹ͓͚ΔόοΫάϥϯυॲཧͷߟྀ - ͷछผຖͷదͳॲཧͷҧ͍ - ߋʹՃػೳ(Promoting IAP, Subscription Offer, etc…)Λར༻͢Δ શ෦ࡌͤͨঢ়ଶͷSKPaymentTransactionObserverΛ૾ͯ͠Έ·͠ΐ͏ ɹͱ͔ͯΜͨΜͱݴ͍͍ɻਅ໘ʹ͖߹͏ͱઐ৬͕ඞཁͳϨϕϧ ɹ3rd partyϥΠϒϥϦΛ͍ͨ͘ͳΔؾ͔࣋ͪΔ● StoreKit2ͰObserverύλʔϯഇࢭ͞Εɺasync/await͕͔Ε͔Δ͙Β͍ͷ ۙԽ͕͞Ε͍ͯΔ ࣮ʹ…࣮ʹγϯϓϧͳAPIʹͳͬͨ ։ൃ͢ΔΞϓϦʹඞཁͳܾࡁॲཧͷ࣮ʹूதͰ͖ΔΑ͏ʹͳͬͨɻૣ͍͍ͨ͘…ɻ● App Store Server(·ͩ͑ͳ͍ͷͰͲ͏ͳΔ͔͔Βͳ͍͚Ͳ)ૣ͍͍ͨ͘…ɻ
ࢀߟɾMeet StoreKit 2 @ WWDC21 ɹhttps://developer.apple.com/videos/play/wwdc2021/10114/ɾManage in-app purchases on your server @ WWDC21 ɹhttps://developer.apple.com/videos/play/wwdc2021/10174/ɾSupport customers and handle refunds @ WWDC21 ɹhttps://developer.apple.com/videos/play/wwdc2021/10175ɾIn-App Purchase ɹhttps://developer.apple.com/documentation/storekit/in-app_purchaseɾImplementing a Store In Your App Using the StoreKit API ɹhttps://developer.apple.com/documentation/storekit/in-app_purchase/implementing_a_store_in_your_app_using_the_storekit_apiɾApp Store Server API ɹhttps://developer.apple.com/documentation/appstoreserverapi