【Unity】ios平臺IAP內購和沙箱測試流程詳解(開發中遇到的坑)

Unity iOS內購

內購流程

  • 1、在 AppStore 中創建相應的物品,創建內購沙盒測試賬號
  • 2、客戶端從後臺獲取相應的物品 ID (當然也可以再客戶端寫死,但後期擴展性就受限制了)
  • 3、依據相應的物品 ID 請求商品的相關信息
  • 4、依據商品信息創建訂單請求交易
  • 5、依據返回的訂單狀態處理交易結果
  • 6、請求後臺再次驗證訂單狀態
  • 7、依據後臺返回結果處理相關邏輯

2、創建內購物品以及沙盒測試賬號


思路:

Unity調用iOS內購代碼實現

效果圖:

購買彈框
購買結果

重要提示:

測試一定要用沙盒賬號,否則無效!

流程

這裏就不重複寫了,直接上截圖 
這裏寫圖片描述

OC代碼:

IAPInterface(主要是實現Unity跟OC的IAP代碼的一個交互作用,等於是一箇中間橋樑)

#import <Foundation/Foundation.h>

@interface IAPInterface : NSObject

@end
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5
#import "IAPInterface.h"
#import "IAPManager.h"

@implementation IAPInterface

void TestMsg(){
    NSLog(@"Msg received");

}

void TestSendString(void *p){
    NSString *list = [NSString stringWithUTF8String:p];
    NSArray *listItems = [list componentsSeparatedByString:@"\t"];

    for (int i =0; i<listItems.count; i++) {
        NSLog(@"msg %d : %@",i,listItems[i]);
    }

}

void TestGetString(){
    NSArray *test = [NSArray arrayWithObjects:@"t1",@"t2",@"t3", nil];
    NSString *join = [test componentsJoinedByString:@"\n"];


    UnitySendMessage("Main", "IOSToU", [join UTF8String]);
}

IAPManager *iapManager = nil;

void InitIAPManager(){
    iapManager = [[IAPManager alloc] init];
    [iapManager attachObserver];

}

bool IsProductAvailable(){
    return [iapManager CanMakePayment];
}

void RequstProductInfo(void *p){
    NSString *list = [NSString stringWithUTF8String:p];
    NSLog(@"productKey:%@",list);
    [iapManager requestProductData:list];
}

void BuyProduct(void *p){
    [iapManager buyRequest:[NSString stringWithUTF8String:p]];
}

@end
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51

IAPManager(真真的iOS的購買功能)

#import <Foundation/Foundation.h>
#import <StoreKit/StoreKit.h>

@interface IAPManager : NSObject<SKProductsRequestDelegate, SKPaymentTransactionObserver>{
    SKProduct *proUpgradeProduct;
    SKProductsRequest *productsRequest;
}

-(void)attachObserver;
-(BOOL)CanMakePayment;
-(void)requestProductData:(NSString *)productIdentifiers;
-(void)buyRequest:(NSString *)productIdentifier;

@end
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
#import "IAPManager.h"

@implementation IAPManager

-(void) attachObserver{
    NSLog(@"AttachObserver");
    [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}

-(BOOL) CanMakePayment{
    return [SKPaymentQueue canMakePayments];
}

-(void) requestProductData:(NSString *)productIdentifiers{
    NSArray *idArray = [productIdentifiers componentsSeparatedByString:@"\t"];
    NSSet *idSet = [NSSet setWithArray:idArray];
    [self sendRequest:idSet];
}

-(void)sendRequest:(NSSet *)idSet{
    SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:idSet];
    request.delegate = self;
    [request start];
}

-(void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
    NSArray *products = response.products;

    for (SKProduct *p in products) {
        UnitySendMessage("Main", "ShowProductList", [[self productInfo:p] UTF8String]);
    }

    for(NSString *invalidProductId in response.invalidProductIdentifiers){
        NSLog(@"Invalid product id:%@",invalidProductId);
    }

    [request autorelease];
}

-(void)buyRequest:(NSString *)productIdentifier{
    SKPayment *payment = [SKPayment paymentWithProductIdentifier:productIdentifier];
    [[SKPaymentQueue defaultQueue] addPayment:payment];
}

-(NSString *)productInfo:(SKProduct *)product{
    NSArray *info = [NSArray arrayWithObjects:product.localizedTitle,product.localizedDescription,product.price,product.productIdentifier, nil];

    return [info componentsJoinedByString:@"\t"];
}

-(NSString *)transactionInfo:(SKPaymentTransaction *)transaction{

    return [self encode:(uint8_t *)transaction.transactionReceipt.bytes length:transaction.transactionReceipt.length];

    //return [[NSString alloc] initWithData:transaction.transactionReceipt encoding:NSASCIIStringEncoding];
}

-(NSString *)encode:(const uint8_t *)input length:(NSInteger) length{
    static char table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";

    NSMutableData *data = [NSMutableData dataWithLength:((length+2)/3)*4];
    uint8_t *output = (uint8_t *)data.mutableBytes;

    for(NSInteger i=0; i<length; i+=3){
        NSInteger value = 0;
        for (NSInteger j= i; j<(i+3); j++) {
            value<<=8;

            if(j<length){
                value |=(0xff & input[j]);
            }
        }

        NSInteger index = (i/3)*4;
        output[index + 0] = table[(value>>18) & 0x3f];
        output[index + 1] = table[(value>>12) & 0x3f];
        output[index + 2] = (i+1)<length ? table[(value>>6) & 0x3f] : '=';
        output[index + 3] = (i+2)<length ? table[(value>>0) & 0x3f] : '=';
    }

    return [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];
}

-(void) provideContent:(SKPaymentTransaction *)transaction{
    UnitySendMessage("Main", "ProvideContent", [[self transactionInfo:transaction] UTF8String]);
}

-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions{
    for (SKPaymentTransaction *transaction in transactions) {
        switch (transaction.transactionState) {
            case SKPaymentTransactionStatePurchased:
                [self completeTransaction:transaction];
                break;
            case SKPaymentTransactionStateFailed:
                [self failedTransaction:transaction];
                break;
            case SKPaymentTransactionStateRestored:
                [self restoreTransaction:transaction];
                break;
            default:
                break;
        }
    }
}

-(void) completeTransaction:(SKPaymentTransaction *)transaction{
    NSLog(@"Comblete transaction : %@",transaction.transactionIdentifier);
    [self provideContent:transaction];
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}

-(void) failedTransaction:(SKPaymentTransaction *)transaction{
    NSLog(@"Failed transaction : %@",transaction.transactionIdentifier);

    if (transaction.error.code != SKErrorPaymentCancelled) {
        NSLog(@"!Cancelled");
    }
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}

-(void) restoreTransaction:(SKPaymentTransaction *)transaction{
    NSLog(@"Restore transaction : %@",transaction.transactionIdentifier);
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}


@end
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128

Unity中調用的C#代碼

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;

public class IAPExample : MonoBehaviour {

    public List<string> productInfo = new List<string>();

    [DllImport("__Internal")]
    private static extern void TestMsg();//測試信息發送

    [DllImport("__Internal")]
    private static extern void TestSendString(string s);//測試發送字符串

    [DllImport("__Internal")]
    private static extern void TestGetString();//測試接收字符串

    [DllImport("__Internal")]
    private static extern void InitIAPManager();//初始化

    [DllImport("__Internal")]
    private static extern bool IsProductAvailable();//判斷是否可以購買

    [DllImport("__Internal")]
    private static extern void RequstProductInfo(string s);//獲取商品信息

    [DllImport("__Internal")]
    private static extern void BuyProduct(string s);//購買商品

    //測試從xcode接收到的字符串
    void IOSToU(string s){
        Debug.Log ("[MsgFrom ios]"+s);
    }

    //獲取product列表
    void ShowProductList(string s){
        productInfo.Add (s);
    }
    bool back = false;
    //獲取商品回執
    void ProvideContent(string s){
        Debug.Log ("[MsgFrom ios]proivideContent : "+s);
        back = true;
    }


    // Use this for initialization
    void Start () {
        InitIAPManager();
    }

    void OnGUI(){

        if(Btn ("GetProducts")){
            if(!IsProductAvailable())
                throw new System.Exception("IAP not enabled");
            productInfo = new List<string>();
            RequstProductInfo("com.aladdin.fishpocker1\tcom.aladdin.fishpocker2");
        }

        GUILayout.Space(40);

        if (back)
            GUI.Label (new Rect (10, 150, 100, 100), "Message back");

        for(int i=0; i<productInfo.Count; i++){
            if(GUILayout.Button (productInfo[i],GUILayout.Height (100), GUILayout.MinWidth (200))){
                string[] cell = productInfo[i].Split('\t');
                Debug.Log ("[Buy]"+cell[cell.Length-1]);
                BuyProduct(cell[cell.Length-1]);
                GUI.Label(new Rect (10, 10, 100, 200), string.Format("[Buy]{0}" ,cell[cell.Length-1]));
            }  
        }
    }

    bool Btn(string msg){
        GUILayout.Space (100);
        return  GUILayout.Button (msg,GUILayout.Width (200),GUILayout.Height(100));
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82

Git地址點擊下載

在這裏需要注意幾點,

  1. 代碼中的_currentProId所填寫的是你的購買項目的的ID,這個和第二步創建的內購的productID要一致;本例中是 123。

  2. 在監聽購買結果後,一定要調用[[SKPaymentQueue defaultQueue] finishTransaction:tran];來允許你從支付隊列中移除交易。

  3. 沙盒環境測試appStore內購流程的時候,請使用沒越獄的設備。

  4. 請務必使用真機來測試,一切以真機爲準。

  5. 項目的Bundle identifier需要與您申請AppID時填寫的bundleID一致,不然會無法請求到商品信息。

  6. 真機測試的時候,一定要退出原來的賬號,才能用沙盒測試賬號

  7. 二次驗證,請注意區分宏, 測試用沙盒驗證,App Store審覈的時候也使用的是沙盒購買,所以驗證購買憑證的時候需要判斷返回Status Code決定是否去沙盒進行二次驗證,爲了線上用戶的使用,驗證的順序肯定是先驗證正式環境,此時若返回值爲21007,就需要去沙盒二次驗證,因爲此購買的是在沙盒進行的。

附:蘋果支付錯誤目錄


如果直接使用上面的代碼需要注意的是場景中這個腳本掛載的對象名必須爲Main,當然你也可以在IAPManager中修改Main爲其他你要掛載的對象名
IAPManager中有個UnitySendMessage("Main",。。。。)方法負責接收ios的回執函數並在Unity中回執調用。

沙盒測試內購功能須先退出iTunes store的id然後直接登錄遊戲 在遊戲內登錄申請的沙盒賬號,然後測試內購功能(審覈顯示等待APP提交狀態中的內購也可以使用沙箱測試)

沙箱賬號請求驗證的蘋果服務器地址和正常發佈購買的地址不同見下文

iOS 內購驗證

如果我們不做任何處理的話,越獄機是可以直接繞過支付驗證直接獲得結果的,這樣對於我們辛辛苦苦的開發者來說簡直噩耗,所以我們有必要了解一下內購驗證相關的知識,以及知道如何去預防這樣的事情。

校驗文章

http://www.cnblogs.com/zhaoqingqing/p/4597794.html

相關資料

本地驗證:

優點:

  • 無需服務器驗證

缺點:

  • 項目裏需要引入 OpenSSL

鏈接:

服務器驗證:

優點:

  • server-side verification over SSL is the most reliable way to determine the authenticity of purchasing records

缺點:

  • 需要部署服務器,服務器和 App 之間的數據交換可能更容易被破解

鏈接:

雙重驗證:

先本地驗證一次,後服務器再驗證一次(感覺沒必要)

其他:

常見的破解方法:

總的來說:

  • 服務器驗證更適合有自己賬號系統的 App,直接可以對 IAP 破解免疫,否則一樣很簡單就被破解
  • 本地驗證使用下面的方法來增強驗證 
    • Check that the SSL certificate used to connect to the App Store server is an EV certificate.
    • Check that the information returned from validation matches the information in the SKPayment object.
    • Check that the receipt has a valid signature.
    • Check that new transactions have a unique transaction ID.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章