From 623d0491301382d19bc4fb834d669f622af409ee Mon Sep 17 00:00:00 2001 From: hyochan Date: Mon, 24 Dec 2018 03:36:56 +0900 Subject: [PATCH] Add additional event listener to handle success purchase after failure. Make workaround for #307. Resolve #307 for now. --- README.md | 17 +++++++++++++++++ RNIapExample/src/components/pages/First.js | 15 +++++++++------ index.d.ts | 13 ++++++++++++- index.js | 16 +++++++++++++++- ios/RNIapIos.h | 8 ++------ ios/RNIapIos.m | 17 ++++++++--------- package.json | 2 +- 7 files changed, 64 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 86dfc655d..68e5ddd8a 100644 --- a/README.md +++ b/README.md @@ -215,6 +215,23 @@ RNIap.buyProduct('com.example.coins100').then(purchase => { Subscribable products can be purchased just like consumable products. Users can cancel subscriptions by using the iOS System Settings. +## Purchase Example 3 (Advanced) +```javascript +try { + const purchase: any = await RNIap.buyProduct(sku); + this.setState({ receipt: purchase.transactionReceipt }, () => this.goToNext()); +} catch (err) { + console.warn(err.code, err.message); + const subscription = RNIap.addAdditionalSuccessPurchaseListenerIOS(async (purchase) => { + this.setState({ receipt: purchase.transactionReceipt }, () => this.goToNext()); + subscription.remove(); + }); +} +``` +If you need to handle the success of purchase which could be called even after purchase failed, +you can add `addAdditionalSuccessPurchaseListenerIOS` to handle nex `successPurchase`. +* This feature is provided from `react-native-iap` version `2.4.0-beta1`. + ## Consumption and Restoring Purchases You can use `getAvailablePurchases()` to do what's commonly understood as "restoring" purchases. Once an item is consumed, it will no longer be available in `getAvailablePurchases()` and will only be available via `getPurchaseHistory()`. However, this method has some caveats on Android -- namely, that purchase history only exists for the single most recent purchase of each SKU -- so your best bet is to track consumption in your app yourself. By default, all items that are purchased will not be consumed unless they are automatically consumed by the store (for example, if you create a consumable item for iOS.) This means that you must manage consumption yourself. Purchases can be consumed by calling `consumePurchase()`. If you want to consume all items, you have to iterate over the purchases returned by `getAvailablePurchases()`. diff --git a/RNIapExample/src/components/pages/First.js b/RNIapExample/src/components/pages/First.js index 8b46230a3..2732025db 100644 --- a/RNIapExample/src/components/pages/First.js +++ b/RNIapExample/src/components/pages/First.js @@ -81,16 +81,19 @@ class Page extends Component { } buyItem = async(sku) => { + console.info('buyItem: ' + sku); + // const purchase = await RNIap.buyProduct(sku); + // const products = await RNIap.buySubscription(sku); + // const purchase = await RNIap.buyProductWithoutFinishTransaction(sku); try { - console.info('buyItem: ' + sku); - // const purchase = await RNIap.buyProduct(sku); - // const products = await RNIap.buySubscription(sku); - const purchase = await RNIap.buyProductWithoutFinishTransaction(sku); - console.info(purchase); + const purchase: any = await RNIap.buyProduct(sku); this.setState({ receipt: purchase.transactionReceipt }, () => this.goToNext()); } catch (err) { console.warn(err.code, err.message); - Alert.alert(err.message); + const subscription = RNIap.addAdditionalSuccessPurchaseListenerIOS(async (purchase) => { + this.setState({ receipt: purchase.transactionReceipt }, () => this.goToNext()); + subscription.remove(); + }); } } diff --git a/index.d.ts b/index.d.ts index d53b0d3e9..04fe5f1c5 100644 --- a/index.d.ts +++ b/index.d.ts @@ -6,7 +6,6 @@ interface Common { price: string currency: string localizedPrice: string - localeIOS: string } export interface Product extends Common { @@ -18,6 +17,11 @@ export interface Subscription extends Common { type: 'subs' | 'sub' productId: ID + introductoryPrice?: string + introductoryPricePaymentModeIOS?: string + introductoryPriceNumberOfPeriods?: number + introductoryPriceSubscriptionPeriod: object + subscriptionPeriodNumberIOS?: string subscriptionPeriodUnitIOS?: number @@ -25,6 +29,7 @@ export interface Subscription extends Common { introductoryPriceCyclesAndroid?: number introductoryPricePeriodAndroid?: string subscriptionPeriodAndroid?: string + freeTrialPeriodAndroid: string } export interface ProductPurchase { @@ -180,3 +185,9 @@ export function validateReceiptIos(receiptBody: Apple.ReceiptValidationRequest, * @param isSub whether this is subscription or inapp. `true` for subscription. */ export function validateReceiptAndroid(packageName: string, productId: string, productToken: string, accessToken: string, isSub: boolean): Promise; + +/** + * Add IAP purchase event in ios. + * @returns {callback(e: Event)} + */ +export function addAdditionalSuccessPurchaseListenerIOS(fn: Function); diff --git a/index.js b/index.js index 50fb1548f..db7a6beba 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,5 @@ -import { NativeModules, Platform } from 'react-native'; +import { NativeModules, Platform, NativeEventEmitter } from 'react-native'; const { RNIapIos, RNIapModule } = NativeModules; @@ -235,6 +235,19 @@ export const validateReceiptAndroid = async(packageName, productId, productToken return response.json(); }; +/** + * Add IAP purchase event in ios. + * @returns {callback(e: Event)} + */ +export const addAdditionalSuccessPurchaseListenerIOS = (e) => { + if (Platform.OS === 'ios') { + const myModuleEvt = new NativeEventEmitter(RNIapIos); + return myModuleEvt.addListener('iap-purchase-event', e); + } else { + console.log('adding purchase listener is only provided in ios.'); + } +}; + /** * deprecagted codes */ @@ -287,4 +300,5 @@ export default { consumePurchase, validateReceiptIos, validateReceiptAndroid, + addAdditionalSuccessPurchaseListenerIOS, }; diff --git a/ios/RNIapIos.h b/ios/RNIapIos.h index 8fca812eb..514f54ab7 100644 --- a/ios/RNIapIos.h +++ b/ios/RNIapIos.h @@ -1,12 +1,8 @@ -#if __has_include("RCTBridgeModule.h") -#import "RCTBridgeModule.h" -#else #import -#endif - +#import #import -@interface RNIapIos : NSObject +@interface RNIapIos : RCTEventEmitter { SKProductsRequest *productsRequest; NSMutableArray *validProducts; diff --git a/ios/RNIapIos.m b/ios/RNIapIos.m index 90b987f8a..c54be731c 100644 --- a/ios/RNIapIos.m +++ b/ios/RNIapIos.m @@ -73,6 +73,11 @@ -(void)rejectPromisesForKey:(NSString*)key code:(NSString*)code message:(NSStrin //////////////////////////////////////////////////// _//////////_// EXPORT_MODULE RCT_EXPORT_MODULE(); +- (NSArray *)supportedEvents +{ + return @[@"iap-purchase-event"]; +} + RCT_EXPORT_METHOD(canMakePayments:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { BOOL canMakePayments = [SKPaymentQueue canMakePayments]; @@ -287,6 +292,9 @@ -(void)purchaseProcess:(SKPaymentTransaction *)transaction { NSURL *receiptUrl = [[NSBundle mainBundle] appStoreReceiptURL]; NSDictionary* purchase = [self getPurchaseData:transaction]; [self resolvePromisesForKey:RCTKeyForInstance(transaction.payment.productIdentifier) value:purchase]; + + // additionally send event + [self sendEventWithName:@"iap-purchase-event" body: purchase]; } -(NSString *)standardErrorCode:(int)code { @@ -315,7 +323,6 @@ -(NSDictionary*)getProductObject:(SKProduct *)product { NSString* localizedPrice = [formatter stringFromNumber:product.price]; NSString* introductoryPrice = localizedPrice; - NSString* locale = localeIdentifierToBCP47(product.priceLocale.localeIdentifier); NSString* introductoryPricePaymentMode = @""; NSString* introductoryPriceNumberOfPeriods = @""; @@ -401,7 +408,6 @@ -(NSDictionary*)getProductObject:(SKProduct *)product { product.localizedTitle ? product.localizedTitle : @"", @"title", product.localizedDescription ? product.localizedDescription : @"", @"description", localizedPrice, @"localizedPrice", - locale, @"localeIOS", periodNumberIOS, @"subscriptionPeriodNumberIOS", periodUnitIOS, @"subscriptionPeriodUnitIOS", introductoryPrice, @"introductoryPrice", @@ -454,11 +460,4 @@ - (NSDictionary *)getPurchaseData:(SKPaymentTransaction *)transaction { return [NSString stringWithFormat:@"%p", instance]; } -static NSString *localeIdentifierToBCP47(NSString* localeIdentifier) -{ - // e.g. `en_US@currency=USD` -> `en-US`. - NSString *identifier = [[product.priceLocale.localeIdentifier componentsSeparatedByString:@"@"] objectAtIndex:0]; - return [identifier stringByReplacingOccurrencesOfString:@"_" withString:@"-"]; -} - @end diff --git a/package.json b/package.json index 11466fd93..9494b63b5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-iap", - "version": "2.3.25", + "version": "2.4.0-betal", "description": "React Native In App Purchase Module.", "main": "index.js", "types": "index.d.ts",