1.蘋(píng)果官網(wǎng)的配置
a.創(chuàng)建一種商品類目,選擇對(duì)應(yīng)的價(jià)格,添加一種描述,這種描述在支付的時(shí)候會(huì)彈出,沙盒測(cè)試的時(shí)候可體驗(yàn),這種商品會(huì)有一個(gè)對(duì)應(yīng)的id
b.添加一個(gè)沙盒賬號(hào),一般使用一個(gè)不常用的郵箱,這個(gè)郵箱和apple賬號(hào)無(wú)關(guān),只要這個(gè)郵箱能收到驗(yàn)證碼就能綁定成功,沙盒賬號(hào)就是這個(gè)郵箱,密碼自己隨便設(shè)置
2.需要熟悉的一些流程上的東西(20210817補(bǔ)充)
1.用戶選擇商品
客戶端記錄用戶選擇的商品對(duì)應(yīng)的Apple的唯一標(biāo)識(shí)符并向自己公司的服務(wù)端請(qǐng)求接口來(lái)創(chuàng)建訂單,
請(qǐng)求參數(shù)是價(jià)格,和對(duì)應(yīng)的商品的在Apple端的唯一標(biāo)識(shí)
服務(wù)端返回的數(shù)據(jù)包含訂單號(hào)orderId,客戶端記錄這個(gè)orderId
2.客戶端檢測(cè)用戶當(dāng)前設(shè)備是否允許內(nèi)購(gòu)
3.客戶端將當(dāng)前App所有的商品類目列表信息發(fā)送給Apple服務(wù)端,request start,Apple服務(wù)端返回可用的商品列表,payment.applicationUsername最好使用的是自己服務(wù)器的orderId
4.客戶端遍歷Apple服務(wù)端返回的商品列表,判斷第1步中記錄的商品唯一標(biāo)識(shí)符是否存在于第3步Apple返回的商品列表中
5.客服端使用蘋(píng)果提供的方法發(fā)起購(gòu)買(mǎi)請(qǐng)求,這時(shí)候需要用戶輸入AppleID的賬號(hào)和密碼,輸入正確后會(huì)走到支付完成的回調(diào)中
6.客戶端檢查,客戶端根據(jù)蘋(píng)果返回的支付回調(diào)的購(gòu)買(mǎi)憑證通過(guò)接口請(qǐng)求向Apple端進(jìn)行驗(yàn)證
7.驗(yàn)證成功之后將購(gòu)買(mǎi)憑證通過(guò)接口發(fā)送給自己公司的服務(wù)端來(lái)表示當(dāng)前訂單已經(jīng)付款了
8.服務(wù)端收到這個(gè)驗(yàn)證請(qǐng)求,再次使用該購(gòu)買(mǎi)憑證向Apple端驗(yàn)證是否已經(jīng)付款
9.公司的服務(wù)端在通過(guò)Apple的服務(wù)端驗(yàn)證完成之后,表示用戶已經(jīng)完成了內(nèi)購(gòu)支付,做虛擬商品發(fā)放,做驗(yàn)證接口的返回處理操作.
需要考慮到的異常情況:
第5步執(zhí)行完之后,如果用戶沒(méi)有來(lái)的及檢查購(gòu)買(mǎi)狀態(tài)和進(jìn)行下一步的操作的時(shí)候,手機(jī)斷電等異常情況出現(xiàn)時(shí),下次打開(kāi)app或者進(jìn)入到充值頁(yè)面時(shí)候,應(yīng)當(dāng)自動(dòng)來(lái)檢測(cè)一下,當(dāng)前用戶是否有正在執(zhí)行中的支付訂單,如果有,則從第6步開(kāi)始繼續(xù)執(zhí)行支付流程
3.擼代碼
控制器里面導(dǎo)入
#import <StoreKit/StoreKit.h>
這里的URL分為兩個(gè),一個(gè)是沙盒用的,一個(gè)是正式用的
//#ifdef DEBUG
//#define yz_AppStore_URL @"https://sandbox.itunes.apple.com/verifyReceipt"
//#else
//#define yz_AppStore_URL @"https://buy.itunes.apple.com/verifyReceipt"
//#endif
沙盒用的驗(yàn)證:就是可以用沙盒賬號(hào)來(lái)購(gòu)買(mǎi),不用實(shí)際話費(fèi)金額,就直接走了購(gòu)買(mǎi)成功的流程
正式用的:需要實(shí)際現(xiàn)金支付
這里要特別注意,一定要保證給AppleStore審核人員用的是用沙盒驗(yàn)證的而不是正式的
而且服務(wù)端的驗(yàn)證方式也需要和移動(dòng)端的驗(yàn)證方式一致
這里說(shuō)下當(dāng)前我們的處理邏輯:
進(jìn)入付款流程的時(shí)候先默認(rèn)是正式的驗(yàn)證url,然后進(jìn)行接口請(qǐng)求,接口返回一個(gè)版本號(hào),這個(gè)版本號(hào)和當(dāng)前的app里面的版本號(hào)做對(duì)比,如果版本號(hào)一致,那么久將驗(yàn)證地址的url改為沙盒的
這樣當(dāng)提交一個(gè)新的版本給蘋(píng)果審核的時(shí)候,將app審核過(guò)后自動(dòng)發(fā)布改為手動(dòng)發(fā)布.
等蘋(píng)果那邊審核通過(guò)之后,先讓服務(wù)端將接口返回的版本號(hào)改成0.0.0(只要不一致就行了)
然后再進(jìn)行手動(dòng)發(fā)布新版本,這樣就保證用戶手里的一定是正式的,審核人員用的就是沙盒的
獲取版本號(hào),對(duì)比之后看是否需要開(kāi)啟沙盒模式驗(yàn)證
基礎(chǔ)操作:
- (void)viewDidLoad {
[super viewDidLoad];
_appleUrl = @"https://buy.itunes.apple.com/verifyReceipt";
NSString * vFromSelfServer = [self getAppleInfoFromSelfServer];
NSString * vCurrIPA = @"當(dāng)前安裝包的版本號(hào)";
if([vFromSelfServer isEqualToString:vCurrIPA]){
// 當(dāng)前安裝包的版本號(hào)和服務(wù)端返回的版本號(hào)一致,就將appleUrl的值改為沙盒驗(yàn)證,因?yàn)檫@種情況下表示app正在被審核中
_appleUrl = @"https://sandbox.itunes.apple.com/verifyReceipt";
}
// 添加交易監(jiān)聽(tīng)
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
[self.view addSubview:self.payLab];
}
#pragma mark --------- self server request
/**
* 用接口從自己服務(wù)器獲取版本號(hào)
* 1.發(fā)布更新提交審核時(shí)候,從itunes中選項(xiàng)改為手動(dòng)發(fā)布
* 2.讓服務(wù)端將getAppleInfoFromSelfServer這個(gè)接口返回的版本號(hào)和發(fā)布給蘋(píng)果審核的版本號(hào)一致
* 3.等待蘋(píng)果審核通過(guò),先讓服務(wù)端將getAppleInfoFromSelfServer的值改一下成無(wú)限大(這里只要不等于線上所有用戶的版本號(hào),也不等于當(dāng)前已審核通過(guò)的版本號(hào)即可)
* 4.itunes手動(dòng)發(fā)布已審核通過(guò)的ipa包
*/
-(NSString *)getAppleInfoFromSelfServer {
return @"自己服務(wù)器返回的一個(gè)版本號(hào)";
}
當(dāng)用戶點(diǎn)擊了購(gòu)買(mǎi)某個(gè)商品的時(shí)候,調(diào)用自己服務(wù)端接口進(jìn)行訂單創(chuàng)建
-(void)clickPayAction {
// 自己在蘋(píng)果商城申請(qǐng)的類別的ID列表為
// bundleid+xxx 就是你添加內(nèi)購(gòu)條目設(shè)置的產(chǎn)品ID
// zydj.product.pay_6 6元4.2豆
// zydj.product.pay_40 40元28豆
// zydj.product.pay_60 60元48豆
// 假設(shè)選中了6元4.2豆則通過(guò)zydj_product_pay_6在自己服務(wù)端創(chuàng)建訂單
self.currentProducId = @"zydj.product.pay_6";
NSString * orderId = [self creatOrderFromSelfServer:self.currentProducId];
self.orderId = orderId;
[self startInAppPay];
}
/**
* 創(chuàng)建訂單
*/
-(NSString *)creatOrderFromSelfServer:(NSString *)priceID {
return @"服務(wù)端根據(jù)priceID返回的一個(gè)orderId";
}
先驗(yàn)證用戶當(dāng)前使用的設(shè)備是否支持內(nèi)購(gòu),允許就走到productsRequest
/**
* Apple Operation
*/
-(void)startInAppPay {
//判斷是否允許內(nèi)購(gòu)
if ([SKPaymentQueue canMakePayments]) {
NSLog(@"用戶允許內(nèi)購(gòu)");
//bundleid+xxx 就是你添加內(nèi)購(gòu)條目設(shè)置的產(chǎn)品ID
NSArray *product = [[NSArray alloc] initWithObjects:@"zydj.product.pay_6",@"zydj.product.pay_40",@"tv.zydj.app_pay_6",@"zydj.product.pay_60", nil];
NSSet *nsset = [NSSet setWithArray:product];
//初始化請(qǐng)求
SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:nsset];
request.delegate = self;
//開(kāi)始請(qǐng)求
[request start];
// 注意,這里請(qǐng)求成功之后,會(huì)走到)productsRequest:
}else{
// NSLog(@"用戶不允許內(nèi)購(gòu)");
// [ToastUtils showMessage:@"當(dāng)前設(shè)備不允許內(nèi)購(gòu)" duration:1 position:Toast_Point_Center];
}
}
接收到產(chǎn)品的返回信息,然后用返回的商品信息進(jìn)行發(fā)起購(gòu)買(mǎi)請(qǐng)求
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response NS_AVAILABLE_IOS(3_0)
{
NSArray *product = response.products;
//如果蘋(píng)果服務(wù)器沒(méi)有產(chǎn)品,那就return
if([product count] == 0){
NSLog(@"沒(méi)有該商品");
return;
}
SKProduct *requestProduct = nil;
NSString * currentProducId = self.currentProducId;
for (SKProduct *pro in product) {
NSLog(@"%@", [pro description]);
NSLog(@"%@", [pro localizedTitle]);
NSLog(@"%@", [pro localizedDescription]);
NSLog(@"%@", [pro price]);
NSLog(@"%@", [pro productIdentifier]);
//如果后臺(tái)消費(fèi)條目的ID與我這里需要請(qǐng)求的一樣(用于確保訂單的正確性)
if([pro.productIdentifier isEqualToString:currentProducId]){
// 到這一步就說(shuō)明商品存在,開(kāi)始發(fā)送購(gòu)買(mǎi)請(qǐng)求
requestProduct = pro;
SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:requestProduct];
payment.applicationUsername = self.orderId;//可以是userId础米,也可以是訂單id分苇,跟你自己需要而定
[[SKPaymentQueue defaultQueue] addPayment:payment];
// 如果請(qǐng)求成功就會(huì)彈出輸入密碼等頁(yè)面,輸入完密碼之后會(huì)出現(xiàn)一個(gè)支付成功的彈窗,點(diǎn)擊了彈窗之后會(huì)進(jìn)入paymentQueue:方法中
}
}
}
paymentQueue:方法監(jiān)聽(tīng),有兩種方式會(huì)進(jìn)入這個(gè)方法回調(diào)中,
方式1:正常流程,用戶輸入密碼,支付成功之后,點(diǎn)擊了系統(tǒng)彈出的支付成功彈窗,就會(huì)進(jìn)入這個(gè)方法回調(diào)中,
方式2:異常流程,當(dāng)用戶輸入完密碼之后,蘋(píng)果實(shí)際扣款成功,用戶手機(jī)異常退出(閃退或者直接殺掉APP,斷電等情況異常離開(kāi)app),下次打開(kāi)APP進(jìn)入到充值頁(yè)面之后,viewDidLoad中添加了支付回調(diào)監(jiān)聽(tīng)之后,也會(huì)進(jìn)這個(gè)方法回調(diào)中
- (void)paymentQueue:(nonnull SKPaymentQueue *)queue updatedTransactions:(nonnull NSArray<SKPaymentTransaction *> *)transactions {
dispatch_async(dispatch_get_main_queue(), ^{
for(SKPaymentTransaction *tran in transactions){
switch (tran.transactionState) {
case SKPaymentTransactionStatePurchased:{
NSLog(@"交易完成");
// 接下來(lái)的流程
[self completeTransaction:tran];
}
break;
case SKPaymentTransactionStatePurchasing:
NSLog(@"商品添加進(jìn)列表");
break;
case SKPaymentTransactionStateRestored:
NSLog(@"已經(jīng)購(gòu)買(mǎi)過(guò)商品");
//[[SKPaymentQueue defaultQueue] finishTransaction:tran]; 消耗型商品不用寫(xiě)
break;
case SKPaymentTransactionStateFailed:{
//這里說(shuō)明支付失敗了啊.密碼輸入錯(cuò)誤或者沒(méi)扣款成功,那就finish掉這筆交易
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
NSLog(@"交易失敗");
}
break;
default:
break;
}
}
});
}
交易結(jié)束,當(dāng)交易結(jié)束后還要去appstore上驗(yàn)證支付信息是否都正確,只有所有都正確后,我們就可以給用戶方法我們的虛擬物品了。為了防止上一步的方式2進(jìn)入這個(gè)方法中所需要的操作,存入identifier和recepit信息, NSData * data = [NSKeyedArchiver archivedDataWithRootObject:model];和 SAVEDEFAULTS(data, transaction.transactionIdentifier);整體實(shí)現(xiàn)如下
- (void)completeTransaction:(SKPaymentTransaction *)transaction
{
dispatch_async(dispatch_get_main_queue(), ^{
if (self.orderId.length != 0) {
// 這里需要展示一個(gè)加載框,展示出來(lái)之后要禁止掉用戶的點(diǎn)擊其他view區(qū)域的操作,包括左滑返回也給禁止掉
// [MBProgressHUD showLoadHUD:@"加載中..." toView:self.view];
}
// appStoreReceiptURL iOS7.0增加的屁桑,購(gòu)買(mǎi)交易完成后医寿,會(huì)將憑據(jù)存放在該地址
// 驗(yàn)證憑據(jù),獲取到蘋(píng)果返回的交易憑據(jù)
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
// 從沙盒中獲取到購(gòu)買(mǎi)憑據(jù)
NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL];
//發(fā)送POST請(qǐng)求蘑斧,對(duì)購(gòu)買(mǎi)憑據(jù)進(jìn)行驗(yàn)證
NSLog(@"before = %@",receiptURL);
NSURL *url = [NSURL URLWithString:_appleUrl];
NSMutableURLRequest *urlRequest = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:15.0f];
urlRequest.HTTPMethod = @"POST";
NSString *encodeStr = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
_receipt = encodeStr;
//防止異常退出做的操作
if (self.orderId.length != 0) {
YZChargeDefaultsModel * model = [[YZChargeDefaultsModel alloc] init];
model.orderId = self.orderId;
model.receipt = encodeStr;
NSData * data = [NSKeyedArchiver archivedDataWithRootObject:model];
SAVEDEFAULTS(data, transaction.transactionIdentifier);
// 這里存到userdefaults
}
NSString *payload = [NSString stringWithFormat:@"{\"receipt-data\" : \"%@\"}", encodeStr];
NSData *payloadData = [payload dataUsingEncoding:NSUTF8StringEncoding];
urlRequest.HTTPBody = payloadData;
NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:urlRequest completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
// [MBProgressHUD hideHUD];
if (data == nil) {
NSLog(@"驗(yàn)證失敗");
return;
}
NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
NSLog(@"請(qǐng)求成功后的數(shù)據(jù):%@",dic);
//這里可以通過(guò)判斷 state == 0 驗(yàn)證憑據(jù)成功靖秩,然后進(jìn)入自己服務(wù)器二次驗(yàn)證须眷,,也可以直接進(jìn)行服務(wù)器邏輯的判斷。
//本地服務(wù)器驗(yàn)證成功之后別忘了 [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
NSString *productId = transaction.payment.productIdentifier;
NSString *applicationUsername = transaction.payment.applicationUsername;
NSLog(@"applicationUsername++++%@",applicationUsername);
NSLog(@"payment.productIdentifier++++%@",productId);
if (dic != nil) {
NSLog(@"order id == null");
NSArray * array = dic[@"receipt"][@"in_app"];
if (array.count <= 0) {
return;
}
self.orderId = applicationUsername;
//服務(wù)器二次驗(yàn)證
NSLog(@"%@",transaction);
for (int i = 0; i < array.count; i ++) {
NSDictionary * dic = array[i];
NSString * transaction_id = dic[@"transaction_id"];
if ([transaction.transactionIdentifier isEqualToString:transaction_id]) {
if (self.orderId.length != 0) {
[self vertifyApplePayRequestWith:transaction receipt:self.receipt transactionOrderId:self.orderId];
}else {
NSData * data = GETDEFAULTS(transaction.transactionIdentifier);
YZChargeDefaultsModel * model = [NSKeyedUnarchiver unarchiveObjectWithData:data];
if (model.orderId.length != 0 && model.receipt.length != 0) {
[self vertifyApplePayRequestWith:transaction receipt:model.receipt transactionOrderId:model.orderId];
}else {
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
}
}
}
}
}];
[task resume];
});
}
上面代碼中的遍歷和二次處理,方式1直接發(fā)起驗(yàn)證請(qǐng)求,方式2從本地拿到一些信息數(shù)據(jù),向服務(wù)端發(fā)起驗(yàn)證請(qǐng)求
for (int i = 0; i < array.count; i ++) {
NSDictionary * dic = array[i];
NSString * transaction_id = dic[@"transaction_id"];
if ([transaction.transactionIdentifier isEqualToString:transaction_id]) {
if (self.orderId.length != 0) {
[self vertifyApplePayRequestWith:transaction receipt:self.receipt transactionOrderId:self.orderId];
}else {
NSData * data = GETDEFAULTS(transaction.transactionIdentifier);
YZChargeDefaultsModel * model = [NSKeyedUnarchiver unarchiveObjectWithData:data];
if (model.orderId.length != 0 && model.receipt.length != 0) {
[self vertifyApplePayRequestWith:transaction receipt:model.receipt transactionOrderId:model.orderId];
}else {
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
}
}
}
根據(jù)一些參數(shù),向服務(wù)端請(qǐng)求驗(yàn)證當(dāng)前訂單是否支付
transaction.transactionIdentifier 唯一的
receipt 一般這個(gè)數(shù)據(jù)很長(zhǎng)很長(zhǎng)很長(zhǎng), md5一下,傳輸方便
orderId
服務(wù)端的一些處理
先判斷這個(gè)transaction.transactionIdentifier對(duì)應(yīng)的訂單是否支付成功且發(fā)放了虛擬物品
如果是就直接返回一個(gè)狀態(tài)比如狀態(tài)1
客戶端在狀態(tài)1的處理就是直接finish掉這筆交易
如果發(fā)送虛擬物品的記錄中沒(méi)有這條記錄,那么先通過(guò)transaction.transactionIdentifier向apple再次驗(yàn)證一下是否支付成功,如果支付成功,那么就發(fā)放虛擬物品給用戶,并在當(dāng)前接口中返回狀態(tài)2
客戶端在狀態(tài)2的處理就是 finish掉這筆交易,并刪除本地的transactionIdentifier對(duì)應(yīng)的YZChargeDefaultsModel,并記錄一下transactionIdentifierd對(duì)應(yīng)的交易已經(jīng)成功,為了應(yīng)對(duì)狀態(tài)1的情況
如果發(fā)送虛擬物品的記錄中沒(méi)有這條記錄,那么先通過(guò)transaction.transactionIdentifier向apple再次驗(yàn)證一下是否支付成功,如果支付失敗,那么返回狀態(tài)3給客戶端,然后什么都不用記錄
客戶端在狀態(tài)3的處理就是直接finish掉這筆交易,因?yàn)榉?wù)端驗(yàn)證獲取到的信息是用戶支付的資金沒(méi)有到賬
/**
* 驗(yàn)證訂單
*/
-(void)vertifyApplePayRequestWith:(SKPaymentTransaction *)transaction receipt:(NSString *)receipt transactionOrderId:(NSString *)orderId{
// NSMutableDictionary * dic = [NSMutableDictionary dictionary];
// [dic setObject:receipt forKey:@"receipt_data"];
// NSString * alltransId = [NSString stringWithFormat:@"%@%@",YZ_Transaction_ID,transaction.transactionIdentifier];
// NSString * needtransID = [alltransId md5String];
// [dic setObject:orderPhpId forKey:@"orderNum"];
// [dic setObject:needtransID forKey:@"transactionID"];
NSInteger status = 0;
if(status == 1){
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}else if (status == 2){
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}else if(status == 3) {
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}
if (self.orderId.length != 0) {
// [ToastUtils showMessage:responseObjc[@"msg"] duration:1 position:Toast_Point_Center];
self.orderId = @"";
[[NSUserDefaults standardUserDefaults] removeObjectForKey:transaction.transactionIdentifier];
}
}
整體流程大概是這樣實(shí)現(xiàn)的,Demo