在WWDC2021时,苹果介绍了新版StoreKit的运用 WWDC2021 MeetStoreKit2,作为紧跟时代的弄潮儿,咱们就开端揣摩怎样在当时版别上兼容运用 StoreKit2.

环境承认

在开端接入之前,咱们需求先承认下 StoreKit2 支撑什么环境:

  1. iOS 15.0 及以后
  2. swift 5.5

那么只要确保 Xcode 是 13.0 以后的版别即可满足需求。

接入方法

在接入方法上,要么便是直接在项目中写个 category 或许独自的一个类进行管理内购,要么便是独自生成一个静态库/动态库完成功用,再提供给项目进行接入。
出于组件化的考虑(便利复用、更新与移除,并且也便利多个项目一起运用),所以决议运用 framewrok 的形式完成了 StoreKit2 的充值功用。
所以,这儿就遇到了第一个坑:要么把framwrok支撑的最低版别设置为15.0,要么在对应的类上添加

@available(iOS 15.0, *)

表明这个类仅支撑15.0以上(因为 StoreKit2 仅支撑 15.0 及以上)。


那么有的小伙伴或许就要问了,这个坑它体现在什么地方呢?问得好,这两个方案的问题详细的影响别离如下:

  1. 最低版别设置为 15.0 ,那么生成的架构就仅支撑 arm64 与 x86_64,而不在支撑 armv7、armv7s、i386


Xcode 这边的规矩是假如最低版别设置为 iOS 11.0 及以上时,就只支撑 arm64 架构了 (存疑的小伙伴能够build devices设置为Any iOS Device 修正最低支撑版别并检查括号后边的架构)(吐个槽,随说现在 32 位架构的设备很少了,但公司要求兼容,那也不得不想办法兼容)

StoreKit2 实际接入时候的踩坑与解决实录
StoreKit2 实际接入时候的踩坑与解决实录
  1. @available(iOS 15.0, *)的坑体现在高低版别的 @available 的兼容性上,也便是高版别Xcode库中假如运用到 @available 那么必须运用同版别或许更高版别的 Xcode 才能编译,不然会呈现 __isPlatformVersionAtLeast not found 的反常。

不了解为什么要考虑这一点的小伙伴必定在想咱们都升级到同一大版别不就能够了吗?那么我的答复是:

其实 Xcode 存在不同版别的状况是比较常见的,特别是假如有一些大的项目或许低维的项目都是尽量以维稳为主,除非苹果要求提审必须运用 xx 版别之后的 Xcode 编译的代码。
尤其后续的版别 Xcode 14 也仅支撑 arm64 与 x86_64,不再兼容 32 位架构,此时就回到了问题1,公司想要能兼容就尽量兼容 (卑微打工人的无奈)


简单总结一下,便是第一个方案不支撑 32 位架构,第二个方案至少需求相同版别 Xcode 且后续或许也不支撑 32 位架构。
既然两个方案或早或迟都会有 32 位架构的兼容问题,那么就想办法把兼容架构补上,一劳永逸!
这时分,就需求请出咱们万能的 lipo 指令了。运用到的便是 lipo -create指令兼并架构库,而说出这一点,信任咱们也都知道咱们想做的是什么了。
是的,咱们所需求做的事情便是生成一个空完成的支撑 32 位架构的库(能够经过macro处理,也能够经过独自特殊处理后恢复)。移除 64 位架构之后,再然后将这个仅支撑 32 位架构库的可执行文件额外进行保存,后续在实践的仅支撑 64 位架构库生成之后,经过 lipo -create 指令完成库的兼并,然后让 sdk 能够正常编译。

空完成32位架构处理

graph LR
空完成代码 --> 编译生成库 --> 兼并模拟器与真机架构 --> 经过lipo移除arm64与x86_64架构 --> 保存移除架构后的可执行文件armv7Store

规范库生成处理

graph LR
规范完成代码 --> 编译生成库 --> 兼并模拟器与真机架构 --> 兼并保存的armv7Store --> 输出库

同步给出规范库生成所对应的脚本

#!/bin/sh
#要build的target名
TARGET_NAME=${PROJECT_NAME}
if [[ $1 ]]
then
TARGET_NAME=$1
fi
UNIVERSAL_OUTPUT_FOLDER="${SRCROOT}/../lib/SwiftStoreKitSDK/"
#创建输出目录,并删去之前的framework文件
mkdir -p "${UNIVERSAL_OUTPUT_FOLDER}"
rm -rf "${UNIVERSAL_OUTPUT_FOLDER}/${TARGET_NAME}.framework"
#别离编译模拟器和真机的Framework
xcodebuild -target "${TARGET_NAME}" ONLY_ACTIVE_ARCH=NO -configuration ${CONFIGURATION} -sdk iphoneos BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}" clean build 
xcodebuild -target "${TARGET_NAME}" ONLY_ACTIVE_ARCH=NO -configuration ${CONFIGURATION} -sdk iphonesimulator BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}" clean build
#复制framework到univer目录
cp -R "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${TARGET_NAME}.framework" "${UNIVERSAL_OUTPUT_FOLDER}"
#兼并framework,输出终究的framework到build目录
#删去模拟器的 arm64 架构,防止兼并时分呈现重复架构导致兼并失利的问题
lipo -remove arm64 "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${TARGET_NAME}.framework/${TARGET_NAME}" -output "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${TARGET_NAME}.framework/${TARGET_NAME}"
lipo -create -output "${UNIVERSAL_OUTPUT_FOLDER}/${TARGET_NAME}.framework/${TARGET_NAME}" "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${TARGET_NAME}.framework/${TARGET_NAME}" "${BUILD_DIR}/${CONFIGURATION}-iphoneos/${TARGET_NAME}.framework/${TARGET_NAME}"
#兼并32位架构
lipo -create -output "${UNIVERSAL_OUTPUT_FOLDER}/${TARGET_NAME}.framework/${TARGET_NAME}" "${UNIVERSAL_OUTPUT_FOLDER}/${TARGET_NAME}.framework/${TARGET_NAME}" "${UNIVERSAL_OUTPUT_FOLDER}/armV7Store"
#删去编译之后生成的无关的配置文件
dir_path="${UNIVERSAL_OUTPUT_FOLDER}/${TARGET_NAME}.framework/"
for file in ls $dir_path
do
if [[ ${file} =~ ".xcconfig" ]]
then
rm -f "${dir_path}/${file}"
fi
done
#判别build文件夹是否存在,存在则删去
if [ -d "${SRCROOT}/build" ]
then
rm -rf "${SRCROOT}/build"
fi
rm -rf "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator" "${BUILD_DIR}/${CONFIGURATION}-iphoneos"
#打开兼并后的文件夹
open "${UNIVERSAL_OUTPUT_FOLDER}"

到这儿,咱们就算是走完了 StoreKit 接入的第一步了。

客户端代码完成

接入方法的坑踩完之后,咱们就正式开端接入 StoreKit 的库了,这儿是苹果提供的 StoreKit2 的代码完成 Implementing a store in your app using the StoreKit API
提炼概括总结之后,首要的代码内容为:


func pay(productId:String,uuid:UUID) {
    Task {
        do {
            var requestProductList:[Product] = []
            requestProductList = try await Product.products(for: [productId])
            guard requestProductList.count > 0 else {
            //TODO: 失利回调处理
            return
        }
        currentProduct = requestProductList.first!
        productList.append(currentProduct!)
        guard currentProduct != nil else {return}
        let reuslt = try await currentProduct!.purchase(options: [Product.PurchaseOption.appAccountToken(uuid)])
        switch reuslt {
            case .pending:
            print("pending");
            case .userCancelled:
            //TODO: 失利回调处理
            case .success(let result):
            handleVerfiedTransaction(result: result)
            @unknown default: break
        }
        } catch let storeKit as StoreKitError {
            //TODO: 失利回调处理
        }
        catch {
            //TODO: 失利回调处理
        }
    }
}
/// 验证与初步处理验证经过的凭据信息
/// - Parameter result: 带验证消息的凭据信息
func handleVerfiedTransaction(result:VerificationResult<Transaction>) {
    switch result {
        case .unverified(let unsafe,let verifError):
            //TODO: 失利回调处理
            Task {await unsafe.finish()}
            return
        case .verified(let safe):
            if safe.productType == .consumable {
            //TODO: 成功回调处理
            }
    }
}

StoreKit2 中咱们最重视的便是新添加的 AppAccountToken。在旧版的充值中,是不支撑凭据与订单之间的相关的,咱们伙就只能各显神通,经过各式各样的方法想办法完成相关,之前也写过一篇文章iOS 内购处理方案与流程的探求专门介绍过个人觉得能完成较好相关的方案,但总归对错官方的偏门方案。 所以在看到了 appAccountToken 之后觉得内购的春天总算要来了,不用再苦逼的想办法相关订单了
appAccountToken 的效果便是将你传递的 UUID 类型的目标在充值成功之后在凭据中回来,并且在苹果服务端中继续保存。后续假如经过苹果回来的订单号查询凭据信息时也会带有这个 appAccountToken 然后完成对应用户的查询。

那么第二个块的要点就在于怎么完成这个 UUID 类型的目标,一般的充值总会带有一个找服务端创建订单的操作 (假如没有,那么请说服老板补上),所以能够考虑直接调用 UUID() 生成一个随机的 UUID 目标,再将 UUID 传递给服务端进行下单,下单完成后,把这个 UUID 传递给 StoreKit 进行充值。等充值完成之后,就能够在凭据中找到这个 UUID,然后完成凭据与订单之间的强相关。

可是呢,这个可是他仍是来了。这个是或许是属于个别状况,也便是我公司之前有被苹果正告过,是关于风控那儿的内容。原先的 applicationUserName 是苹果用作风控进行运用的(尽管最新的 applicationUserName 没有说明用作诈骗风控了,可是以防万一适当该怂仍是要怂一些),并且不确保会传递会客户端,之前正告的时分有特意强调这个字段的运用(尽管是依照主张的规范来运用的),所以咱们是运用了用户id来转化完成,能够考虑以下几个方案,不过以下方法会将订单与凭据之间的相关转为凭据与用户之间的相关,不过相同用户发货到账影响不大

  1. 用户id为纯数字时,直接填充,缺乏的部分补充F
  2. 用户id为特殊字符串时,正反各能够md5加密生成16位后拼接,为了防止低概率的md5撞库
  3. 用户id不会太长时,转 hex 字符串并运用 pkcs5 或许 pkcs7 的方法进行填充

祝贺,到了这儿客户端的到充值步骤就算是完成了。完成了充值的第二步,再往后便是充值成功之后怎么验证凭据的问题了。

服务端凭据校验

旧版的凭据,一般是经过 NSBundle 获取本地凭据来完成,而新版这边因为苹果出了新的JWS 的接口,~~详细的完成就由服务端大佬来考虑了,咱们就不用介意这些细节,~~那么就考虑运用 Get Transaction History 直接经过凭据ID回来凭据信息(首要是考虑到一个准则-客户端易篡改不可信)。不过这个接口有一个前提,即买卖不能被封闭,假如买卖被封闭,那么这个接口是查不到对应信息的.

另外还有一点需求注意,这个接口回来的是数组,有时需求查询整个数组才能找到对应的凭据id。

而这儿,就呈现了另外一个大坑,新旧版买卖一起兼容的状况下呈现的。
因为实践环境比较复杂,这边简单说下呈现的前提状况吧

  1. 新版旧版的完成方案不相通,包含订单的处理规矩也不同
  2. 新版旧版做了灰度,会一起对新旧版买卖做监听,灰度判别下单与充值时分走新充值仍是旧充值
  3. 新旧版的凭据上报验证时分先接纳的凭据消息,告诉客户端封闭充值后,再去匹配订单号(为了防止特殊状况下,找不到订单号时一向卡住,导致用户无法充值)

在这个前提下,苹果有一个隐藏的,至少笔者没有找到有说明的点(也有或许是我漏了?),那便是从头告诉买卖成功的时分(买卖未封闭时,从头回调买卖凭据),会两头一起告诉,有时也会呈现旧版告诉买卖封闭之后,后续还会在新版的买卖监听中告诉回调。

收到找不到匹配订单号的买卖凭据时,也封闭买卖事务的原因,别离是因为笔者这边旧版是有做锁单处理的,可参考iOS 内购处理方案与流程的探求了解,假如不封闭,那么后续将一向无法买卖;二是新版充值这边,假如相同商品,上笔买卖未封闭,则苹果下发的回调将一向是未封闭的那笔,也形成用户无法买卖

那么就会有以下三种状况:

  1. 旧版下单时分
sequenceDiagram
苹果->>客户端: 回调充值成功(新)
客户端->>服务端: 上报凭据ID到新版充值上报接口
服务端->>苹果: 经过 history 接口获取凭据信息
苹果-->>服务端: 回来凭据信息
服务端->>服务端: 凭据信息入库
服务端-->>客户端: 告诉封闭买卖
服务端->>服务端: 经过凭据查询订单信息(因为旧版下单,无法找到订单,无法发货)
苹果->>客户端: 回调充值成功(旧)
客户端->>服务端: 上报凭据信息到旧版充值上报接口
服务端->>服务端: 发现凭据id重复
服务端-->>客户端: 告诉封闭买卖
  1. 新版下单时分
sequenceDiagram
苹果->>客户端: 回调充值成功(旧)
客户端->>服务端: 上报凭据ID到旧版充值上报接口
服务端-->>客户端: 告诉封闭买卖
苹果->>客户端: 回调充值成功(新)
客户端->>服务端: 上报凭据信息到新版充值上报接口
苹果-->>服务端: 回来凭据信息中不存在该订单号(买卖事务已被封闭)
服务端-->>客户端: 告诉封闭买卖
服务端->>苹果: 经过 verifyReceipt 接口获取凭据信息
苹果-->>服务端: 回来凭据信息
服务端->>服务端: 凭据信息入库
服务端->>服务端: 经过凭据查询订单信息(因为新版下单,无法找到订单,无法发货)
服务端->>服务端: 发现凭据id重复
  1. 新版下单时分
sequenceDiagram
苹果->>客户端: 回调充值成功(旧)
客户端->>服务端: 上报凭据ID到旧版充值上报接口
服务端-->>客户端: 告诉封闭买卖
服务端->>苹果: 经过 verifyReceipt 接口获取凭据信息
苹果-->>服务端: 回来凭据信息
服务端->>服务端: 凭据信息入库
服务端->>服务端: 经过凭据查询订单信息(因为新版下单,无法找到订单,无法发货)
苹果->>客户端: 回调充值成功(新)
客户端->>服务端: 上报凭据信息到新版充值上报接口
服务端->>服务端: 发现凭据id重复
服务端-->>客户端: 告诉封闭买卖

这三种状况都会形成用户充值不到账的状况,尽管状况相对少,可是关于单个用户而言,必定会炸锅,究竟人家真金白银花出去了,结果什么反响都没有,换谁都必定不爽,所以咱们连夜评论出了两个解决方案

  1. 服务端暂时处理方案:上报的凭据都优先经过 history 接口获取凭据信息,假如存在 appAccountToken 那么固定走新版别对应的订单查询接口(经过 appAccountToken 做相关),假如不存在,那么就不操作,回来客户端不处理,等候旧版的数据上报,上报成功,凭据入库之后,走旧版别对应订单查询(经过 UniqueVendorIdentifier 查询)
graph LR
收到凭据信息 --> 调用history接口获取凭据信息 --> 存在appAccountToken --> 经过新版别订单查询接口相关订单
调用history接口获取凭据信息 --> 不存在appAccountToken --> 经过旧版别订单查询接口相关订单
  1. 客户端完整解决方案:收到新版充值回调时,判别回调内容中是否有 appAccountToken ,假如存在则直接上报,若不存在,则记载订单号,等旧版回调上报成功之后,同步封闭两头凭据,收到旧版充值回调时,假如存在 applicationUserName,那么直接上报旧版回调,不然等候新版充值回调做判别(applicationUserName 或许直接存在为空)
graph LR
收到新版凭据回调 --> 判别回调是否存在appAccountToken --> 存在appAccountToken --> 上报新版充值
判别回调是否存在appAccountToken --> 不存在appAccountToken --> 是否是旧版已记载的订单id
是否是旧版已记载的订单id --> 非旧版已记载的订单id --> 记载订单id --> 等候旧版上报
是否是旧版已记载的订单id --> 是旧版已记载的订单id --> 上报旧版充值 
收到旧版凭据回调 --> 判别回调是否存在applicationUserName --> 存在applicationUserName --> 上报旧版充值
判别回调是否存在applicationUserName --> 不存在applicationUserName --> 是否是新版已记载的订单id  --> 是新版已记载的订单id --> 上报旧版充值
是否是新版已记载的订单id  --> 非新版已记载的订单id --> 记载订单id与凭据信息 --> 等候新版凭据回调

到这儿,基本能够正常的运用咱们的 StoreKit2 了,U1S1,安稳后的 StoreKit2 仍是挺香的,并且服务端也有了 JWS 的运用根底,后续在新添加调用 Look Up Order ID 等接口的时分,也更加简单便利。

结尾

以上则是笔者在运用 StoreKit2 时踩到的一些坑以及处了解决方案的思考与处理过程。
假如有遇到其他的状况,也欢迎随时评论。


  • 我正在参加技能社区创作者签约方案招募活动,点击链接报名投稿。