提起内购,有多少小朋友要干呕的?我要举个爪,呜呜呜呜呜。。。
前段时间公司要做一个知识付费项目,这意味着有新的功能和产品结构,团队摩拳擦掌准备好一切提交审核,Android端一切perfect~,iOS端静待审核,经过两天的等待,我终于在一个周末早上的五点半收到了被拒信息,真的是晴天霹雳,接入了支付宝和微信,支付方式是没有问题的,关键是知识付费要使用内购支付,说实话做项目之前没听过内购(原谅我一个iOS端用的是Android机),团队没人做过这个东西,于是顶着鸟窝头搜了下内购,任何在程序内可完成的付费商品在iOS端付费时都必须走内购,发现代码实现其实挺简单,但是网上很多人写帖子吐槽内购,主要原因如下:
1.内购要求严苛,不得涉及可触碰实体的任何一部分
2.抽取30%的金额,所以商家为了能挣到钱就把这30%放到了消费者身上,而提升后的价格和市场实际价格不符,很容易流失客源
3.内购只允许设置具体的等级价格,可设置某段时间内的价格波动,不能随心所欲地设置
4.内购类型的选择和使用,如选错内购类型你又不联系他们,你就会不断地体会什么叫“怀疑人生”,什么叫“绝望”,哈哈哈哈。。。呜呜呜呜呜。。。。
下面说正经的,不逼逼,上新鲜的代码
import UIKit
class DataTools: NSObject {
class func getGoodsList(_ result: (Set<String>)->()) {
result([内购产品id数组,字符串])
}
}

import UIKit
import StoreKit
class TempViewController: UIViewController {
var products: [SKProduct] = [SKProduct]()
override func viewDidLoad() {
super.viewDidLoad()
DataTools.getGoodsList { (ids: Set<String>) -> () in//从我们自己的服务器上获取需要销售的额商品
let request: SKProductsRequest = SKProductsRequest(productIdentifiers: ids)//上面的商品还要到苹果服务器进行验证, 看下哪些商品是可以真正被销售的(创建一个商品请求并设置请求的代理,由代理告知结果)
request.delegate = self//设置代理, 接收可以被销售的商品列表数据
request.start()//转到extension ViewController: SKProductsRequestDelegate {
}
}
deinit {
SKPaymentQueue.default().remove(self)
}
/// 调起内购
// 检测是否有未完成的交易
func start() {
let transactions = SKPaymentQueue.default().transactions
if transactions.count > 0 {
let transaction = transactions.first
if let firstTransaction = transaction,firstTransaction.transactionState == SKPaymentTransactionState.purchased || firstTransaction.transactionState == SKPaymentTransactionState.purchasing {
SKPaymentQueue.default().finishTransaction(firstTransaction)
SKPaymentQueue.default().remove(self)
}
}
if SKPaymentQueue.canMakePayments() {//判断当前的支付环境, 是否可以支付
// self.showMessage("")
let productModel = self.products.first(where: { (model) -> Bool in
return model.productIdentifier == (你本地的产品内购id)
})
guard let product = productModel else {
self.hideMessage("")
HUD.showError("该产品不支持内购")
return
}
let payment = SKPayment(product: product)
SKPaymentQueue.default().restoreCompletedTransactions()
SKPaymentQueue.default().add(payment)//添加到支付队列
SKPaymentQueue.default().add(self)//监听交易状态
}
}
extension TempViewController: SKProductsRequestDelegate {
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {// 当请求完毕之后, 从苹果服务器获取到数据之后调用
print(response.products)
print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\(response.invalidProductIdentifiers)")
self.products = response.products// 给数据源进行赋值
}
}
extension TempViewController: SKPaymentTransactionObserver {// 交易队列监听
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {// 当交易队列里面添加的每一笔交易状态发生变化的时候调用
switch transaction.transactionState {
case .deferred:
print("延迟处理")
case .failed:
print("支付失败")
queue.finishTransaction(transaction)
SKPaymentQueue.default().remove(self)
case .purchased:
HUD.showInfo("支付成功")
// 本地验证凭证
verifyPurchaseWithPaymentTrasaction()
self.hideMessage("")
// 验证凭证
self.getReceipt()
queue.finishTransaction(transaction)
SKPaymentQueue.default().remove(self)
case .purchasing:
print("正在支付")
case .restored:
self.hideMessage("")
print("已购买过此商品")
SKPaymentQueue.default().remove(self)
default: break
}
}
}
/// 获取票据
func getReceipt() {
let receiptUrl = Bundle.main.appStoreReceiptURL
if let url = receiptUrl {
do {
let receiptData = try Data(contentsOf: url)
logDebug("票据地址\(url)")
let base64String = receiptData.base64EncodedString(options: .endLineWithLineFeed)
// 将凭证传给后台由后台确认
self.checkAppstorePayResultWithBase64String(base64String)
}catch let error {
logDebug(error)
}
}
}
/// 验证购买
func verifyPurchaseWithPaymentTrasaction() {
//测试验证地址
let SANDBOX: String = "https://sandbox.itunes.apple.com/verifyReceipt"
//正式验证地址
let AppStore: String = "https://buy.itunes.apple.com/verifyReceipt"
let receiptUrl = Bundle.main.appStoreReceiptURL
if let url = receiptUrl {
do {
let receiptData = try Data(contentsOf: url)
logDebug("票据地址\(url)")
let receiptURLString = receiptUrl?.absoluteString
var vertifyAddress: String = SANDBOX
vertifyAddress = (receiptURLString?.contains("sandbox") ?? true) ? SANDBOX : AppStore
// 发送网络POST请求,对购买凭据进行验证
if let sandBoxUrl = URL(string: vertifyAddress) {
var urlRequest = URLRequest(url: sandBoxUrl, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10.0)
urlRequest.httpMethod = "POST"
let encodeStr = receiptData.base64EncodedString(options: .endLineWithLineFeed)
let payload = "{\"receipt-data\" : \"\(encodeStr)\"}"
let payloadData = payload.data(using: .utf8)
urlRequest.httpBody = payloadData
do {
let result = try NSURLConnection.sendSynchronousRequest(urlRequest as URLRequest, returning: nil)
if result.isEmpty {
HUD.showError("收据验证失败")
return
}
do {
_ = try JSONSerialization.jsonObject(with: result, options: .allowFragments)
HUD.showInfo("收据验证成功")
}catch let error {
logDebug(error)
}
}
}
}catch let error {
logDebug(error)
}
}
}
在纯知识付费中不能够含有知识付费以外的任何实体的展示和描述,包括地址和教具,由于触及具体的金额消费所以内购不允许使用优惠券,只能搞活动促销了,是不是很悲伤😢😢😢😢😢?

祝每一位在内购路上奋战的小伙伴可以尽快战胜内购💪💪💪💪💪💪!!!
后续:
今天是2020年9月21日,我今天回顾了之前写的这篇关于内购的帖子有些问题,是代码上的问题.

修改为
let session = URLSession.shared.dataTask(with: urlRequest) { (recvData, response, err) in
if err != nil {
print("无法连接服务器,购买校验失败")
return
}
if let data = recvData {
do {
_ = try JSONSerialization.jsonObject(with: data, options: .allowFragments)
print("收据验证成功")
// self.hideMessage("收据验证")
HUD.showInfo("收据验证成功")
// self.hideMessage("收据验证")
}catch let error {
logDebug(error)
}
}
}
session.resume()
}
NSURLConnection.sendSynchronousRequest已被弃用,显改为URLSession.shared.dataTask。

此处去除purchasing状态。
如有其它有误地方,欢迎指正,共同进步。
网友评论