1. 程式人生 > 其它 >齊桓公ios內購對接之原始碼篇--詳細

齊桓公ios內購對接之原始碼篇--詳細

在開篇之前首先提供一個除錯工具,對ios開發人員來說微不足道,但對新手來說可謂如獲至寶,mac工具,圖示如下控制檯

開啟之後

有時候我們的xcode並不能丟擲這些錯誤,只有通過控制檯連線手機才可才看所有相關的資訊日誌,報錯資訊等

對接ios內購

前提條件:ios內購有一個庫storeKit.framework,這個庫封裝好了ios內購相關的程式碼,我們只需要呼叫即可,一般情況下工程會自動新增這個庫,同時我們要保證自己的app開啟了內購,在開發者平臺的appid裡可以開啟,現在都是預設開啟了

那麼第一步我們要先了解ios內購的流程,看過很多教程,流程圖都一大堆,但桓公只做最簡單的流水線流程

通過productid向蘋果伺服器請求商品 --- 拿到之後即掉支付進入支付佇列 --- 支付完成後回撥支付狀態 ----  支付完成之後會有一個receipt欄位是一個加密字串  --- 把這個receipt給到自己的服務端 ----  自己的服務端對receipt進行解碼並向蘋果伺服器驗證  ----  驗證成功  通知客戶端發放獎勵   

基本流程如此,就相當於  去書店(appstore)買書 --- 買黃帝內經素問(productid) ---  書店查有沒有(請求商品)  ---- 有素問(查詢成功) ----  支付成功店裡銀行賬戶到賬了(蘋果支付了)  給了一個發票 ----  拿著發票去服務人員那領書素問 (向伺服器請求receipt 並驗證成功 發放獎勵) --- 完成

流程清楚了,接下來就好辦了,需要一步步來,不要越步  ,做任何事只要你知道了流程就如同你預知了未來一樣,只要按照步驟流程走,預言就一定會實現

此以laya為例,由於laya生產的ios專案是需要H5這邊來掉的,雖然laya自帶了一個conchMarket,但這裡不用他的,因為有坑,這裡會自己封裝JSBridge,JSBridge是客戶端與H5的通訊橋樑,皆以字串的形式完成掉用和回撥

在jsbridge裡新增方法

+(void)callBackToJs:(NSString*)strFunc param:(NSString*)param
{
    [[conchRuntime GetIOSConchRuntime] callbackToJSWithClassName:NSStringFromClass(self.class) methodName:strFunc ret:param];
}
+(void)callRunJs:(NSString*)strCode
{
    [[conchRuntime GetIOSConchRuntime] runJS:strCode];
}

callBackToJs是回撥js的方法,其中的methodName既是js端掉用的方法名,注意這裡的回撥是非同步的

向jsbridge新增內購及回撥的方法

//蘋果內購
+(void)doIosZF:(NSString*)jsonParam
{
    NSData* data = [jsonParam dataUsingEncoding:NSUTF8StringEncoding];
    NSDictionary* json = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
    if (json == nil) return [self iosPayCallBack:-1 reason:@"訂單資訊錯誤!" receipt:@"" orderid:@""];
    NSString *productId = json[@"productID"];
    NSString *orderid = json[@"orderID"];
    if (productId == nil || orderid == nil) return [self iosPayCallBack:-1 reason:@"訂單資訊缺失!" receipt:@"" orderid:@""];
    [[IAPManager getInstance] buy:productId orderid:orderid];
}

+(void)iosPayCallBack:(NSInteger)p_iRet reason:(NSString *)p_pReason receipt:(NSString *)p_pReceipt orderid:(NSString *)p_pOrderid
{
    NSString* pJsonString = @"{  \"act\" : \"0\", \"tip\" : \"error\", \"order\" : \"error\", \"receipt\" : \"error\"}";
    NSString* sNum = [NSString stringWithFormat:@"%ld",p_iRet];
    NSDictionary* pDictionary = [NSDictionary dictionaryWithObjectsAndKeys:sNum,@"act",p_pReason?p_pReason:@"error",@"tip",p_pOrderid?p_pOrderid:@"error",@"order",p_pReceipt?p_pReceipt:@"error",@"receipt",nil];
    NSError* pError = nil;
    NSData* pJsonData = [NSJSONSerialization dataWithJSONObject:pDictionary options:NSJSONWritingPrettyPrinted error:&pError];
    if( !pError )
    {
        pJsonString = [[NSString alloc] initWithData:pJsonData encoding:NSUTF8StringEncoding];
    }
    [self callBackToJs:@"doIosZF:" param:pJsonString];
}

js端則如下

private static doIosZF(productId, cb_completed){
        let func = (res)=>{
            let code = res["act"];
            log("---ios iap back act:", code, "data:", res);
            if (code == 0) {
                let param =  {
                    token:userData.token,
                    uid:userData.uid,
                    receipt:res.receipt,
                    product_id:productId
                };
                WebAPI.post('shop/applePay',param,(obj)=>{
                    let son = JSON.parse(obj);
                    dealPayEnd(son.code, cb_completed);
                })
            }
            else{
                app.showAlert('支付失敗');
            }
        }
        let params = {
            "orderID":`OrderID_${new Date().getTime()}_${productId}`,
            "productID":this.marketCommedities[productId],
        }
        let json = JSON.stringify(params);
        log('----reqest buy ios ',json)
        Native.callWithBack(func,'doIosZF',json);
    }

Native.ts 初始化jsbridge及呼叫如下

public static initBridge() {
        if (this.isAndroidApp()) {
            this.bridge = Laya.PlatformClass.createClass("app.JSBridge");
        }
        if (this.isIOSApp()) {
            this.bridge = Laya.PlatformClass.createClass("JSBridge")
        }
    }

public static callWithBack(callback: Function, methodName: string, ...args: any[]): void {
        if (!this.bridge) { this.initBridge(); }
        if (this.isIOSApp() && args.length == 1) {
            if (args.length == 1) {
                methodName += ":";
            } else {
                log("ios call native error!!!");
            }
        }
        this.bridge.callWithBack(function (msg) { callback(JsonParseSafe(msg)); }, methodName, ...args);
    }

注意:

1、ios的方法要加 : 這個字串, oc端回撥的時候亦是如此,如下

2、這裡的receipt的校驗是在js端,非oc端,oc端僅做內購功能及回撥

OK 這裡我們H5端js呼叫了oc端的   doIosZF  方法,接下來就是oc端去實現

IAPManager的邏輯

桓公本對此一竅不通,如何對接?我們看到JSBridge裡的doIosZF方法裡掉了IAPManager的buy方法,並傳了productid和orderid兩個引數,那麼我們來看下IAPManager的buy方法

-(void)buy:(NSString*)productId orderid:(NSString*) orderid
{
    bool canBuy = [SKPaymentQueue canMakePayments];
    
    if (canBuy) {
        NSLog(@"允許程式內付費購買");
        //直接購買
        //SKPayment *payment = [SKPayment paymentWithProductIdentifier:productId];
        
        SKMutablePayment *payment = [SKMutablePayment paymentWithProductIdentifier:productId];
        payment.applicationUsername = orderid;
        
        NSLog(@"---------傳送購買請求------------");
        [[SKPaymentQueue defaultQueue] addPayment:payment];
    }
    else
    {
        [self.delegate onCallBack:-1 reason:@"You can‘t purchase in app store(沒允許應用程式內購買)" retry:false receipt:@"" orderid:@""];
    }
}

這裡自己去研究,oc程式碼:函式的執行皆是[],其實也很容易懂

IAPManager首先要初始化,初始化方法,這個初始化方法其實在MarketAppStore裡初始化了

- (id)initWithGameID:(NSString*)gameID andDelegate:(id<JCIapProcessCtrlDelegate>)delegate{
    if ((self = [super init])) {
        NSAssert(gameID != nil,@"[IAP] gameID can not be nil");
        NSAssert(gameID.length <= __MAX_GAME_ID_SIZE,@"gameID is too long");
        self.gameAppID = [NSString stringWithString:gameID];
        NSAssert(delegate != nil, @"[IAP] delegate can not be nil");
        self.delegate = delegate;
        self.strPlateform = [NSString stringWithFormat:@"%@,%@,%@", [[UIDevice currentDevice] model], [[UIDevice currentDevice] systemName],[[UIDevice currentDevice] systemVersion]];
        NSLog(@"[IAP]plateform info: %@\n", self.strPlateform);
        [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
    }
    return self;
}

注意 這裡不使用MarketAppStore作為回撥,而是使用另外封裝的SDKManager作為回撥物件代理,因此這裡初始化的程式碼就變成了

-(void)	LP_Init
{
    SDKManager *sdkmgr = [SDKManager getInstance];
    [[IAPManager getInstance] initWithGameID:[conchConfig GetInstance]->m_sGameID andDelegate:sdkmgr];
}

SDKManager.mm類裡新增這個回撥,這裡有個retry不用管,完成後他會去掉JSBridge的iosPayCallBack,這裡就會回撥給js的doIosZF的回撥方法了,同時把引數也傳過去了,注意傳的是json字串,注意解析

- (void)onCallBack:(NSInteger)p_iRet reason:(NSString *)p_pReason retry:(Boolean)bRetry receipt:(NSString *)p_pReceipt orderid:(NSString *)p_pOrderid
{
    if(bRetry)
    {
        NSString* sJs = [NSString stringWithFormat:@"if(window.Laya)window.Laya.Browser.window.onRetryIosZF(%ld,'%@','%@','%@');",(long)p_iRet,p_pReason,p_pReceipt,p_pOrderid];
        [JSBridge callRunJs:[NSString stringWithCString:[sJs UTF8String] encoding:NSUTF8StringEncoding]];
    }
    else
    {
        [JSBridge iosPayCallBack:p_iRet reason:(p_pReason?p_pReason:@"") receipt:(p_pReceipt?p_pReceipt:@"") orderid:(p_pOrderid?p_pOrderid:@"")];
    }
}

然後我們看IAPManager裡內購完成的回撥方法

- (void) completeTransaction: (SKPaymentTransaction *)transaction retry:(Boolean)bRetry
{
    NSLog(@"-----completeTransaction--------");
    NSString* pszBase64 = [transaction.transactionReceipt base64Encoding];
    [self.delegate onCallBack:0 reason:@"購買成功!" retry:bRetry receipt:pszBase64 orderid:transaction.payment.applicationUsername];
}

- (void) failedTransaction: (SKPaymentTransaction *)transaction retry:(Boolean)bRetry
{
    NSLog(@"失敗,%ld,%@",(long)transaction.error.code,[transaction.error localizedDescription]);
    
    [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
    //使用者取消
    if (transaction.error.code == SKErrorPaymentCancelled)
        [self.delegate onCallBack:-1 reason:@"您取消支付!" retry:bRetry receipt:@"" orderid:@""];
    else
        [self.delegate onCallBack:-1 reason:[transaction.error localizedDescription] retry:bRetry receipt:@"" orderid:@""];
}

都會有這個代理方法  self.delegate onCallBack:  這個就會回撥到SDKManager的onCallBack方法

注意:productid必須是與app內購裡配置的id一致,否則找不到

而且重中之重的是:productid  一定是字串,絕不可傳number型,否則你會看到請求沒任何反應,也沒報錯,這時候就要用到開頭說的控制檯了,這個讓我查了足足三天之多,可見陰陽變化莫測,雖極微之道,亦可傾天蓋地也。

這裡奉上IAPManger 原始碼,望世間無如吾之愚人

#import "IAPManager.h"
#define __MAX_GAME_ID_SIZE     32

@interface IAPManager()<NSURLConnectionDataDelegate>

@property (nonatomic,assign) id<JCIapProcessCtrlDelegate> delegate;
@property (nonatomic,strong) NSString* gameAppID;
@property (nonatomic,strong) NSString *strPlateform;
@end

@implementation IAPManager

+ (IAPManager*) getInstance{
    static IAPManager* iap = nil;
    if (iap == nil){
        iap = [IAPManager alloc];
    }
    return iap;
}

- (id)initWithGameID:(NSString*)gameID andDelegate:(id<JCIapProcessCtrlDelegate>)delegate{
    if ((self = [super init])) {
        NSAssert(gameID != nil,@"[IAP] gameID can not be nil");
        NSAssert(gameID.length <= __MAX_GAME_ID_SIZE,@"gameID is too long");
        self.gameAppID = [NSString stringWithString:gameID];
        NSAssert(delegate != nil, @"[IAP] delegate can not be nil");
        self.delegate = delegate;
        self.strPlateform = [NSString stringWithFormat:@"%@,%@,%@", [[UIDevice currentDevice] model], [[UIDevice currentDevice] systemName],[[UIDevice currentDevice] systemVersion]];
        NSLog(@"[IAP]plateform info: %@\n", self.strPlateform);
        [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
        
        // [self.delegate onCallBack:1 reason:@"" retry:true receipt:@"" orderid:@""];
    }
    return self;
}

- (void)dealloc
{
    [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}

-(void)buy:(NSString*)productId orderid:(NSString*) orderid
{
    bool canBuy = [SKPaymentQueue canMakePayments];
    
    if (canBuy) {
        NSLog(@"允許程式內付費購買");
        //直接購買
        //SKPayment *payment = [SKPayment paymentWithProductIdentifier:productId];
        
        SKMutablePayment *payment = [SKMutablePayment paymentWithProductIdentifier:productId];
        payment.applicationUsername = orderid;
        
        NSLog(@"---------傳送購買請求------------");
        [[SKPaymentQueue defaultQueue] addPayment:payment];
    }
    else
    {
        [self.delegate onCallBack:-1 reason:@"You can‘t purchase in app store(沒允許應用程式內購買)" retry:false receipt:@"" orderid:@""];
    }
}

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions//交易結果
{
    //完成購買
    NSLog(@"-----paymentQueue--------");
    for (SKPaymentTransaction *transaction in transactions)
    {
        switch (transaction.transactionState)
        {
            case SKPaymentTransactionStatePurchased://交易完成
                [self completeTransaction:transaction retry:false];
                NSLog(@"-----交易完成 --------");
                break;
            case SKPaymentTransactionStateFailed://交易失敗
                [self failedTransaction:transaction retry:false];
                break;
            case SKPaymentTransactionStateRestored://已經購買過該商品
                //[self restoreTransaction:transaction];
                //NSLog(@"-----已經購買過該商品 --------");
                break;
            case SKPaymentTransactionStatePurchasing:      //商品新增進列表
                NSLog(@"-----商品新增進列表 --------");
                break;
            default:
                break;
        }
    }
}

- (void) completeTransaction: (SKPaymentTransaction *)transaction retry:(Boolean)bRetry
{
    NSLog(@"-----completeTransaction--------");
    NSString* pszBase64 = [transaction.transactionReceipt base64Encoding];
    [self.delegate onCallBack:0 reason:@"購買成功!" retry:bRetry receipt:pszBase64 orderid:transaction.payment.applicationUsername];
}

- (void) failedTransaction: (SKPaymentTransaction *)transaction retry:(Boolean)bRetry
{
    NSLog(@"失敗,%ld,%@",(long)transaction.error.code,[transaction.error localizedDescription]);
    
    [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
    //使用者取消
    if (transaction.error.code == SKErrorPaymentCancelled)
        [self.delegate onCallBack:-1 reason:@"您取消支付!" retry:bRetry receipt:@"" orderid:@""];
    else
        [self.delegate onCallBack:-1 reason:[transaction.error localizedDescription] retry:bRetry receipt:@"" orderid:@""];
}

-(void)retryProvideProduct
{
    bool bTraslate = false;
    NSLog(@"-----paymentQueue--------");
    
    for (SKPaymentTransaction *transaction in [[SKPaymentQueue defaultQueue]transactions])
    {
        switch (transaction.transactionState)
        {
            case SKPaymentTransactionStatePurchased://交易完成
            {
                [self completeTransaction:transaction retry:true];
                bTraslate = true;
                NSLog(@"-----交易完成 --------");
            }
                break;
            case SKPaymentTransactionStateFailed://交易失敗
            {
                [self failedTransaction:transaction retry:true];
                bTraslate = true;
            }
                break;
            default:
                break;
        }
    }
    if(!bTraslate){
        [self.delegate onCallBack:1 reason:@"" retry:true receipt:@"" orderid:@""];
    }
}

-(void)finishTransaction
{
    NSLog(@"-----finishTransaction --------");
    for (SKPaymentTransaction *transaction in [[SKPaymentQueue defaultQueue]transactions])
    {
        [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
    }
}

@end

至此ios內購可謂告一段落,對ios專業開發人員來說,似乎是鳥之距媲美鵬之高也,然

天下難事,必作於易;天下大事,必作於細 

望樓頂之人不小泥潭之蛙,此吾生之願也!