cocos creator 版本3.3
以非消耗型为例:非消耗型和订阅类型,存在app升级或者重装后恢复内购的需求
1、配置产品ID
在对应项目中找到-》管理点击+
非消耗型可以调用恢复接口恢复:
- 消耗品(Consumable products):比如游戏内金币等。
- 非消耗品(Non-consumable products):简单来说就是一次购买,终身可用(用户可随时从App Store restore)。
- 自动更新订阅品(Auto-renewable subscriptions):和不可消耗品的不同点是有失效时间。比如一整年的付费周刊。在这种模式下,开发者定期投递内容,用户在订阅期内随时可以访问这些内容。订阅快要过期时,系统将自动更新订阅(如果用户同意)。
- 非自动更新订阅品(Non-renewable subscriptions):一般使用场景是从用户从IAP购买后,购买信息存放在自己的开发者服务器上。失效日期/可用是由开发者服务器自行控制的,而非由App Store控制,这一点与自动更新订阅品有差异。
- 免费订阅品(Free subscriptions):在Newsstand中放置免费订阅的一种方式。免费订阅永不过期。只能用于Newsstand-enabled apps。
填写好相对于的所有信息,其中产品id,就是传递的接口
元数据丢失状态是内容没填全,可能会影响测试
2、Xcode代码调用(如何发布ios可参考前面文章)
a、添加内购的Lib库:StoreKit.framework
b、Xcode接口,供cocos调用及回掉cocos
IAPInterface.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface IAPInterface : NSObject
+ (instancetype)sharedSingleton;
+ (void) InitIAPManager;
+ (bool) IsProductAvailable;
+ (void) RequstProductInfo :(NSString *)p;
+ (void) BuyProduct :(NSString *)p;
+ (void) RestoreBuyProduct :(NSString *)p;
- (void) BuyProcuctSucessCallBack:(NSString *)str;
- (void) BuyProcuctFailedCallBack:(NSString *)str;
- (void) ShowProductList:(NSString *)str;
- (void) RestoreBuyProductSucessCallBack:(NSString *)str;
- (void) test1;
@end
NS_ASSUME_NONNULL_END
IAPInterface.mm
#import "IAPInterface.h"
#import "IAPManager.h"
#import "cocos/bindings/jswrapper/SeApi.h"
@interface IAPInterface()
@property IAPManager *iapManager;
@end
@implementation IAPInterface
+ (instancetype)sharedSingleton {
static IAPInterface *_sharedSingleton = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//不能再使用alloc方法
//因为已经重写了allocWithZone方法,所以这里要调用父类的分配空间的方法
if (_sharedSingleton==nil) {
_sharedSingleton = [[super allocWithZone:NULL] init];
}
//[_sharedSingleton initDelegate];
//[_sharedSingleton checkTransaction];
});
return _sharedSingleton;
}
/**
//初始化商品信息
*/
+ (void) InitIAPManager{
NSLog(@"InitIAPManagerUser");
[[IAPInterface sharedSingleton] InitIAPManagerUser];
}
- (void) InitIAPManagerUser{
self.iapManager=[[IAPManager alloc] init];
[self.iapManager attachObserver];
}
//判断用户是否可以付费
+ (bool) IsProductAvailable{
return [[IAPInterface sharedSingleton] IsProductAvailableUser];
}
- (bool) IsProductAvailableUser{
return [self.iapManager CanMakePayment];
}
//获取商品信息
+ (void) RequstProductInfo:(NSString *)p{
NSLog(@"商品列表:%@",p);
[[IAPInterface sharedSingleton] RequstProductInfoUser:p];
}
- (void) RequstProductInfoUser:(NSString *)p{
[self.iapManager requestProductData:p];
}
//购买商品
+ (void) BuyProduct:(NSString *)p{
NSLog(@"购买:%@",p);
[[IAPInterface sharedSingleton] BuyProductUser:p];
//[IAPInterface RestoreBuyProduct:p];
}
- (void) BuyProductUser:(NSString *)p{
[self.iapManager buyRequest:p];
}
//恢复购买商品
+ (void) RestoreBuyProduct:(NSString *)p{
NSLog(@"恢复购买:%@",p);
[[IAPInterface sharedSingleton] RestoreBuyProductUser:p];
}
- (void) RestoreBuyProductUser:(NSString *)p{
[self.iapManager Restore:p];
}
/// <summary>
/// 购买商品成功的回调
/// </summary>
/// <param name="str">String.</param>
- (void) BuyProcuctSucessCallBack:(NSString *)str
{
NSLog(@"-----ObjectToJs----BuyProcuctSucessCallBack-------");
NSString *bodyString = [NSString stringWithFormat:@"window.PlatformApi.BuyProcuctSucessCallBack(%@)", str];//拼接请求数据
NSLog(bodyString);
std::string param1=[bodyString UTF8String];
//通过evalString执行上面的代码
se::ScriptEngine::getInstance()->evalString(param1.c_str());
}
/// <summary>
/// 购买商品失败的回调
/// </summary>
/// <param name="str">String.</param>
- (void) BuyProcuctFailedCallBack:(NSString *)str
{
NSLog(@"-----ObjectToJs----BuyProcuctFailedCallBack-------");
NSString *bodyString = [NSString stringWithFormat:@"window.PlatformApi.BuyProcuctFailedCallBack(%@)", str];//拼接请求数据
NSLog(bodyString);
std::string param1=[bodyString UTF8String];
//通过evalString执行上面的代码
se::ScriptEngine::getInstance()->evalString(param1.c_str());
}
/// <summary>
/// 产品反馈信息回掉-
/// </summary>
/// <param name="str">String.</param>
- (void) ShowProductList:(NSString *)str
{
NSLog(@"-----ObjectToJs----ShowProductList-------");
NSString *bodyString = [NSString stringWithFormat:@"window.PlatformApi.ShowProductList(%@)", str];//拼接请求数据
std::string param1=[bodyString UTF8String];
//通过evalString执行上面的代码
se::ScriptEngine::getInstance()->evalString(param1.c_str());
}
/// <summary>
/// 恢复购买的商品完成的回调
/// </summary>
/// <param name="str">String.</param>
- (void) RestoreBuyProductSucessCallBack:(NSString *)strId
{
NSLog(@"-----ObjectToJs----RestoreBuyProductSucessCallBack-------");
NSString *bodyString = [NSString stringWithFormat:@"window.PlatformApi.RestoreBuyProductSucessCallBack(%@)", strId];
//NSLog(bodyString);
std::string param1=[bodyString UTF8String];
//通过evalString执行上面的代码
se::ScriptEngine::getInstance()->evalString(param1.c_str());
}
- (void) test1
{
NSLog(@"-----ObjectToJs----test1-------");
NSString *bodyString = [NSString stringWithFormat:@"window.PlatformApi.showRewardVideoADCallback()"];//拼接请求数据
std::string param1=[bodyString UTF8String];
//通过evalString执行上面的代码
se::ScriptEngine::getInstance()->evalString(param1.c_str());
}
@end
c、具体调用内购sdk方法
IAPManager.h
#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;
-(void)buyRequest:(NSString *)productIdentifier;//购买
-(void)Restore:(NSString *)productIdentifier;
@end
IAPManager.m
#import "IAPManager.h"
#import "IAPInterface.h"
@implementation IAPManager
//初始化
-(void) attachObserver{
NSLog(@"AttachObserver");
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}
//判断用户是否可以付费
-(BOOL) CanMakePayment{
return [SKPaymentQueue canMakePayments];
}
//已内购的内容,恢复购买
-(void) Restore:(NSString *)productIdentifier{
NSLog(@"Restore");
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}
//请求商品数据
-(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{
NSLog(@"-----------收到产品反馈信息--------------");
NSArray *products = response.products;
NSLog(@"产品Product ID:%@",response.invalidProductIdentifiers);
NSLog(@"产品付费数量: %d", (int)[products count]);
// populate UI
for (SKProduct *p in products) {
NSLog(@"product info");
NSLog(@"SKProduct 描述信息%@", [products description]);
NSLog(@"产品标题 %@" , p.localizedTitle);
NSLog(@"产品描述信息: %@" , p.localizedDescription);
NSLog(@"价格: %@" , p.price);
NSLog(@"Product id: %@" , p.productIdentifier);
//UnitySendMessage("IOSIAPMgr", "ShowProductList", [[self productInfo:p] UTF8String]);
NSString *pps= [self productInfo:p];
[[IAPInterface sharedSingleton] ShowProductList:pps];
}
for(NSString *invalidProductId in response.invalidProductIdentifiers){
NSLog(@"Invalid product id:%@",invalidProductId);
}
// [request autorelease];
}
//请求购买
-(void)buyRequest:(NSString *)productIdentifier{
// NSArray* transactions=[SKPaymentQueue defaultQueue].transactions;
// if(transactions.count>0)
// {
// for(SKPaymentTransaction *tran in transactions)
// {
// NSLog(@"**************************************************************%@",tran.transactionState);
//检查是否有完成的交易
// SKPaymentTransaction* transaction =[transactions firstObject];
// if(tran.transactionState==SKPaymentTransactionStatePurchasing)
// {
// NSLog(@"----------------------%@",tran.transactionState);
// [[SKPaymentQueue defaultQueue] finishTransaction:tran];
// return;
// }
//}
// }
productIndentify=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("IOSIAPMgr", "ProvideContent", [[self transactionInfo:transaction] UTF8String]);
}
//沙盒测试环境验证
#define SANDBOX @"https://sandbox.itunes.apple.com/verifyReceipt"
//正式环境验证
#define AppStore @"https://buy.itunes.apple.com/verifyReceipt"
/**
* 验证购买,避免越狱软件模拟苹果请求达到非法购买问题
*
*/
-(void)verifyPurchaseWithPaymentTransaction{
//从沙盒中获取交易凭证并且拼接成请求体数据
NSURL *receiptUrl=[[NSBundle mainBundle] appStoreReceiptURL];
NSData *receiptData=[NSData dataWithContentsOfURL:receiptUrl];
NSString *receiptString=[receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];//转化为base64字符串
NSString *bodyString = [NSString stringWithFormat:@"{\"receipt-data\" : \"%@\"}", receiptString];//拼接请求数据
NSData *bodyData = [bodyString dataUsingEncoding:NSUTF8StringEncoding];
//测试的时候填写沙盒路径,上APPStore的时候填写正式环境路径
NSURL *url=[NSURL URLWithString:SANDBOX];
NSMutableURLRequest *requestM=[NSMutableURLRequest requestWithURL:url];
requestM.HTTPBody=bodyData;
requestM.HTTPMethod=@"POST";
//创建连接并发送同步请求
NSError *error=nil;
NSData *responseData=[NSURLConnection sendSynchronousRequest:requestM returningResponse:nil error:&error];
if (error) {
NSLog(@"验证购买过程中发生错误,错误信息:%@",error.localizedDescription);
return;
}
NSDictionary *dic=[NSJSONSerialization JSONObjectWithData:responseData options:NSJSONReadingAllowFragments error:nil];
NSLog(@"%@",dic);
if([dic[@"status"] intValue]==0){
NSLog(@"购买成功!");
NSDictionary *dicReceipt= dic[@"receipt"];
NSLog(@"--------------%@",dicReceipt);
//NSDictionary *dicInApp=[dicReceipt[@"in_app"] firstObject];
for(NSDictionary *tmp in dicReceipt[@"in_app"])
{
// NSLog(@"+++++++++++%@",dicInApp);
NSString *productIdentifier= tmp[@"product_id"];//读取产品标识
NSLog(@"+++++++++++++++++++++++++++++++++++++%@",productIdentifier);
NSLog(@"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx%@",productIndentify);
//如果是消耗品则记录购买数量,非消耗品则记录是否购买过
NSUserDefaults *defaults=[NSUserDefaults standardUserDefaults];
if ([productIdentifier isEqualToString:productIndentify]) {
NSInteger purchasedCount=[defaults integerForKey:productIdentifier];//已购买数量
[[NSUserDefaults standardUserDefaults] setInteger:(purchasedCount+1) forKey:productIdentifier];
[[IAPInterface sharedSingleton] BuyProcuctSucessCallBack:productIdentifier];
break;
}else{
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:productIdentifier];
[[IAPInterface sharedSingleton] BuyProcuctSucessCallBack:productIdentifier];
break;
}
}
}else{
NSLog(@"购买失败,未通过验证!");
[[IAPInterface sharedSingleton] BuyProcuctFailedCallBack:productIndentify];
}
}
//从App Store支付
- (BOOL)paymentQueue:(SKPaymentQueue *)queue shouldAddStorePayment:(SKPayment *)payment forProduct:(SKProduct *)product {
NSLog(@"从App Store支付");
//bool _is = paySuccess;
//根据product.productIdentifier去判断是否去直接弹出购买弹窗
if ([product.productIdentifier isEqualToString:@"1000000000"]) {
return true;
}
return false;
}
//监听购买结果
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transaction{
for(SKPaymentTransaction *tran in transaction){
switch (tran.transactionState) {
case SKPaymentTransactionStatePurchased:{
NSLog(@"交易完成");
// 发送到苹果服务器验证凭证
[self verifyPurchaseWithPaymentTransaction];
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
}
break;
case SKPaymentTransactionStatePurchasing:
NSLog(@"商品添加进列表");
break;
case SKPaymentTransactionStateRestored:{
NSLog(@"交易状态恢复");
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
}
break;
case SKPaymentTransactionStateFailed:{
NSLog(@"交易取消");
UIAlertView *mBoxView = [[UIAlertView alloc]
initWithTitle:@"Transaction reminder"
message:@"Transaction failed"
delegate:nil
cancelButtonTitle:nil
otherButtonTitles:@"OK", nil];
[mBoxView show];
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
[[IAPInterface sharedSingleton] BuyProcuctFailedCallBack:productIndentify];
//[[IAPInterface sharedSingleton] test1];
}
break;
default:
{
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
}
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];
}
//已内购的内容,恢复购买时回掉
-(void) paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue {
NSLog (@"received restored transactions: %i", queue.transactions.count);
//没有购买过
if (queue.transactions.count==0) {
}
//购买过
for (SKPaymentTransaction *transaction in queue.transactions)
{
NSString *productID = transaction.payment.productIdentifier;
NSLog(@"Restore transaction : %@",transaction.payment.productIdentifier);
[[IAPInterface sharedSingleton] RestoreBuyProductSucessCallBack:productID];
}
UIAlertView *mBoxView = [[UIAlertView alloc]
initWithTitle:@"Transaction reminder"
message:@"Restore purchase completed"
delegate:nil
cancelButtonTitle:nil
otherButtonTitles:@"OK", nil];
[mBoxView show];
}
@end
d、初始化内购
e、cocos调用
PlatformApi.ts
import { _decorator, Component, sys, game, director } from 'cc';
import GameManager from './GameManager';
const { ccclass, property } = _decorator;
@ccclass('PlatformApi')
export class PlatformApi extends Component {
private static _instance: PlatformApi | null = null;
public static get Instance() {
if (!PlatformApi._instance) {
window["PlatformApi"] = new PlatformApi();
PlatformApi._instance = new PlatformApi()
}
// return PlatformApi._instance || (PlatformApi._instance = new PlatformApi());
return PlatformApi._instance;
}
//内购请求(目前测试:1000000000)
public BuyProduct(eventId: string) {
console.log("BuyProduct " + eventId);
if (sys.platform == sys.Platform.ANDROID) {
} else if (sys.platform == sys.Platform.IOS) {
jsb.reflection.callStaticMethod("IAPInterface", "BuyProduct:", eventId);
}
}
//内购商品信息请求
public RequstProductInfo(eventId: string) {
console.log("RequstProductInfo " + eventId);
if (sys.platform == sys.Platform.ANDROID) {
} else if (sys.platform == sys.Platform.IOS) {
jsb.reflection.callStaticMethod("IAPInterface", "RequstProductInfo:", eventId);
}
}
//app升级或者重装后,对已内购信息恢复请求
public RestoreBuyProduct(eventId: string) {
console.log("RestoreBuyProduct " + eventId);
if (sys.platform == sys.Platform.ANDROID) {
} else if (sys.platform == sys.Platform.IOS) {
jsb.reflection.callStaticMethod("IAPInterface", "RestoreBuyProduct:", eventId);
}
}
//内购成功xcode回掉
public BuyProcuctSucessCallBack(eventId: string) {
console.log("BuyProcuctSucessCallBack " + eventId);
}
//内购失败xcode回掉
public BuyProcuctFailedCallBack(eventId: string) {
console.log("BuyProcuctFailedCallBack " + eventId);
}
//商品信息xcode回掉
public ShowProductList(eventId: string) {
console.log("ShowProductList " + eventId);
}
//对已购买的商品进行恢复xcode回掉
public RestoreBuyProductSucessCallBack(eventId: string) {
console.log("RestoreBuyProductSucessCallBack " + eventId);
if (eventId=="1000000000") {
//恢复已购买的商品
}
}
}
F:有两个接口需要特别注意
1、非消耗品,需要恢复接口,不然可能会拒审核,单机项目,app升级或者重装,对已购买的商品,进行恢复
参考:
ios内购——因缺少“恢复购买”功能被拒解决方案_专治各种审核难题的博客-CSDN博客_恢复购买
//已内购的内容,恢复购买
-(void) Restore:(NSString *)productIdentifier{
NSLog(@"Restore");
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}
//已内购的内容,恢复购买时回掉
-(void) paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue {
NSLog (@"received restored transactions: %i", queue.transactions.count);
//没有购买过
if (queue.transactions.count==0) {
}
//购买过
for (SKPaymentTransaction *transaction in queue.transactions)
{
NSString *productID = transaction.payment.productIdentifier;
NSLog(@"Restore transaction : %@",transaction.payment.productIdentifier);
[[IAPInterface sharedSingleton] RestoreBuyProductSucessCallBack:productID];
}
UIAlertView *mBoxView = [[UIAlertView alloc]
initWithTitle:@"Transaction reminder"
message:@"Restore purchase completed"
delegate:nil
cancelButtonTitle:nil
otherButtonTitles:@"OK", nil];
[mBoxView show];
}
2、appstore推广时,需要重载的接口,否则无法推广
参考:App内购项目的App Store推广 - 胡东东博客
//从App Store支付
- (BOOL)paymentQueue:(SKPaymentQueue *)queue shouldAddStorePayment:(SKPayment *)payment forProduct:(SKProduct *)product {
NSLog(@"从App Store支付");
//bool _is = paySuccess;
//推广的id对比
//根据product.productIdentifier去判断是否去直接弹出购买弹窗,
if ([product.productIdentifier isEqualToString:@"1000000000"]) {
return true;
}
return false;
}
3、测试
内购测试
内购也是需要测试的,但是内购涉及到钱,所以苹果为内购测试提供了 沙箱测试账号
的功能,Apple Pay 推出之后 沙箱测试账号
也可以用于 Apple Pay 支付的测试,沙箱测试账号
简单理解就是:只能用于内购和 Apple Pay 测试功能的 Apple ID,它并不是真实的 Apple ID,下面看如何创建 配置沙箱测试账号
测试结果类似下面截图:
App store推广测试
修改下面的链接地址,然后在手机safari浏览器打开,就可以测试从App Store发起购买了,进程序就会自动打开购买流程,其中链接中的bundleId
修改为你自己应用的bundleId,比如com.hudongdong.blog,productId
修改为你创建的商品的id
itms-services://?action=purchaseIntent&bundleId=bundleId
&productIdentifier=productId