起因是这样,自去年12月份,就陆续有玩家反馈以下问题购买了商品,却无法获得,也无法恢复购买

兑换码无法兑换到商品

重现:在设备1上兑换了A商品,恢复购买和再次免费购买,是无效的,而在设备2上用同一个账号却是有效的。

在设备上再次购买已购买的A商品,提示已购买将免费获取,点击后却无反应。

由于这只能在正式环境上重现,可以代码调试的沙盒测试是不行的,只能通过猜测和上线调试代码才能验证问题

初步猜测:没有拿到收据

收据没有通过二次验证

于是加入了以下代码验证:

var data = new Dictionary
{
{ "receipt_data", transactionResult.receipt },
};
//将内购信息转发到服务器收集查询ServerManager.instance.SendToUrl("purchase/receipts", "POST", data, jObject =>
{
var statusToken = jObject["status"];
if (statusToken.IsNullOrEmpty())
{
callback(false);
return;
}
var status = (int)statusToken;
callback(status == 0);
});

通过收据内容就可以分析。

最后发现,客户端的内购没有回调

//内购状态回调

-(void) paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions

继续往下分析Apple bug

由于内购OC代码是借助Unity插件IOSNative的,所以可能是这部分OC代码问题

内购丢单和兑换码无效是同一个模块导致的

OC代码问题

由于代码是IOSNative的,找了一些主流的内购插件,甚至Unity自带的Unity IAP,底层原理都一样,还是无法解决问题,所以需要自己去写OC代码,把内购信息从OC转发到C#去验证

#import 
#import 
NSString *managerName = @"IAPManager";
NSString *initSucceedFuncName = @"OnInitSucceed";
NSString *transactionFuncName = @"OnTransactionCompleted";
NSString *restoreCompleteFuncName = @"OnRestoreCompleted";
NSString *splitString = @"-";
@interface IAPManager : NSObject
{
SKProductsRequest *productsRequest;
NSArray *products;
}
-(void)initIAP;
-(void)buy:(NSString *)productIdentifier;
-(void)restore;
@end
@implementation IAPManager
IAPManager *iapManager = nil;
//初始化, 商品id用,分隔
-(void) initIAP:(NSString *)productIdentifiers
{
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
NSArray *idArray = [productIdentifiers componentsSeparatedByString:splitString];
NSSet *idSet = [NSSet setWithArray:idArray];
SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:idSet];
request.delegate = self;
[request start];
}
//购买
-(void) buy:(NSString *)productIdentifier
{
SKProduct *product = nil;
for (SKProduct *p in products)
{
if ([p.productIdentifier isEqualToString:productIdentifier])
{
product = p;
break;
}
}
if (product != nil)
{
SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
}
//恢复购买
-(void) restore
{
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}
//接受初始化后获得的商品信息事件
-(void) productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
products = response.products;
NSString *stringInfos = @"";
for (SKProduct *product in products)
{
NSArray *info = [NSArray arrayWithObjects:product.productIdentifier, product.localizedTitle, product.price.stringValue, product.priceLocale.currencySymbol, nil];
NSString *stringInfo = [info componentsJoinedByString:splitString];
NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
[numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4];
[numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
[numberFormatter setLocale:product.priceLocale];
NSString *formattedPrice = [numberFormatter stringFromNumber:product.price];
stringInfos = [stringInfos stringByAppendingString:stringInfo];
stringInfos = [stringInfos stringByAppendingString:splitString];
stringInfos = [stringInfos stringByAppendingString:formattedPrice];
stringInfos = [stringInfos stringByAppendingString:@"\n"];
}
[self sendToManager:initSucceedFuncName :stringInfos];
}
-(void) onTransactionCompleted:(SKPaymentTransaction *)transaction
{
NSString *state = nil;
switch (transaction.transactionState)
{
case SKPaymentTransactionStatePurchased:
state = @"purchased";
break;
case SKPaymentTransactionStateRestored:
state = @"restored";
break;
case SKPaymentTransactionStateDeferred:
state = @"deferred";
break;
case SKPaymentTransactionStateFailed:
state = @"failed";
break;
default:
break;
}
NSString* receipt = transaction.transactionReceipt.base64Encoding;
NSString* productId = transaction.payment.productIdentifier;
NSArray *info = [NSArray arrayWithObjects:productId, state, receipt, nil];
NSString *stringInfo = [info componentsJoinedByString:splitString];
[self sendToManager:transactionFuncName:stringInfo];
}
//内购状态回调
-(void) paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
for (SKPaymentTransaction *transaction in transactions)
{
if (transaction.transactionState != SKPaymentTransactionStatePurchasing)
{
[self onTransactionCompleted:transaction];
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
}
}
-(void) paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
{
[self sendToManager:restoreCompleteFuncName :nil];
}
-(void) paymentQueue:(SKPaymentQueue *) paymentQueue restoreCompletedTransactionsFailedWithError:(NSError *)error
{
[self sendToManager:restoreCompleteFuncName :nil];
}
-(void)sendToManager:(NSString *)methodName :(NSString *)args
{
UnitySendMessage([managerName UTF8String], [methodName UTF8String], args == nil ? "" : [args UTF8String]);
}
@end
extern "C"
{
bool IsPurchaseAvaible()
{
return [SKPaymentQueue canMakePayments];
}
void InitIAP(char *p)
{
iapManager = [[IAPManager alloc] init];
NSString *list = [NSString stringWithUTF8String:p];
[iapManager initIAP:list];
}
void Buy(char *p)
{
[iapManager buy:[NSString stringWithUTF8String:p]];
}
void Restore()
{
[iapManager restore];
[iapManager refreshReceipt];
}
}

结果上线了一个新的版本还是一样,没有解决

Apple bug

Apple TSI(苹果技术支持)

在设备1上兑换了A商品,恢复购买和再次免费购买,没有“updatedTransactions”的回调,而在设备2上用同一个账号却是有的,然后在Console上把整个过程的log保存下来对比,发现有大量术语和标志的一些不明意义的命名,只能联系Apple TSI帮忙查看,估计内部有对照的一些问题的标志,得到以下回复


如果有“fetchSoftwareAddons”的话,发他看。整理一下log又发了一封邮件,等待的时候重写了内购底层OC的IAP,Apple效率还是挺高的,两天就有回复了,如下


这确实是一个bug,需要反馈到Apple bug去解决。XD

解决方案反馈到Apple Bug,等他们修复

找寻另外的办法解决

到Apple developer forum找解决方案

遇到了不少同样问题的开发者,但都还没有解决方案这个是兑换码一样也是有的玩家无法兑换的

这个是无法恢复购买的

通过另外的方式去恢复购买或兑换

Apple TSI的技术提供了新思路,通过刷新购买收据,解析收据去恢复购买

NSString *receiptCompletedFuncName = @"OnReceiptCompleted";
//刷新凭证
-(void) refreshReceipt
{
SKReceiptRefreshRequest *request = [[SKReceiptRefreshRequest alloc] init];
request.delegate = self;
[request start];
NSLog(@"[IAPManager]: On refresh receipt started");
}
-(void) requestDidFinish:(SKRequest *)request
{
NSLog(@"[IAPManager]: On refresh receipt finished");
if ([request isKindOfClass:[SKReceiptRefreshRequest class]])
{
NSURL *receiptUrl = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receiptData = [NSData dataWithContentsOfURL:receiptUrl];
NSString *receiptString = receiptData.base64Encoding;
[self sendToManager:receiptCompletedFuncName:receiptString];
}
}
extern "C"
{
void Restore()
{
...
[iapManager refreshReceipt];
}
}
receipt-data转base64转发到C#服务器处理(目前是转发App Store做二次验证,以后为了更安全需要改动转发到公司服务器验证)
//TODO: 公司服务器验证private void OnReceiptCompleted(string receiptData)
{
var data = new Dictionary
{
{ "receipt-data", receiptData },
};
var binaryData = System.Text.Encoding.UTF8.GetBytes(JsonUtils.Serialize(data));
var www = new WWW("https://buy.itunes.apple.com/verifyReceipt", binaryData);
CoroutineManager.instance.StartCoroutine(() =>
{
if (www.error != null)
{
//错误处理 }
else
{
var jObject = JsonUtils.Deserialize(www.text);
var status = (int)jObject["status"];
if (status == 0)
{
OnReceiptDataVertify(jObject);
}
else if (status == 21007)
{
//sandbox test www = new WWW("https://sandbox.itunes.apple.com/verifyReceipt", binaryData);
CoroutineManager.instance.StartCoroutine(() =>
{
if (www.error != null)
{
//错误处理 }
else
{
Debug.Log(string.Format("[IAPManager]: receiptData {0}", www.text));
jObject = JsonUtils.Deserialize(www.text);
status = (int)jObject["status"];
if (status == 0)
{
OnReceiptDataVertify(jObject);
}
}
}, () => www.isDone);
}
else
{
}
}
}, () => www.isDone);
}

根据收据结构解析jObject的in_app数组即可获知玩家真实拥有的内购商品id,最后问题解决了~

结语所有有丢单问题和兑换码无效的玩家的问题都解决了,不过这个bug还是得反馈的,一个功能对应一段代码,恢复购买目前是通过两种方式实现,其实并不是一个好的代码结构,反馈中,等待Apple bug进一步的回复

OC的语法是shi

与Apple TSI沟通3要素简单描述问题

录制视频重现问题

同时连接Macos的Console.app发调试信息