配置
https://appstoreconnect.apple.com/agreements/
设置税务和银行业务
银行卡
App Store Connect 协议、税务和银行业务中,给付费APP类型添加银行卡需要填写 CNAPS代码,其实就是联行号。
联行号又称大额行号、银联号、银行行号或CNAPS号。
https://www.cwjyz.com.cn/bank/index.html
报税表
- 默认选择美国
- 填写美国报税表, 全部
否
报税人的职位,如 CEO / CTO / Manager 等
勾选同意, 提交
App 内购项目
产品 ID
用于报告的唯一字母数字 ID。一旦你将产品 ID 用于某产品,即使删除该产品,此产品 ID 也无法再次使用
我的格式 bundleId_产品名_序号
(如: com.comp.app_vip_1
)
登录/内购
https://developer.apple.com/account/resources/identifiers
沙盒测试
任何东西随便填, 自己能记住就行,
正确的步骤: 选购商品, 支付, 弹出窗口并登录沙盒, 在 手机/设置/App Store 维护
我傻乎乎的跑邮箱去认证了一下, 成功注册了新的 apple id
沙盒账户无法自主登录, 所以最开始在iPhone - 设置 - App Store
找不到沙盒账户
入口, 必须从支付那里触发登录
SpringBoot
使用
@ApiOperation("appleId 登录")
@PostMapping(value = "/sign_in_with_apple")
public Result signInWithApple(String userId, String name, String token) {
try {
//验证identityToken
if(!AppleUtil.verify(token, userId)){
return Result.fail("授权验证失败");
}
// TODO: 保存用户信息
return Result.success(name);
} catch (Exception e) {
return Result.fail("登录失败,稍后再试");
}
}
pom.xml
<!-- sign in with apple -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>jwks-rsa</artifactId>
<version>0.9.0</version>
</dependency>
AppleUtil
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.auth0.jwk.Jwk;
import io.jsonwebtoken.*;
import lombok.Data;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.springframework.web.client.RestTemplate;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.math.BigInteger;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.security.spec.RSAPublicKeySpec;
import java.util.HashMap;
import java.util.List;
@Slf4j
public class AppleUtil {
/**
* 获取苹果的公钥
*
* @return
* @throws Exception
*/
private static JSONArray getAuthKeys() {
String url = "https://appleid.apple.com/auth/keys";
RestTemplate restTemplate = new RestTemplate();
JSONObject json = restTemplate.getForObject(url, JSONObject.class);
JSONArray arr = json.getJSONArray("keys");
return arr;
}
/**
* 生成授权公钥
*/
private static PublicKey buildAuthPublicKey(String kid) {
try {
// 调用苹果接口获取公钥参数
JSONArray keys = getAuthKeys();
if (keys == null || keys.size() == 0) {
return null;
}
JSONObject currentKey = null;
// 通常情况下返回值keys包含2个,需要根据kid来决定使用哪套公钥参数
for (int i = 0; i < keys.size(); i++) {
JSONObject key = keys.getJSONObject(i);
if (kid.equals(key.getString("kid"))) {
currentKey = key;
break;
}
}
if (currentKey == null) {
return null;
}
Jwk jwa = Jwk.fromValues(currentKey);
PublicKey publicKey = jwa.getPublicKey();
return publicKey;
// String n = currentKey.getString("n");
// String e = currentKey.getString("e");
// BigInteger modulus = new BigInteger(1, Base64.decodeBase64(n));
// BigInteger publicExponent = new BigInteger(1, Base64.decodeBase64(e));
// RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, publicExponent);
// KeyFactory kf = KeyFactory.getInstance("RSA");
// return kf.generatePublic(spec);
} catch (Exception ex) {
ex.printStackTrace();
}
return null;
}
/**
* 验证授权信息是否有效
*
* @param identityToken
* @param userId
* @return
*/
public static boolean verify(String identityToken, String userId) {
String[] identityArr = identityToken.split("\\.");
if (identityArr.length == 0) {
return false;
}
try {
// {"kid":"YuyXoY","alg":"RS256"}
String headStr = new String(Base64.decodeBase64(identityArr[0]), StandardCharsets.UTF_8);
JSONObject headData = JSONObject.parseObject(headStr);
/*
{
"iss": "https://appleid.apple.com",
"aud": "com.**.**",
"exp": 16819**99,
"iat": 1681**899,
"sub": "000645.9d**256.1141",
"c_hash": "1Y9j6**Z-PSDQQ",
"email": "vqz**elay.appleid.com",
"email_verified": "true",
"is_private_email": "true",
"auth_time": 1681872899,
"nonce_supported": true
}
*/
String identityStr = new String(Base64.decodeBase64(identityArr[1]), StandardCharsets.UTF_8);
JSONObject identityData = JSONObject.parseObject(identityStr);
log.debug("headStr: " + headStr);
log.debug("identityStr: " + identityStr);
String kid = headData.getString("kid");
String sub = identityData.getString("sub");
String iss = identityData.getString("iss");
String aud = identityData.getString("aud");
// 对比iOS客户端传过来的用户唯一标识是否和授权凭证一致
if (!userId.equals(sub)) {
return false;
}
PublicKey publicKey = buildAuthPublicKey(kid);
if (publicKey == null) {
return false;
}
JwtParser jwtParser = Jwts.parser().setSigningKey(publicKey);
jwtParser.requireIssuer(iss); // https://appleid.apple.com
// iOS应用标识
jwtParser.requireAudience(aud);
// 用户的唯一标识
jwtParser.requireSubject(sub);
Jws<Claims> claims = jwtParser.parseClaimsJws(identityToken);
if (claims != null && claims.getBody().containsKey("auth_time")) {
return true;
}
} catch (Exception ex) {
ex.printStackTrace();
}
return false;
}
// MARK: apple 内购 (In App Purchases)
@Data
@Accessors(chain = true)
public static class IAPReceiptRespModel {
/** 订单号 */
private String transactionId;
/** 产品 id*/
private String productId;
/** 有 2 种数据类型, 要么直接 productId, 要么在 in_app 数组里 */
private List<IAPReceiptInAppRespModel> inApp;
}
@Data
public static class IAPReceiptInAppRespModel {
/** 订单号 */
private String transactionId;
/** 产品 id*/
private String productId;
}
/** 购买凭证验证地址 */
private static final String verifyReceiptURL = "https://buy.itunes.apple.com/verifyReceipt";
/** 测试的购买凭证验证地址 */
private static final String sandboxVerifyReceiptURL = "https://sandbox.itunes.apple.com/verifyReceipt";
/**
* 重写X509TrustManager
*/
private static TrustManager myX509TrustManager = new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
};
/**
* 发送请求 向苹果发起验证支付请求是否有效:本方法有认证方法进行调用
*
* @param isSandbox 支付的环境
* @param receiptStr 接口传递的 receipt
* @return 结果
*/
public static String receiptResult(boolean isSandbox, String receiptStr) {
String urlStr = verifyReceiptURL;
if (isSandbox) {
urlStr = sandboxVerifyReceiptURL;
}
// 将传过来的转义符 """ 替换成 "\""
//receiptStr = receiptStr.replaceAll(""","\"");
try {
//设置SSLContext
SSLContext ssl = SSLContext.getInstance("SSL");
ssl.init(null, new TrustManager[]{myX509TrustManager}, new java.security.SecureRandom());
URL console = new URL(urlStr);
//打开连接
HttpsURLConnection conn = (HttpsURLConnection) console.openConnection();
//设置套接工厂
conn.setSSLSocketFactory(ssl.getSocketFactory());
//加入数据
conn.setRequestMethod("POST");
//conn.setRequestProperty("Content-type", "application/json");
conn.setRequestProperty("content-type", "text/json");
conn.setRequestProperty("Proxy-Connection", "Keep-Alive");
conn.setDoInput(true);
conn.setDoOutput(true);
//String str = String.format(java.util.Locale.CHINA, "{\"receipt-data\":\"" + receiptStr + "\"}"); //拼成固定的格式传给平台
JSONObject obj = new JSONObject();
obj.put("receipt-data", receiptStr);
BufferedOutputStream buffOutStr = new BufferedOutputStream(conn.getOutputStream());
buffOutStr.write(obj.toString().getBytes());
buffOutStr.flush();
buffOutStr.close();
//获取输入流
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line = null;
StringBuffer sb = new StringBuffer();
while ((line = reader.readLine()) != null) {
sb.append(line);
}
return sb.toString();
} catch (Exception ex) {
ex.printStackTrace();
}
return null;
}
public static Result<IAPReceiptRespModel> checkReceipt(String receiptStr, String transactionId) {
// 错误的对象是:{"status":21002},苹果官网写的错误也都是2XXXX 具体含义可查:https://developer.apple.com/documentation/appstorereceipts/status
// 先提交prod进行验证,如果返回status=21007,然后我们在提交sandbox验证。
String resultStr = receiptResult(false, receiptStr);
if (resultStr == null) {
return Result.fail("苹果验证失败,返回数据为空");
} else {
log.info("苹果验证json: " + resultStr);
JSONObject appleReturn = JSONObject.parseObject(resultStr);
String status = appleReturn.getString("status");
//无数据则沙箱环境验证
if ("21007".equals(status)) {
resultStr = receiptResult(true, receiptStr);
log.info("沙盒环境, 苹果验证json: " + resultStr);
appleReturn = JSONObject.parseObject(resultStr);
status = appleReturn.getString("status");
}
if ("0".equals(status)) {
// status=0,表示支付成功
//HashMap receiptMap = appleReturn.getObject("receipt", HashMap.class);
//JSONObject receiptJSON = appleReturn.getJSONObject("receipt");
IAPReceiptRespModel resp = appleReturn.getObject("receipt", IAPReceiptRespModel.class);
if (resp.inApp == null || resp.inApp.isEmpty()) {
if (transactionId.equals(resp.getTransactionId())) {
return Result.success(resp);
}
} else {
if (resp.inApp.contains(transactionId)){
return Result.success(resp);
}
}
return Result.fail("订单不匹配");
} else {
return Result.fail("苹果支付失败" + status);
}
}
}
}
Swift
//
// AppleUtil.swift
//
import Foundation
import RxSwift
import AuthenticationServices
import StoreKit
/*
- 苹果审核: [如果 App 使用第三方或社交登录服务 (例如,Facebook 登录、Google 登录、通过 Twitter 登录、通过 LinkedIn 登录、通过 Amazon 登录或微信登录) 来对其进行设置或验证这个 App 的用户主帐户,则该 App 必须同时提供“通过 Apple 登录”作为同等选项。](https://developer.apple.com/cn/app-store/review/guidelines/#4.8)
- 示例代码: [Implementing User Authentication with Sign in with Apple](https://developer.apple.com/documentation/authenticationservices/implementing_user_authentication_with_sign_in_with_apple)
- 按钮样式: https://appleid.apple.com/signinwithapple/button
*/
/**
- 开发者账号中勾选 苹果登录
- Xcode 签名中添加 苹果登录
*/
@available(iOS 13, *)
class AppleUtil: NSObject {
static let shared = AppleUtil()
private override init(){
super.init()
}
/**
- AsyncSubject<String>.init() 将在源 Observable 产生完成事件后,发出最后一个元素
- PublishSubject<String>.init() 将对观察者发送订阅后产生的元素,而在订阅前发出的元素将不会发送给观察者。
- ReplaySubject<String>.create(bufferSize: 8) 将对观察者发送缓存的8个元素 (或者全部的元素),无论观察者是何时进行订阅的。
- BehaviorSubject<String>.init(value: "") 对观察者进行订阅时,它会将源 Observable 中最新的元素发送出来(或发送默认元素)
*/
let userSubject = PublishSubject<AppleUserModel>.init()
}
// MARK: apple 登录 (sign in with apple)
extension AppleUtil {
struct AppleUserModel {
let user: String?
let token: String?
let name: String?
let error: String?
}
func checkAuth(){
let appleIDProvide = ASAuthorizationAppleIDProvider()
appleIDProvide.getCredentialState(forUserID: "") { (credentialState: ASAuthorizationAppleIDProvider.CredentialState, err: Error?) in
switch credentialState {
case .revoked, .notFound:
break
default:
break
}
}
}
/// 发起苹果登录
/// 执行授权请求来获取用户的全名和电子邮件地址
func login() {
// 基于用户的Apple ID授权用户,生成用户授权请求的一种机制 主要作用是用创建相应的请求,查询用户授权状态
let appleIDProvide = ASAuthorizationAppleIDProvider()
// 授权请求AppleID
let appIDRequest: ASAuthorizationAppleIDRequest = appleIDProvide.createRequest()
// 在用户授权期间请求的联系信息 设置具体的请求信息
appIDRequest.requestedScopes = [ASAuthorization.Scope.fullName]
// 由ASAuthorizationAppleIDProvider创建的授权请求 管理授权请求的控制器
let authorizationController = ASAuthorizationController(authorizationRequests: [appIDRequest])
// 设置授权控制器通知授权请求的成功与失败的代理
authorizationController.delegate = self
// 设置提供 展示上下文的代理,在这个上下文中 系统可以展示授权界面给用户
authorizationController.presentationContextProvider = self
// 在控制器初始化期间启动授权流
authorizationController.performRequests()
}
// 如果存在iCloud Keychain 凭证或者AppleID 凭证提示用户
func perfomExistingAccountSetupFlows() {
// 基于用户的Apple ID授权用户,生成用户授权请求的一种机制
let appleIDProvide = ASAuthorizationAppleIDProvider()
// 授权请求AppleID
let appIDRequest = appleIDProvide.createRequest()
// 为了执行钥匙串凭证分享生成请求的一种机制
let passwordProvider = ASAuthorizationPasswordProvider()
let passwordRequest = passwordProvider.createRequest()
// 由ASAuthorizationAppleIDProvider创建的授权请求 管理授权请求的控制器
let authorizationController = ASAuthorizationController(authorizationRequests: [appIDRequest, passwordRequest])
// 设置授权控制器通知授权请求的成功与失败的代理
authorizationController.delegate = self
// 设置提供 展示上下文的代理,在这个上下文中 系统可以展示授权界面给用户
authorizationController.presentationContextProvider = self
// 在控制器初始化期间启动授权流
authorizationController.performRequests()
}
}
// MARK: 授权请求结果 ASAuthorizationControllerDelegate
extension AppleUtil: ASAuthorizationControllerDelegate {
/// 授权成功回调
/// app 使用该函数将用户的数据储存在钥匙串中
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
// 用户登录使用ASAuthorizationAppleIDCredential
let user = appleIDCredential.user
let identityToken = appleIDCredential.identityToken ?? Data()
let token = String(data: identityToken, encoding: String.Encoding.utf8) ?? ""
let authorizationCode = appleIDCredential.authorizationCode ?? Data()
// 使用过授权的,可能获取不到以下三个参数
let familyName = appleIDCredential.fullName?.familyName ?? ""
let givenName = appleIDCredential.fullName?.givenName ?? ""
//let email = appleIDCredential.email ?? ""
// 用于判断当前登录的苹果账号是否是一个真实用户,取值有:unsupported、unknown、likelyReal
//let realUserStatus = appleIDCredential.realUserStatus
log.info("AppleUtil.ASAuthorizationAppleIDCredential ==== user \(user)")
log.info("AppleUtil.ASAuthorizationAppleIDCredential ==== token \(token)")
log.info("AppleUtil.ASAuthorizationAppleIDCredential ==== familyName \(familyName)")
log.info("AppleUtil.ASAuthorizationAppleIDCredential ==== givenName \(givenName)")
let model = AppleUserModel(user: user, token: token, name: "\(familyName)\(givenName)", error: nil)
userSubject.onNext(model)
} else if let passworCreddential = authorization.credential as? ASPasswordCredential {
// 这个获取的是iCloud记录的账号密码,需要输入框支持iOS 12 记录账号密码的新特性,如果不支持,可以忽略
// Sign in using an existing iCloud Keychain credential.
// 用户登录使用现有的密码凭证
// 密码凭证对象的用户标识 用户的唯一标识
let user = passworCreddential.user
// 密码凭证对象的密码
let password = passworCreddential.password
log.info("AppleUtil.ASPasswordCredential ==== user \(user)")
log.info("AppleUtil.ASPasswordCredential ==== password \(password)")
// 目前不支持 用户名+密码 登录
let model = AppleUserModel(user: user, token: password, name: nil, error: nil)
userSubject.onNext(model)
}
}
/// 授权失败回调
func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
let errorStr : String
switch (error as NSError).code {
case ASAuthorizationError.canceled.rawValue :
errorStr = "用户取消了授权请求"
case ASAuthorizationError.failed.rawValue :
errorStr = "授权请求失败"
case ASAuthorizationError.invalidResponse.rawValue :
errorStr = "授权请求无响应"
case ASAuthorizationError.notHandled.rawValue :
errorStr = "未能处理授权请求"
case ASAuthorizationError.unknown.rawValue :
errorStr = "授权请求失败原因未知"
default:
errorStr = error.localizedDescription
}
let model = AppleUserModel(user: nil, token: nil, name: nil, error: errorStr)
userSubject.onNext(model)
}
}
// MARK: 展示授权控制器的上下文 ASAuthorizationControllerPresentationContextProviding
extension AppleUtil: ASAuthorizationControllerPresentationContextProviding {
// 从 app 中获取窗口, 将“通过 Apple 登录”内容以模态表单的形式呈现给用户。
func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
return UI.window ?? ASPresentationAnchor()
}
}
// MARK: apple 内购 (In App Purchases)
extension AppleUtil {
struct AppleIAPModel {
/// 生产环境验证地址
static let verifyReceipt = "https://buy.itunes.apple.com/verifyReceipt"
/// 沙盒验证地址
static let itmsSandboxVerifyReceipt = "https://sandbox.itunes.apple.com/verifyReceipt"
static let productIdentifiers: Set<String> = [
"bundle_id_product_name_1",
"bundle_id_product_name_2",
"bundle_id_product_name_3",
]
}
var receiptURL: String {
switch NetworkApi.activeEnvironment {
case .appStore:
return AppleIAPModel.verifyReceipt
case .develop:
return AppleIAPModel.itmsSandboxVerifyReceipt
}
}
/// 添加监听
func addSKPaymentTransactionObserver() {
SKPaymentQueue.default().add(self)
}
/// 申请商品列表
func productsRequest() {
HUD.show()
let productsRequest = SKProductsRequest(productIdentifiers: AppleIAPModel.productIdentifiers)
productsRequest.delegate = self
productsRequest.start()
}
func payment(product: SKProduct){
guard SKPaymentQueue.canMakePayments() else {
HUD.error(title: "不支持内购", message: nil)
return
}
HUD.show()
let payment = SKMutablePayment(product: product)
payment.quantity = 1 // 商品数量
SKPaymentQueue.default().add(payment) // SKPaymentQueue负责与App Store的通信
}
func reqReceipt(){
let receiptRefreshReq = SKReceiptRefreshRequest(receiptProperties: nil)
receiptRefreshReq.delegate = self
receiptRefreshReq.start()
}
func getReceipt(){
// 验证购买凭据
// Get the receipt if it's available.
if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {
do {
let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)
let encodedReceipt = receiptData.base64EncodedString(options: Data.Base64EncodingOptions.endLineWithLineFeed)
log.console("==== AppleUtil ==== check \(encodedReceipt)")
}
catch { print("Couldn't read receipt data with error: " + error.localizedDescription) }
} else {
reqReceipt()
}
}
/// 放在服务器上 校验
func check(encodedReceipt: String) {
let param = ["receipt-data" : encodedReceipt]
let urlStr = AppleIAPModel.verifyReceipt
guard let body = try? JSONSerialization.data(withJSONObject: param, options: JSONSerialization.WritingOptions.prettyPrinted),
let url = URL(string: urlStr) else { return }
var request = URLRequest(url: url, cachePolicy: URLRequest.CachePolicy.useProtocolCachePolicy, timeoutInterval: 20)
request.httpMethod = "POST"
request.httpBody = body
let session = URLSession.shared
let task = session.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?)in
guard let data = data,
let dict = try? JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers) as? [String: Any?],
let status = dict["status"] as? Int, status == 0
else {
return
}
let original_transaction_id = dict["original_transaction_id"]
let transaction_id = dict["transaction_id"]
let product_id = dict["product_id"]
}
task.resume()
}
}
extension AppleUtil: SKProductsRequestDelegate {
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
HUD.hide()
let products: [SKProduct] = response.products
let invalidProductIdentifiers: [String] = response.invalidProductIdentifiers
log.console("==== AppleUtil ==== 商品: \(invalidProductIdentifiers)")
products.forEach {
log.console("==== AppleUtil ==== 商品, 标志符: \($0.productIdentifier)")
log.console("==== AppleUtil ==== 商品, 标题: \($0.localizedTitle)")
log.console("==== AppleUtil ==== 商品, 描述: \($0.localizedDescription)")
log.console("==== AppleUtil ==== 商品, 价格: \($0.price)")
log.console("==== AppleUtil ==== 商品, 本地价格: \($0.priceLocale)")
//创建一个NumberFormatter对象
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .currencyAccounting
numberFormatter.positiveSuffix = "元" //自定义后缀
numberFormatter.locale = $0.priceLocale
let format = numberFormatter.string(from: $0.price)
log.console("==== AppleUtil ==== 商品, format 价格: \(format)")
let currency = NumberFormatter.localizedString(from: $0.price, number: NumberFormatter.Style.currency)
log.console("==== AppleUtil ==== 商品, currency 价格: \(currency)")
}
}
}
extension AppleUtil: SKPaymentTransactionObserver {
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
HUD.hide()
for transaction: SKPaymentTransaction in transactions {
let transactionIdentifier = transaction.transactionIdentifier
print("==== AppleUtil ==== 付款, transaction: \(transactionIdentifier)")
let productIdentifier = transaction.payment.productIdentifier
print("==== AppleUtil ==== 付款, 商品: \(productIdentifier)")
let transactionState: SKPaymentTransactionState = transaction.transactionState
switch transactionState {
case SKPaymentTransactionState.purchasing:
print("==== AppleUtil ==== 付款状态: 付款中")
case .purchased:
print("==== AppleUtil ==== 付款状态: 已付款")
getReceipt()
SKPaymentQueue.default().finishTransaction(transaction)
case .failed:
print("==== AppleUtil ==== 付款状态: 付款失败")
SKPaymentQueue.default().finishTransaction(transaction)
case .restored:
print("==== AppleUtil ==== 付款状态: 之前已经付款")
SKPaymentQueue.default().finishTransaction(transaction)
case .deferred:
print("==== AppleUtil ==== 付款状态: 待定, 需要家长确认")
@unknown default:
print("==== AppleUtil ==== 付款状态: default")
SKPaymentQueue.default().finishTransaction(transaction)
}
}
}
}