第四篇 App的内购功能

背景

        本来功能比较简单,没打算搞内购。上AppStore搜索一下,类似应用都有。然后我发现,我一直做PC端的开发,软件研发十几年居然不知内购业务是什么样,所以就带着试试看的目的,搞了一个。这个我是第一次搞,这篇都是我的理解,所有很可能有错误,主要参考官方文档(https://developer.apple.com/documentation/storekit/in-app_purchase)。


内购种类

        按照我的理解,内购一共分为3种:

        1、消耗品(Consumables)类型

            拿游戏举个例子,买点卡。可以一直买,反复买的。

        2、非消耗品(Non-consumables)类型

            再拿游戏举个例子,解锁特定人物。买一次就可以啦。

        3、订阅(Subscriptions)类型

            按照苹果的文档一共有4种内购,订阅分为自动续订内购(Auto-renewable subscriptions)和非自动续订内购(Non-renewing subscriptions)。但我从实际使用角度,我们提供的都是自动续订的内购,只是用户可以选择取消自动续订。

        我实际本次使用的是订阅类型。


内购生效范围

        这里主要解决的问题是购买之后是当前设备生效?还是当前账号(是否和平台有关,iOS和Mac)生效?如果是当前设备生效,那么用户卸载后再次安装怎么处理的问题?

        这个章节我没有找到官方的要求规范(实际上我没有仔细找),因为订阅类型是跟随账号的(在订阅有效期内而距离过期还有较长时间时,是不能再次订购的),所以我理解订阅是跟随账号的。

        为了解决用户卸载后安装,同账号在其实设备上安装等问题,我把用户购买信息作为私有数据储存在iCloud中。


监视购买状态

        主要就是创建一个实例,这个实例实现SKPaymentTransactionObserver协议。并且在程序启动时,启动这个监听。

SKPaymentTransactionObserver协议实现

示例代码如下:

class StoreObserver: NSObject {

    // 实现简单单例模式

    static let shared = StoreObserver()

    private override init() {

        // 继续初始化,使用私有,不允许外部创建

    }

}

extension StoreObserver: SKPaymentTransactionObserver {

    func paymentQueue(_queue:SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {

        // 购买状态更新回调

    }

}

需要注意的是SKPaymentTransactionObserver协议继承自NSObjectProtocol,所以StoreObserver才继承NSObject,这样NSObjectProtocol协议就默认实现啦。

PS: SKPaymentTransactionObserver协议的一些可选方法没有实现


启动监听

示例代码:

@UIApplicationMain

class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(_application:UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey:Any]?) ->Bool{

        SKPaymentQueue.default().add(StoreObserver.shared)

        // 可以在这里发起异步请求订购信息

        return true

    }

    func applicationWillTerminate(_ application: UIApplication) {

        SKPaymentQueue.default().remove(StoreObserver.shared)

    }

    // ...

}


请求支持的订购商品

        主要是实现SKProductsRequestDelegate协议

示例代码:

class StoreManager: NSObject {

    static let shared = StoreManager()

    private override init() {

        // ...

    }

    func fetchProducts() {

        let ids: [String]

        // 初始化ids,使用在Connect上配置的ID

        productRequest = SKProductsRequest(productIdentifiers: Set(ids))

        productRequest.delegate = self

        productRequest.start()

    }

    func buy() {

        let payment: SKMutablePayment

        // 初始化payment

        SKPaymentQueue.default().add(payment)

    }

}

extension StoreManager: SKProductsRequestDelegate {

    func productsRequest(_request:SKProductsRequest, didReceive response:SKProductsResponse) {

        // ...

    }

}

extension StoreManager: SKRequestDelegate {

    func request(_request:SKRequest, didFailWithError error:Error) {

        // ...

    }

}

StoreManager类也是一个单例

PS: SKProductsRequestDelegate协议的一些可选方法没有实现


Connect配置

    支持的商品需要在Connect上配置


购买有效性校验

    示例代码:

  func checkReceipt(isSandbox :Bool) {

        DispatchQueue.global().asyncAfter(deadline:DispatchTime.now() + .seconds(10)) {

            if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL, FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {

                var verifyUrl :URL?

                if isSandbox {

                    verifyUrl =URL(string: "https://sandbox.itunes.apple.com/verifyReceipt")

                } else {

                    verifyUrl =URL(string: "https://buy.itunes.apple.com/verifyReceipt")

                }

                if let url = verifyUrl {

                    do {

                        let receiptData =try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)

                        let receiptString = receiptData.base64EncodedString(options:.endLineWithLineFeed)

                        var request =URLRequest(url: url)

                        let body = String(format: "{\"receipt-data\" : \"%@\", \"exclude-old-transactions\" : false, \"password\" : \"这里写Connect里设置的密码\"}", receiptString)

                        request.httpBody = body.data(using: .utf8)

                        request.httpMethod ="POST"

                        let session = URLSession.shared.dataTask(with: request) { recvData, response, erro rin

                            if error != nil{

                                // 网络错误

                                return

                            }

                            if let data = recvData {

                                if let jsonData = try?JSONSerialization.jsonObject(with: data, options: .allowFragments) as? DictionaryAny> {

                                    if let status :Int = jsonData["status"] as? Int {

                                        if status == 21007 {

                                            if !isSandbox {

                                                checkReceipt(isSandbox:true)

                                            }

                                        } else if status == 21002 || status == 21005 || status == 21009 {

                                            checkReceipt(isSandbox: isSandbox)

                                        } else if status == 0 {

                                            // 解析数据

                                        }

                                    } 

                                }

                            }

                        }

                        session.resume()

                    } catch {}

                }

            } else {

                fetchReceipt()

            }

        }

    }


关联文档

        第一篇 开发构想

        第二篇 编辑器控件

        第三篇 导入导出功能

        第五篇 本地化

你可能感兴趣的:(第四篇 App的内购功能)