#import "IAPWrapper.h" #import #import "platform/apple/JsbBridgeWrapper.h" #include "application/ApplicationManager.h" #include "application/BaseApplication.h" #include "base/Scheduler.h" #include "engine/BaseEngine.h" @interface IAPWrapper () @property(nonatomic, strong) NSMutableDictionary *products; @property(nonatomic, strong) SKProductsRequest *productsRequest; @property(nonatomic, strong) NSDictionary *initRequest; @property(nonatomic, strong) NSDictionary *purchaseRequest; @property(nonatomic, strong) NSDictionary *restoreRequest; @property(nonatomic, strong) NSMutableArray *restored; @end @implementation IAPWrapper + (instancetype)shared { static IAPWrapper *instance; static dispatch_once_t once; dispatch_once(&once, ^{ instance = [IAPWrapper new]; instance.products = [NSMutableDictionary new]; }); return instance; } - (void)start { static dispatch_once_t once; dispatch_once(&once, ^{ // Global, capture-free listener survives Creator 3.8.8 MRC ownership. static OnScriptEventListener const listener = ^(NSString *payload) { dispatch_async(dispatch_get_main_queue(), ^{ [[IAPWrapper shared] handle:payload]; }); }; [[JsbBridgeWrapper sharedInstance] addScriptEventListener:@"iap.request" listener:listener]; [SKPaymentQueue.defaultQueue addTransactionObserver:self]; }); } - (void)reply:(NSDictionary *)request data:(id)data error:(NSString *)error { if (!request) return; NSMutableDictionary *response = [@{@"requestId": request[@"requestId"] ?: @"", @"method": request[@"method"] ?: @"", @"success": @(!error)} mutableCopy]; if (data) response[@"data"] = data; if (error) response[@"error"] = error; NSData *json = [NSJSONSerialization dataWithJSONObject:response options:0 error:nil]; NSString *payload = [[NSString alloc] initWithData:json encoding:NSUTF8StringEncoding]; auto app = CC_CURRENT_APPLICATION(); if (!app || !app->getEngine() || !payload) return; app->getEngine()->getScheduler()->performFunctionInCocosThread([payload]() { [[JsbBridgeWrapper sharedInstance] dispatchEventToScript:@"iap.response" arg:payload]; }); } - (NSDictionary *)dataFor:(SKPaymentTransaction *)transaction { NSURL *url = NSBundle.mainBundle.appStoreReceiptURL; NSData *receipt = url ? [NSData dataWithContentsOfURL:url] : nil; NSString *status = transaction.transactionState == SKPaymentTransactionStateRestored ? @"restored" : transaction.transactionState == SKPaymentTransactionStateDeferred ? @"deferred" : @"purchased"; return @{@"status": status, @"productId": transaction.payment.productIdentifier ?: @"", @"transactionId": transaction.transactionIdentifier ?: @"", @"originalTransactionId": transaction.originalTransaction.transactionIdentifier ?: transaction.transactionIdentifier ?: @"", @"receipt": [receipt base64EncodedStringWithOptions:0] ?: @""}; } - (NSArray *)pending { NSMutableArray *items = [NSMutableArray new]; for (SKPaymentTransaction *t in SKPaymentQueue.defaultQueue.transactions) { if (t.transactionState == SKPaymentTransactionStatePurchased || t.transactionState == SKPaymentTransactionStateRestored || t.transactionState == SKPaymentTransactionStateDeferred) [items addObject:[self dataFor:t]]; } return items; } - (void)handle:(NSString *)payload { id decoded = [NSJSONSerialization JSONObjectWithData:[payload dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil]; if (![decoded isKindOfClass:NSDictionary.class]) return; NSDictionary *r = decoded; if (![r[@"requestId"] isKindOfClass:NSString.class] || ![r[@"method"] isKindOfClass:NSString.class]) return; NSString *method = r[@"method"]; NSDictionary *p = [r[@"params"] isKindOfClass:NSDictionary.class] ? r[@"params"] : @{}; if ([method isEqualToString:@"init"]) { if (self.productsRequest) { [self reply:r data:nil error:@"正在查询商品,请稍后重试。"]; return; } NSArray *ids = p[@"productIds"]; if (![ids isKindOfClass:NSArray.class] || !ids.count) { [self reply:r data:nil error:@"商品列表不能为空。"]; return; } for (id value in ids) if (![value isKindOfClass:NSString.class] || ![value length]) { [self reply:r data:nil error:@"商品 ID 必须为非空字符串。"]; return; } self.initRequest = r; self.productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithArray:ids]]; self.productsRequest.delegate = self; [self.productsRequest start]; } else if ([method isEqualToString:@"purchase"]) { if (self.purchaseRequest || self.restoreRequest || SKPaymentQueue.defaultQueue.transactions.count) { [self reply:r data:nil error:@"存在进行中或未完成交易,请先处理未完成交易。"]; return; } NSString *productID = p[@"productId"]; SKProduct *product = [productID isKindOfClass:NSString.class] ? self.products[productID] : nil; if (!product || ![SKPaymentQueue canMakePayments]) { [self reply:r data:nil error:@"商品未初始化或此设备不允许内购。"]; return; } self.purchaseRequest = r; [SKPaymentQueue.defaultQueue addPayment:[SKPayment paymentWithProduct:product]]; } else if ([method isEqualToString:@"restore"]) { if (self.restoreRequest || self.purchaseRequest) { [self reply:r data:nil error:@"已有购买或恢复操作进行中。"]; return; } self.restoreRequest = r; self.restored = [NSMutableArray new]; [SKPaymentQueue.defaultQueue restoreCompletedTransactions]; } else if ([method isEqualToString:@"pending"]) { [self reply:r data:[self pending] error:nil]; } else if ([method isEqualToString:@"finish"]) { NSString *identifier = p[@"transactionId"]; if (![identifier isKindOfClass:NSString.class] || !identifier.length) { [self reply:r data:nil error:@"无效的交易 ID。"]; return; } for (SKPaymentTransaction *t in SKPaymentQueue.defaultQueue.transactions) { if ([t.transactionIdentifier isEqualToString:identifier] && (t.transactionState == SKPaymentTransactionStatePurchased || t.transactionState == SKPaymentTransactionStateRestored)) { [SKPaymentQueue.defaultQueue finishTransaction:t]; [self reply:r data:@{} error:nil]; return; } } [self reply:r data:nil error:@"未找到可完成的交易,请查询未完成交易。"]; } else { [self reply:r data:nil error:@"不支持的内购方法。"]; } } - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response { dispatch_async(dispatch_get_main_queue(), ^{ if (request != self.productsRequest) return; NSMutableArray *items = [NSMutableArray new]; for (SKProduct *product in response.products) { self.products[product.productIdentifier] = product; NSNumberFormatter *formatter = [NSNumberFormatter new]; formatter.numberStyle = NSNumberFormatterCurrencyStyle; formatter.locale = product.priceLocale; [items addObject:@{@"productId": product.productIdentifier, @"title": product.localizedTitle ?: @"", @"description": product.localizedDescription ?: @"", @"price": product.price.stringValue, @"localizedPrice": [formatter stringFromNumber:product.price] ?: @"", @"currencyCode": formatter.currencyCode ?: @""}]; } [self reply:self.initRequest data:@{@"products": items, @"invalidProductIds": response.invalidProductIdentifiers} error:nil]; self.initRequest = nil; self.productsRequest = nil; }); } - (void)request:(SKRequest *)request didFailWithError:(NSError *)error { dispatch_async(dispatch_get_main_queue(), ^{ if (request != self.productsRequest) return; [self reply:self.initRequest data:nil error:error.localizedDescription]; self.initRequest = nil; self.productsRequest = nil; }); } - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions { dispatch_async(dispatch_get_main_queue(), ^{ for (SKPaymentTransaction *t in transactions) { if (t.transactionState == SKPaymentTransactionStatePurchasing) continue; BOOL matches = [self.purchaseRequest[@"params"][@"productId"] isEqualToString:t.payment.productIdentifier]; if (t.transactionState == SKPaymentTransactionStateFailed) { BOOL cancelled = t.error.code == SKErrorPaymentCancelled && [t.error.domain isEqualToString:SKErrorDomain]; NSDictionary *data = @{@"status": @"cancelled", @"productId": t.payment.productIdentifier ?: @""}; if (matches) { [self reply:self.purchaseRequest data:cancelled ? data : nil error:cancelled ? nil : t.error.localizedDescription ?: @"支付失败。"]; self.purchaseRequest = nil; } [queue finishTransaction:t]; continue; } NSDictionary *data = [self dataFor:t]; if (t.transactionState == SKPaymentTransactionStateRestored && self.restoreRequest) [self.restored addObject:data]; if (matches && t.transactionState != SKPaymentTransactionStateRestored) { [self reply:self.purchaseRequest data:data error:nil]; self.purchaseRequest = nil; } // Successful transactions remain in StoreKit until backend delivery is acknowledged. [self reply:@{@"method": @"transaction"} data:data error:nil]; } }); } - (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue { dispatch_async(dispatch_get_main_queue(), ^{ [self reply:self.restoreRequest data:self.restored ?: @[] error:nil]; self.restoreRequest = nil; self.restored = nil; }); } - (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error { dispatch_async(dispatch_get_main_queue(), ^{ [self reply:self.restoreRequest data:nil error:error.localizedDescription]; self.restoreRequest = nil; self.restored = nil; }); } @end