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地址點擊下載
在這裏需要注意幾點,
-
代碼中的_currentProId所填寫的是你的購買項目的的ID,這個和第二步創建的內購的productID要一致;本例中是 123。
-
在監聽購買結果後,一定要調用[[SKPaymentQueue defaultQueue] finishTransaction:tran];來允許你從支付隊列中移除交易。
-
沙盒環境測試appStore內購流程的時候,請使用沒越獄的設備。
-
請務必使用真機來測試,一切以真機爲準。
-
項目的Bundle identifier需要與您申請AppID時填寫的bundleID一致,不然會無法請求到商品信息。
-
真機測試的時候,一定要退出原來的賬號,才能用沙盒測試賬號
-
二次驗證,請注意區分宏, 測試用沙盒驗證,App Store審覈的時候也使用的是沙盒購買,所以驗證購買憑證的時候需要判斷返回Status Code決定是否去沙盒進行二次驗證,爲了線上用戶的使用,驗證的順序肯定是先驗證正式環境,此時若返回值爲21007,就需要去沙盒二次驗證,因爲此購買的是在沙盒進行的。
附:蘋果支付錯誤目錄
iOS 內購驗證
如果我們不做任何處理的話,越獄機是可以直接繞過支付驗證直接獲得結果的,這樣對於我們辛辛苦苦的開發者來說簡直噩耗,所以我們有必要了解一下內購驗證相關的知識,以及知道如何去預防這樣的事情。
校驗文章
http://www.cnblogs.com/zhaoqingqing/p/4597794.html
相關資料
- https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html#//apple_ref/doc/uid/TP40010573-CH106-SW1
- http://hpique.github.io/RMStore-presentation-for-NSBarcelona
- http://asciiwwdc.com/2014/sessions/305
本地驗證:
優點:
- 無需服務器驗證
缺點:
- 項目裏需要引入 OpenSSL
鏈接:
- http://stackoverflow.com/a/20039394/656428
- https://github.com/robotmedia/RMStore#receipt-verification
- https://github.com/robotmedia/RMStore/wiki/Receipt-verification
服務器驗證:
優點:
- server-side verification over SSL is the most reliable way to determine the authenticity of purchasing records
缺點:
- 需要部署服務器,服務器和 App 之間的數據交換可能更容易被破解
鏈接:
雙重驗證:
先本地驗證一次,後服務器再驗證一次(感覺沒必要)
其他:
- http://receigen.etiemble.com Mac App,直接生成代碼,Xcode 集成
常見的破解方法:
- http://blog.hussulinux.com/2013/04/apple-ios-in-app-purchase-hacking-how-to-prevent-specially-com-zeptolab-ctrbonus-superpower1-hacks/
- http://stackoverflow.com/a/17687827/656428
總的來說:
- 服務器驗證更適合有自己賬號系統的 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.