avatar

Unity 调用iOS原生实现内购

引言

前面我写了一篇文章:AppStore上架:未使用iOS原生实现内购,说明了为什么最好使用原生代码实现内购,所以本篇便是介绍如何代码实现!我在代码中做了漏单处理,我们需要尽早的监听付款队列,并且要等后端验证完成后才可以结束这笔交易。因为在app启动后,系统会检查是否有未完成的交易,或者新的续订,如果有的话会加入交易队列。所以如果我们在app启动后没有及时添加监听或者监听后没有处理未完成的交易,就会造成漏单。如果我们在后端没有完成验证的情况下结束了正在进行的交易,但是不巧的是后端并没有收到你的请求,同样会造成漏单。

具体步骤

在XCode工程中添加内购代码
1.添加IAPManager.h文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#import < Foundation/Foundation.h >
#import < StoreKit/StoreKit.h >

@interface IAPManager : NSObject<SKProductsRequestDelegate, SKPaymentTransactionObserver>{
SKProduct *proUpgradeProduct;
SKProductsRequest *productsRequest;
NSString *productIndentify;
}
-(void)attachObserver; // 监听付款队列
-(BOOL)CanMakePayment; // 判断是否可以付款
-(void)requestProductData:(NSString *)productIdentifiers; // 获取这个ID的商品信息
-(void)buyRequest:(NSString *)productIdentifier; // 请求购买这个ID的商品
-(void)InitWatting; // 初始化等待遮罩
-(void)gameServerSendProductEnd:(NSString *)transactionID; // 后端验证购买完成后的回调

@end

2.添加IAPManager.m

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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
#import "IAPManager.h"

// 弹窗和等待遮罩
@implementation IAPManager
{
UIView *_viewBg;
UIView *_viewBorder;
UIActivityIndicatorView * _activityIndicator;
UIWindow *_window;
}

-(void) 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;
// populate UI
for (SKProduct *p in products) {
// 将获取到的商品信息回传给Unity
UnitySendMessage("IOSIAPMgr", "ShowProductList", [[self productInfo:p] UTF8String]);
}
}

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

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

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

//创建一个空字典,保存正在进行的交易
NSMutableArray*transArray;

/**
* 验证购买,避免非正常购买问题
*
*/
-(void)verifyPurchaseWithPaymentTransaction:(SKPaymentTransaction*)tran{

NSURL *receiptUrl=[[NSBundle mainBundle] appStoreReceiptURL];
NSData *receiptData=[NSData dataWithContentsOfURL:receiptUrl];

NSString *receiptString=[receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];//转化为base64字符串

//除去receiptdata中的特殊字符
NSString *receipt1=[receiptString stringByReplacingOccurrencesOfString:@"\\n" withString:@""];
NSString *receipt2=[receipt1 stringByReplacingOccurrencesOfString:@"\\r" withString:@""];
NSString *receipt3=[receipt2 stringByReplacingOccurrencesOfString:@"%2B" withString:@"+"];

NSMutableDictionary *mdic =[NSMutableDictionary dictionaryWithObject:tran.transactionIdentifier forKey:@"TransactionID"];
[mdic setObject:receipt3 forKey:@"Payload"];
NSData *data=[NSJSONSerialization dataWithJSONObject:mdic options:NSJSONWritingPrettyPrinted error:nil];
NSString *str=[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];

// 保存正在进行的交易
if (transArray==nil) {
transArray=[NSMutableArray array];
}
[transArray addObject:tran];
// 通知Unity购买成功,将数据交由后端去苹果后台进行验证
UnitySendMessage("IOSIAPMgr", "BuyProductSucessCallBack", str.UTF8String);
}
//交易结果返回,根据状态进行不同的处理
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transaction{
for(SKPaymentTransaction *tran in transaction){
switch (tran.transactionState) {
case SKPaymentTransactionStatePurchased:{
// 进行验证
[self verifyPurchaseWithPaymentTransaction:tran];
}
break;
case SKPaymentTransactionStatePurchasing:{
[self showWatting];
}
break;
case SKPaymentTransactionStateRestored:{
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
[self hideWatting];
}
break;
case SKPaymentTransactionStateFailed:{
// 弹窗提示交易失败
UIAlertController *alertview=[UIAlertController alertControllerWithTitle:@"交易提示" message:@"交易失败" preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *defult = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil];
[alertview addAction:defult];
if (_window==nil) {
_window = [UIApplication sharedApplication].keyWindow;
}
[_window.rootViewController presentViewController:alertview animated:YES completion:nil];
// 结束这笔交易
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
[self hideWatting];
}
break;
default:
{
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
[self showWatting];
}
break;
}
}
}

-(void)gameServerSendProductEnd:(NSString *)transactionID;{
//订单数组为空或者没有,返回
if (transArray==nil||transArray.count==0) {
return;
}
//遍历订单数组,将服务端处理完成的订单移除
for (int i=0; i<[transArray count]; i++) {
SKPaymentTransaction*tran = transArray[i];
if ([tran.transactionIdentifier isEqualToString:transactionID ]) {
[transArray removeObject:tran];
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
}
}
//如果订单数组的元素个数为0.证明当前订单已全部处理完成,关闭等待UI
if (transArray.count==0) {
[self hideWatting];
}
}

-(void)showWatting{

[_activityIndicator startAnimating];
[_window addSubview:_viewBg];

}

-(void)hideWatting{

[_activityIndicator stopAnimating];
[_viewBg removeFromSuperview];

}

-(void)InitWatting{

_window = [UIApplication sharedApplication].keyWindow;

_viewBg = [[UIView alloc ] initWithFrame:CGRectMake(0,0, UIScreen.mainScreen.bounds.size.width,UIScreen.mainScreen.bounds.size.height)];
_viewBg.backgroundColor=[UIColor colorWithRed:0 green:0 blue:0 alpha:0.01];


_viewBorder = [[UIView alloc ] initWithFrame:CGRectMake(0,0,100,100)];
[_viewBg addSubview:_viewBorder];

_viewBorder.center= CGPointMake(_viewBg.frame.size.width/2.0 ,_viewBg.frame.size.height/2.0);
_viewBorder.backgroundColor =[UIColor colorWithRed:0 green:0 blue:0 alpha:0.9];
_viewBorder.layer.cornerRadius=8;
_viewBorder.layer.masksToBounds=true;

_activityIndicator = [[UIActivityIndicatorView alloc]initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
[_viewBorder addSubview:_activityIndicator];

_activityIndicator.frame= CGRectMake(0,0,200,200);
_activityIndicator.center= CGPointMake(_viewBorder.frame.size.width/2.0 ,_viewBorder.frame.size.height/2.0);
_activityIndicator.hidesWhenStopped = NO;
}

@end

在XCode中添加和Unity交互的代码
1.IAPInterface.h

1
2
3
4
5
#import < Foundation/Foundation.h >

@interface IAPInterface : NSObject

@end

2.IAPInterface.m

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
#import "IAPInterface.h"
#import "IAPManager.h"

@implementation IAPInterface
#if defined (__cplusplus)
extern "C"
{
#endif
IAPManager *iapManager = nil;
//初始化
void InitIAPManager(){
iapManager = [[IAPManager alloc] init];
[iapManager attachObserver];
[iapManager InitWatting];
}
//判断是否可以购买
bool IsProductAvailable(){
return [iapManager CanMakePayment];
}
//获取商品信息
void RequstProductInfo(char *p){
NSString *list = [NSString stringWithUTF8String:p];
[iapManager requestProductData:list];
}
//购买商品
void BuyProduct(char *p){
[iapManager buyRequest:[NSString stringWithUTF8String:p]];
}
//游戏服务端发货完成移除订单
void GameServerSendProductEnd(char *p){
return [iapManager gameServerSendProductEnd:[NSString stringWithUTF8String:p]];
}
#if defined (__cplusplus)
}
#endif
@end

在Unity中添加和iOS交互的代码

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
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;

public class IOSIAPMgr : MonoBehaviour
{
#if UNITY_IOS
private List<string> productInfo = new List<string>();

private static IOSIAPMgr _instance;

public static IOSIAPMgr Instance{
get{
if(_instance==null)
{
GameObject go=new GameObject("IOSIAPMgr");
DontDestroyOnLoad (go);
_instance=go.AddComponent<IOSIAPMgr>();

}
return _instance;
}
}

void Awake()
{
if(_instance==null){
_instance = this;
}
_instance.Init();
}

[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);//购买商品

[DllImport("__Internal")]
private static extern void GameServerSendProductEnd(string transactionID);//游戏后端验证完成回调

//展示商品信息
void ShowProductList(string s){
productInfo.Add(s);
}

private void Init ()
{
if (Application.platform==RuntimePlatform.IPhonePlayer) {
InitIAPManager();
}
}

public bool IsProductVailable()
{
return IsProductAvailable();
}

//根据商品ID请求商品信息
public void RequstALLProductInfo()
{
if(IsProductAvailable())
{
RequstProductInfo("your ProductID");
}
}

public void IAPBuyProduct(string str)
{
if (IsProductAvailable())
{
BuyProduct(str);
}

}

// IOS 购买商品成功的回调
public void BuyProductSucessCallBack(string args)
{
IOSIAPJson json =LitJson.JsonMapper.ToObject<IOSIAPJson>(args);
ProductsArray.Add(json);
SendProductsToServer();
}

private List<IOSIAPJson> ProductsArray=new List<IOSIAPJson>();

public void SendProductsToServer() {
if (cor!=null) {
StopCoroutine(cor);
}
cor = StartCoroutine(ieSendProductsToServer());
}

private Coroutine cor;

//开启协程。每过一分钟检查一次是否有未完成的订单
private IEnumerator ieSendProductsToServer() {
while (ProductsArray != null&& ProductsArray.Count>0) {
foreach (var product in ProductsArray) {
string json = LitJson.JsonMapper.ToJson(product);
FYMJ.Guide.Service.GuideServiceRequest.Instance().FangKaBack(json);
}
yield return new WaitForSeconds(60f);
}
}

// 后端验证完成,返回交易的ID
public void ServerCallBack(AnyString transactionID) {
if (!string.IsNullOrEmpty(transactionID)) {
for (int i = 0; i < ProductsArray.Count; i++) {
IOSIAPJson json = ProductsArray[i];
if (!string.IsNullOrEmpty(json.TransactionID)&&json.TransactionID==transactionID) {
ProductsArray.RemoveAt(i);
i--;
GameServerSendProductEnd(transactionID);
}
}
}
}
#endif
}

备注

  1. 等待遮罩是为了玩家频繁下单,可以按需修改
  2. 请求商品信息部分我没有用到,看上去没什么用
  3. 协程循环发送未完成的交易,可以删除价值不大
  4. 上架前和上架后,拉起的支付页面长得不太一样

文章内容参考了博客园-墙外的世界,不用于商业用途,如有侵权请联系,我将尽快删除。

文章作者: tiger
文章链接: https://chenghu.online/posts/fb902642/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 tiger
打赏
  • 微信
    微信
  • 支付宝
    支付宝

评论