├── README.md ├── RNInAppPurchaseModule.h └── RNInAppPurchaseModule.m /README.md: -------------------------------------------------------------------------------- 1 | # RNInAppPurchaseModule 2 | React-Native iOS内购模块,含缓存购买成功凭证防止丢单逻辑 3 | 4 | RN内购GitHub上也有很多封装好的模块,不过基本上都是国外的,包含Google的Android iap,对国内来说不需要,而且也没有丢单的处理,于是根据自己需要封装了一个,就两个文件,没必要用npm了,直接拖到Xcode中使用。 5 | 简书地址:https://www.jianshu.com/p/71b3382455f1 6 | 7 | ### 一、调用API一览 8 | ``` 9 | // 1.获取与服务器交互验证失败,缓存下来的漏单数组 10 | const iapUnverifyOrdersArray = RNInAppPurchaseModule.iapUnverifyOrdersArray; 11 | 12 | // 2.注册iap,监听并处理因App意外退出产生的漏单 13 | RNInAppPurchaseModule.addTransactionObserverWithCallback((error, purchase) => { 14 | }); 15 | 16 | // 3.与服务器验证成功,删除缓存的凭证 17 | RNInAppPurchaseModule.removePurchase(purchase); 18 | 19 | // 4.与苹果服务器交互,加载可卖的内购商品 20 | RNInAppPurchaseModule.loadProducts(iapProductIds, (error, products) => { 21 | }); 22 | 23 | // 5.购买某个商品 ProductId:苹果内购商品ID,myProductId:自己服务器商品ID(用来传回给服务器,字段名可以自己去文件里修改) 24 | RNInAppPurchaseModule.purchaseProduct(ProductId, myProductId, (error, purchase) => { 25 | }); 26 | 27 | // 6.恢复购买 28 | RNInAppPurchaseModule.restorePurchases((error, products) => { 29 | }); 30 | ``` 31 | 32 | ### 二、两种漏单内购情况 33 | ##### 1.App已经监听到苹果的购买成功回调,并且获得了内购凭证,再与服务器交互的过程中,验证失败(网络问题、或者服务器与苹果验证时产生问题)。这时需要缓存内购凭证,在适当的时候重新与服务器验证。 34 | ##### 2.用户购买过程中,App尚未接收到苹果购买成功回调,App意外闪退(没电、异常关机)。这时需要在App下次启动时,重新监听苹果购买回调并处理。 35 | 36 | ### 三、内购逻辑 37 | 以下是在我自己项目中的逻辑,可以根据自己项目需要调整: 38 | 39 | ##### 1.在App启动时,注册iap并检查有无漏单内购,如有,向服务器验证漏单内购 40 | ``` 41 | // 注册通知并检查漏单内购 42 | async registAndCheckIap() { 43 | 44 | // 处理与服务器交互失败,缓存下来的漏单 45 | const iapUnverifyOrdersArray = RNInAppPurchaseModule.iapUnverifyOrdersArray; 46 | for (let purchase of iapUnverifyOrdersArray) { 47 | // TODO: 与服务器交互验证购买凭证 48 | console.log(purchase); 49 | ...... 50 | // 验证成功,删除缓存的凭证 51 | RNInAppPurchaseModule.removePurchase(purchase); 52 | } 53 | 54 | // 注册iap,监听并处理因App意外推出产生的漏单 55 | RNInAppPurchaseModule.addTransactionObserverWithCallback((error, purchase) => { 56 | // TODO: 与服务器交互验证购买凭证 57 | console.log(purchase); 58 | ...... 59 | // 验证成功,删除缓存的凭证 60 | RNInAppPurchaseModule.removePurchase(purchase); 61 | }); 62 | } 63 | ``` 64 | ##### 2.进入商品列表页面,请求所有可卖iap商品 65 | ``` 66 | // 加载iOS内购商品 67 | if (iOS) { 68 | RNInAppPurchaseModule.loadProducts(this.state.iapProductIds, (error, products) => { 69 | }); 70 | } 71 | ``` 72 | ##### 3.购买商品并验证 73 | ``` 74 | if (iOS) { 75 | 76 | RNInAppPurchaseModule.purchaseProduct(produceId, myProductId, (error, result) => { 77 | 78 | if (error) { 79 | BXAlert.showTipAlert('提示', error || '购买失败'); 80 | } else { 81 | // TODO: 与服务器交互购买凭证 82 | console.log(result); 83 | ...... 84 | // 验证成功,删除缓存的凭证 85 | RNInAppPurchaseModule.removePurchase(result); 86 | } 87 | }); 88 | } 89 | ``` 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /RNInAppPurchaseModule.h: -------------------------------------------------------------------------------- 1 | // 2 | // RNInAppPurchaseModule.h 3 | // zybx 4 | // 5 | // Created by 刘乙灏 on 2018/9/5. 6 | // Copyright © 2018年 Facebook. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import 12 | #import 13 | 14 | @interface RNInAppPurchaseModule : NSObject 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /RNInAppPurchaseModule.m: -------------------------------------------------------------------------------- 1 | // 2 | // RNInAppPurchaseModule.m 3 | // zybx 4 | // 5 | // Created by 刘乙灏 on 2018/9/5. 6 | // Copyright © 2018年 Facebook. All rights reserved. 7 | // 8 | 9 | ////沙盒测试环境验证 10 | //#define SANDBOX @"https://sandbox.itunes.apple.com/verifyReceipt" 11 | ////正式环境验证 12 | //#define AppStore @"https://buy.itunes.apple.com/verifyReceipt" 13 | /* 14 | 内购验证凭据返回结果状态码说明 15 | 21000 App Store无法读取你提供的JSON数据 16 | 21002 收据数据不符合格式 17 | 21003 收据无法被验证 18 | 21004 你提供的共享密钥和账户的共享密钥不一致 19 | 21005 收据服务器当前不可用 20 | 21006 收据是有效的,但订阅服务已经过期。当收到这个信息时,解码后的收据信息也包含在返回内容中 21 | 21007 收据信息是测试用(sandbox),但却被发送到产品环境中验证 22 | 21008 收据信息是产品环境中使用,但却被发送到测试环境中验证 23 | */ 24 | 25 | #import "RNInAppPurchaseModule.h" 26 | #import 27 | 28 | // 未验证订单持久化参数 29 | #define kIapUnverifyOrders @"iap_unverify_orders" 30 | 31 | @interface SKProduct (StringPrice) // 格式化价格字符串 32 | 33 | @property (nonatomic, readonly) NSString *priceString; 34 | 35 | @end 36 | 37 | @implementation SKProduct (StringPrice) 38 | 39 | - (NSString *)priceString { 40 | NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init]; 41 | formatter.formatterBehavior = NSNumberFormatterBehavior10_4; 42 | formatter.numberStyle = NSNumberFormatterCurrencyStyle; 43 | formatter.locale = self.priceLocale; 44 | 45 | return [formatter stringFromNumber:self.price]; 46 | } 47 | 48 | @end 49 | 50 | @interface RNInAppPurchaseModule() 51 | 52 | @end 53 | 54 | @implementation RNInAppPurchaseModule 55 | { 56 | NSArray *products; // 所有可卖商品 57 | NSMutableDictionary *_callbacks; // 回调,key是商品id 58 | RCTResponseSenderBlock _lostCallBack; // 丢单数据的重新监听回调 59 | NSMutableDictionary *_myProductIds; // 业务服务器商品ID,key是商品id 60 | } 61 | 62 | - (instancetype)init 63 | { 64 | if ((self = [super init])) { 65 | _callbacks = [[NSMutableDictionary alloc] init]; 66 | _myProductIds = [[NSMutableDictionary alloc] init]; 67 | } 68 | return self; 69 | } 70 | 71 | - (dispatch_queue_t)methodQueue 72 | { 73 | return dispatch_get_main_queue(); 74 | } 75 | 76 | RCT_EXPORT_MODULE() 77 | /** 78 | * 添加商品购买状态监听 79 | * @params: 80 | * callback 针对购买过程中,App意外退出的丢单数据的回调 81 | */ 82 | RCT_EXPORT_METHOD(addTransactionObserverWithCallback:(RCTResponseSenderBlock)callback) { 83 | // 监听商品购买状态变化 84 | [[SKPaymentQueue defaultQueue] addTransactionObserver:self]; 85 | _lostCallBack = callback; 86 | } 87 | 88 | /** 89 | * 服务器验证成功,删除缓存的凭证 90 | */ 91 | RCT_EXPORT_METHOD(removePurchase:(NSDictionary *)purchase) { 92 | NSMutableArray *iapUnverifyOrdersArray = [NSMutableArray array]; 93 | if ([[NSUserDefaults standardUserDefaults] objectForKey:kIapUnverifyOrders] != nil) { 94 | [iapUnverifyOrdersArray addObjectsFromArray:[[NSUserDefaults standardUserDefaults] objectForKey:kIapUnverifyOrders]]; 95 | } 96 | for (NSDictionary *unverifyPurchase in iapUnverifyOrdersArray) { 97 | if ([unverifyPurchase[@"transactionIdentifier"] isEqualToString:purchase[@"transactionIdentifier"]]) { 98 | [iapUnverifyOrdersArray removeObject:unverifyPurchase]; 99 | } 100 | } 101 | [[NSUserDefaults standardUserDefaults] setObject:[iapUnverifyOrdersArray copy] forKey:kIapUnverifyOrders]; 102 | [[NSUserDefaults standardUserDefaults] synchronize]; 103 | } 104 | 105 | - (NSDictionary *)constantsToExport 106 | { 107 | // 获取当前缓存的所有凭证 108 | NSMutableArray *iapUnverifyOrdersArray = [NSMutableArray array]; 109 | if ([[NSUserDefaults standardUserDefaults] objectForKey:kIapUnverifyOrders] != nil) { 110 | [iapUnverifyOrdersArray addObjectsFromArray:[[NSUserDefaults standardUserDefaults] objectForKey:kIapUnverifyOrders]]; 111 | } 112 | return @{ @"iapUnverifyOrdersArray": iapUnverifyOrdersArray }; 113 | } 114 | 115 | /** 116 | * 购买某个商品 117 | * @params: 118 | * productIdentifier: 商品id 119 | * callback: 回调,返回 120 | */ 121 | RCT_EXPORT_METHOD(purchaseProduct:(NSString *)productIdentifier 122 | myProductId:(NSString *)myProductId 123 | callback:(RCTResponseSenderBlock)callback) 124 | { 125 | SKProduct *product; 126 | for(SKProduct *p in products) 127 | { 128 | if([productIdentifier isEqualToString:p.productIdentifier]) { 129 | product = p; 130 | break; 131 | } 132 | } 133 | 134 | if(product) { 135 | SKPayment *payment = [SKPayment paymentWithProduct:product]; 136 | [[SKPaymentQueue defaultQueue] addPayment:payment]; 137 | _callbacks[RCTKeyForInstance(payment.productIdentifier)] = callback; 138 | _myProductIds[RCTKeyForInstance(payment.productIdentifier)] = myProductId; 139 | } else { 140 | callback(@[@"无效商品"]); 141 | } 142 | } 143 | 144 | /** 145 | * 恢复购买 146 | */ 147 | RCT_EXPORT_METHOD(restorePurchases:(RCTResponseSenderBlock)callback) 148 | { 149 | NSString *restoreRequest = @"restoreRequest"; 150 | _callbacks[RCTKeyForInstance(restoreRequest)] = callback; 151 | [[SKPaymentQueue defaultQueue] restoreCompletedTransactions]; 152 | } 153 | 154 | /** 155 | * 加载所有可卖的商品 156 | */ 157 | RCT_EXPORT_METHOD(loadProducts:(NSArray *)productIdentifiers 158 | callback:(RCTResponseSenderBlock)callback) 159 | { 160 | if([SKPaymentQueue canMakePayments]){ 161 | SKProductsRequest *productsRequest = [[SKProductsRequest alloc] 162 | initWithProductIdentifiers:[NSSet setWithArray:productIdentifiers]]; 163 | productsRequest.delegate = self; 164 | _callbacks[RCTKeyForInstance(productsRequest)] = callback; 165 | [productsRequest start]; 166 | } else { 167 | callback(@[@"not_available"]); 168 | } 169 | } 170 | 171 | - (void)paymentQueue:(SKPaymentQueue *)queue 172 | updatedTransactions:(NSArray *)transactions 173 | { 174 | for (SKPaymentTransaction *transaction in transactions) { 175 | switch (transaction.transactionState) { 176 | // 购买失败 177 | case SKPaymentTransactionStateFailed: { 178 | NSString *key = RCTKeyForInstance(transaction.payment.productIdentifier); 179 | RCTResponseSenderBlock callback = _callbacks[key]; 180 | if (callback) { 181 | if(transaction.error.code != SKErrorPaymentCancelled){ 182 | NSLog(@"购买失败"); 183 | callback(@[@"购买失败"]); 184 | } else { 185 | NSLog(@"购买取消"); 186 | callback(@[@"购买取消"]); 187 | } 188 | [_callbacks removeObjectForKey:key]; 189 | } else { 190 | RCTLogWarn(@"No callback registered for transaction with state failed."); 191 | } 192 | [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; 193 | break; 194 | } 195 | // 购买成功 196 | case SKPaymentTransactionStatePurchased: { 197 | NSLog(@"购买成功"); 198 | NSString *key = RCTKeyForInstance(transaction.payment.productIdentifier); 199 | RCTResponseSenderBlock callback = _callbacks[key]; 200 | 201 | [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; 202 | 203 | if (callback) { 204 | 205 | // 购买成功,获取凭证 206 | [self buyAppleStoreProductSucceedWithPaymentTransactionp:transaction callback:callback]; 207 | } else if (_lostCallBack) { 208 | // 购买过程中出现意外App推出,下次启动App时的处理 209 | // 购买成功,获取凭证 210 | [self buyAppleStoreProductSucceedWithPaymentTransactionp:transaction callback:_lostCallBack]; 211 | } else { 212 | RCTLogWarn(@"No callback registered for transaction with state purcahsed."); 213 | } 214 | 215 | break; 216 | } 217 | 218 | // 恢复购买 219 | case SKPaymentTransactionStateRestored:{ 220 | NSLog(@"恢复购买成功"); 221 | NSString *key = RCTKeyForInstance(transaction.payment.productIdentifier); 222 | RCTResponseSenderBlock callback = _callbacks[key]; 223 | if (callback) { 224 | callback(@[@"恢复购买成功"]); 225 | [_callbacks removeObjectForKey:key]; 226 | } else { 227 | RCTLogWarn(@"No callback registered for transaction with state failed."); 228 | } 229 | [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; 230 | break; 231 | } 232 | // 正在购买 233 | case SKPaymentTransactionStatePurchasing: 234 | NSLog(@"正在购买"); 235 | break; 236 | 237 | // 交易还在队列里面,但最终状态还没有决定 238 | case SKPaymentTransactionStateDeferred: 239 | NSLog(@"推迟"); 240 | break; 241 | default: 242 | break; 243 | } 244 | } 245 | } 246 | 247 | // 苹果内购支付成功,获取凭证 248 | - (void)buyAppleStoreProductSucceedWithPaymentTransactionp:(SKPaymentTransaction *)transaction callback:(RCTResponseSenderBlock)callback { 249 | NSString *key = RCTKeyForInstance(transaction.payment.productIdentifier); 250 | NSString *transactionReceiptString= nil; 251 | // 验证凭据,获取到苹果返回的交易凭据 252 | // appStoreReceiptURL iOS7.0增加的,购买交易完成后,会将凭据存放在该地址 253 | NSURLRequest * appstoreRequest = [NSURLRequest requestWithURL:[[NSBundle mainBundle]appStoreReceiptURL]]; 254 | NSError *error = nil; 255 | NSData * receiptData = [NSURLConnection sendSynchronousRequest:appstoreRequest returningResponse:nil error:&error]; 256 | 257 | if (!receiptData) { 258 | callback(@[@"获取交易凭证失败"]); 259 | } else { 260 | transactionReceiptString = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed]; 261 | NSString *key = RCTKeyForInstance(transaction.payment.productIdentifier); 262 | NSString *myProductId = _myProductIds[key]; 263 | NSDictionary *purchase = @{ 264 | @"transactionIdentifier": transaction.transactionIdentifier, 265 | @"productIdentifier": transaction.payment.productIdentifier, 266 | @"receiptData": transactionReceiptString, 267 | @"myProductId": myProductId 268 | }; 269 | // 将凭证缓存,后台验证结束后再删除 270 | NSMutableArray *iapUnverifyOrdersArray = [NSMutableArray array]; 271 | if ([[NSUserDefaults standardUserDefaults] objectForKey:kIapUnverifyOrders] != nil) { 272 | [iapUnverifyOrdersArray addObjectsFromArray:[[NSUserDefaults standardUserDefaults] objectForKey:kIapUnverifyOrders]]; 273 | } 274 | [iapUnverifyOrdersArray addObject:purchase]; 275 | [[NSUserDefaults standardUserDefaults] setObject:[iapUnverifyOrdersArray copy] forKey:kIapUnverifyOrders]; 276 | [[NSUserDefaults standardUserDefaults] synchronize]; 277 | 278 | callback(@[[NSNull null], purchase]); 279 | [_callbacks removeObjectForKey:key]; 280 | } 281 | 282 | } 283 | 284 | - (void)paymentQueue:(SKPaymentQueue *)queue 285 | restoreCompletedTransactionsFailedWithError:(NSError *)error 286 | { 287 | NSString *key = RCTKeyForInstance(@"restoreRequest"); 288 | RCTResponseSenderBlock callback = _callbacks[key]; 289 | if (callback) { 290 | callback(@[@"恢复购买失败"]); 291 | [_callbacks removeObjectForKey:key]; 292 | } else { 293 | RCTLogWarn(@"No callback registered for restore product request."); 294 | } 295 | } 296 | 297 | - (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue 298 | { 299 | NSString *key = RCTKeyForInstance(@"restoreRequest"); 300 | RCTResponseSenderBlock callback = _callbacks[key]; 301 | if (callback) { 302 | NSMutableArray *productsArrayForJS = [NSMutableArray array]; 303 | for(SKPaymentTransaction *transaction in queue.transactions){ 304 | if(transaction.transactionState == SKPaymentTransactionStateRestored) { 305 | [productsArrayForJS addObject:transaction.payment.productIdentifier]; 306 | [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; 307 | } 308 | } 309 | callback(@[[NSNull null], productsArrayForJS]); 310 | [_callbacks removeObjectForKey:key]; 311 | } else { 312 | RCTLogWarn(@"No callback registered for restore product request."); 313 | } 314 | } 315 | 316 | // 所有可卖商品回调 317 | - (void)productsRequest:(SKProductsRequest *)request 318 | didReceiveResponse:(SKProductsResponse *)response 319 | { 320 | NSString *key = RCTKeyForInstance(request); 321 | RCTResponseSenderBlock callback = _callbacks[key]; 322 | if (callback) { 323 | products = [NSMutableArray arrayWithArray:response.products]; 324 | NSMutableArray *productsArrayForJS = [NSMutableArray array]; 325 | for(SKProduct *item in response.products) { 326 | NSDictionary *product = @{ 327 | @"identifier": item.productIdentifier, 328 | @"priceString": item.priceString, 329 | @"downloadable": item.downloadable ? @"true" : @"false" , 330 | @"description": item.localizedDescription ? item.localizedDescription : @"商品描述", 331 | @"title": item.localizedTitle ? item.localizedTitle : @"商品名称", 332 | }; 333 | [productsArrayForJS addObject:product]; 334 | } 335 | callback(@[[NSNull null], productsArrayForJS]); 336 | [_callbacks removeObjectForKey:key]; 337 | } else { 338 | RCTLogWarn(@"No callback registered for load product request."); 339 | } 340 | } 341 | 342 | - (void)dealloc 343 | { 344 | [[SKPaymentQueue defaultQueue] removeTransactionObserver:self]; 345 | } 346 | 347 | #pragma mark Private 348 | 349 | static NSString *RCTKeyForInstance(id instance) 350 | { 351 | return [NSString stringWithFormat:@"%p", instance]; 352 | } 353 | @end 354 | --------------------------------------------------------------------------------