Skip to content

Commit

Permalink
Merge pull request #58 from dooboolab/addAdditionalSuccessPurchaseLis…
Browse files Browse the repository at this point in the history
…tenerIOS

Add additional success purchase listener ios
  • Loading branch information
hyochan committed Jan 29, 2019
2 parents 3030851 + 1001544 commit d32ba9c
Show file tree
Hide file tree
Showing 7 changed files with 111 additions and 44 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* Use dictionaryWithObjectsAndKeys in NSDictionary to fetch product values. This will prevent from NSInvalidArgumentException in ios which rarely occurs.
* Fixed wrong npe in `android` when `getAvailablePurchases`.
+ Only parse `orderId` when exists in `Android` to prevent crashing.
+ Add additional success purchase listener in `iOS`. Related [#54](https://github.com/dooboolab/flutter_inapp_purchase/issues/54)

## 0.7.1
* Implemented receiptValidation for both android and ios.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ For help on editing plugin code, view the [documentation](https://flutter.io/dev
| getAppStoreInitiatedProducts | | `List<IAPItem>` | If the user has initiated a purchase directly on the App Store, the products that the user is attempting to purchase will be returned here. (iOS only) Note: On iOS versions earlier than 11.0 this method will always return an empty list, as the functionality was introduced in v11.0. [See Apple Docs for more info](https://developer.apple.com/documentation/storekit/skpaymenttransactionobserver/2877502-paymentqueue) Always returns an empty list on Android.
| buySubscription | `string` Subscription ID/sku, `string` Old Subscription ID/sku (on Android) | `PurchasedItem` | Create (buy) a subscription to a sku. For upgrading/downgrading subscription on Android pass second parameter with current subscription ID, on iOS this is handled automatically by store. |
| buyProduct | `string` Product ID/sku | `PurchasedItem` | Buy a product |
| buyProductWithoutFinishTransaction | `string` Product ID/sku | `PurchasedItem` | Buy a product without finish transaction call (iOS only) |
| ~~buyProductWithoutFinishTransaction~~ | `string` Product ID/sku | `PurchasedItem` | Buy a product without finish transaction call (iOS only) |
| finishTransaction | `void` | `String` | Send finishTransaction call to Apple IAP server. Call this function after receipt validation process |
| consumePurchase | `String` Purchase token | `String` | Consume a product (on Android.) No-op on iOS. |
| endConnection | | `String` | End billing connection (on Android.) No-op on iOS. |
Expand Down
2 changes: 1 addition & 1 deletion analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
analyzer:
strong-mode:
implicit-casts: false
implicit-casts: true
implicit-dynamic: false
4 changes: 0 additions & 4 deletions example/ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
27672194C9D5FA8DDDF731B8 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A72A37C715E27BACEF745D36 /* libPods-Runner.a */; };
2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */ = {isa = PBXBuildFile; fileRef = 2D5378251FAA1A9400D5DBA9 /* flutter_assets */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; };
3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
Expand Down Expand Up @@ -42,7 +41,6 @@
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
2D5378251FAA1A9400D5DBA9 /* flutter_assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = flutter_assets; path = Flutter/flutter_assets; sourceTree = SOURCE_ROOT; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
Expand Down Expand Up @@ -85,7 +83,6 @@
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
2D5378251FAA1A9400D5DBA9 /* flutter_assets */,
3B80C3931E831B6300D905FE /* App.framework */,
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEBA1CF902C7004384FC /* Flutter.framework */,
Expand Down Expand Up @@ -214,7 +211,6 @@
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
3 changes: 3 additions & 0 deletions ios/Classes/FlutterInappPurchasePlugin.m
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,9 @@ -(void)purchaseProcess:(SKPaymentTransaction *)transaction {
[requestedPayments removeObjectForKey:transaction.payment];
}
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];

// additionally send event
[self.channel invokeMethod:@"iap-purchase-event" arguments: purchase];
}

- (NSDictionary *)getPurchaseData:(SKPaymentTransaction *)transaction {
Expand Down
141 changes: 104 additions & 37 deletions lib/flutter_inapp_purchase.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ class FlutterInappPurchase {
'subs',
];

static StreamController<PurchasedItem> _purchaseController;
static StreamSubscription _purchaseSub;
static Stream<PurchasedItem> get onAdditionalSuccessPurchaseIOS => _purchaseController.stream;

/// Defining the [MethodChannel] for Flutter_Inapp_Purchase
static const MethodChannel _channel = const MethodChannel('flutter_inapp');

Expand Down Expand Up @@ -82,7 +86,7 @@ class FlutterInappPurchase {
if (skus == null || skus.contains(null)) return [];
skus = skus.toList();
if (Platform.isAndroid) {
dynamic result = await _channel.invokeMethod(
dynamic result = await _channel.invokeMethod<dynamic>(
'getItemsByType',
<String, dynamic>{
'type': _typeInApp[0],
Expand All @@ -92,7 +96,7 @@ class FlutterInappPurchase {

return extractItems(result);
} else if (Platform.isIOS) {
dynamic result = await _channel.invokeMethod(
dynamic result = await _channel.invokeMethod<dynamic>(
'getItems',
{
'skus': skus,
Expand All @@ -112,7 +116,7 @@ class FlutterInappPurchase {
if (skus == null || skus.contains(null)) return [];
skus = skus.toList();
if (Platform.isAndroid) {
dynamic result = await _channel.invokeMethod(
dynamic result = await _channel.invokeMethod<dynamic>(
'getItemsByType',
<String, dynamic>{
'type': _typeInApp[1],
Expand All @@ -122,7 +126,7 @@ class FlutterInappPurchase {

return extractItems(result);
} else if (Platform.isIOS) {
dynamic result = await _channel.invokeMethod(
dynamic result = await _channel.invokeMethod<dynamic>(
'getItems',
{
'skus': skus,
Expand All @@ -141,14 +145,14 @@ class FlutterInappPurchase {
/// Identical to [getAvailablePurchases] on `iOS`.
static Future<List<PurchasedItem>> getPurchaseHistory() async {
if (Platform.isAndroid) {
dynamic result1 = await _channel.invokeMethod(
dynamic result1 = await _channel.invokeMethod<dynamic>(
'getPurchaseHistoryByType',
<String, dynamic>{
'type': _typeInApp[0],
},
);

dynamic result2 = await _channel.invokeMethod(
dynamic result2 = await _channel.invokeMethod<dynamic>(
'getPurchaseHistoryByType',
<String, dynamic>{
'type': _typeInApp[1],
Expand All @@ -157,7 +161,8 @@ class FlutterInappPurchase {

return extractPurchased(result1) + extractPurchased(result2);
} else if (Platform.isIOS) {
dynamic result = await _channel.invokeMethod('getAvailableItems');
dynamic result =
await _channel.invokeMethod<dynamic>('getAvailableItems');

return extractPurchased(json.encode(result));
}
Expand All @@ -170,14 +175,14 @@ class FlutterInappPurchase {
/// This is identical to [getPurchaseHistory] on `iOS`
static Future<List<PurchasedItem>> getAvailablePurchases() async {
if (Platform.isAndroid) {
dynamic result1 = await _channel.invokeMethod(
dynamic result1 = await _channel.invokeMethod<dynamic>(
'getAvailableItemsByType',
<String, dynamic>{
'type': _typeInApp[0],
},
);

dynamic result2 = await _channel.invokeMethod(
dynamic result2 = await _channel.invokeMethod<dynamic>(
'getAvailableItemsByType',
<String, dynamic>{
'type': _typeInApp[1],
Expand All @@ -186,7 +191,8 @@ class FlutterInappPurchase {

return extractPurchased(result1) + extractPurchased(result2);
} else if (Platform.isIOS) {
dynamic result = await _channel.invokeMethod('getAvailableItems');
dynamic result =
await _channel.invokeMethod<dynamic>('getAvailableItems');

return extractPurchased(json.encode(result));
}
Expand All @@ -199,8 +205,8 @@ class FlutterInappPurchase {
/// Identical to [buySubscription] on `iOS`.
static Future<PurchasedItem> buyProduct(String sku) async {
if (Platform.isAndroid) {
dynamic result =
await _channel.invokeMethod('buyItemByType', <String, dynamic>{
dynamic result = await _channel
.invokeMethod<dynamic>('buyItemByType', <String, dynamic>{
'type': _typeInApp[0],
'sku': sku,
'oldSku': null, //TODO can this be removed?
Expand All @@ -211,15 +217,27 @@ class FlutterInappPurchase {

return item;
} else if (Platform.isIOS) {
dynamic result = await _channel
.invokeMethod('buyProductWithFinishTransaction', <String, dynamic>{
'sku': sku,
});
result = json.encode(result);

Map<String, dynamic> param = json.decode(result.toString());
PurchasedItem item = PurchasedItem.fromJSON(param);
return item;
try {
dynamic result = await _channel.invokeMethod<dynamic>(
'buyProductWithFinishTransaction', <String, dynamic>{
'sku': sku,
});
result = json.encode(result);

Map<String, dynamic> param = json.decode(result.toString());
PurchasedItem item = PurchasedItem.fromJSON(param);
return item;
} catch (err) {
print('Caused err. Set additionalSuccessPurchaseListenerIOS.');
print(err);
await _addAdditionalSuccessPurchaseListenerIOS();
_purchaseSub = onAdditionalSuccessPurchaseIOS.listen((data) {
_removePurchaseListener();
Map<String, dynamic> param = json.decode(data.toString());
PurchasedItem item = PurchasedItem.fromJSON(param);
return item;
});
}
}
throw PlatformException(
code: Platform.operatingSystem, message: "platform not supported");
Expand All @@ -233,8 +251,8 @@ class FlutterInappPurchase {
static Future<PurchasedItem> buySubscription(String sku,
{String oldSku}) async {
if (Platform.isAndroid) {
dynamic result =
await _channel.invokeMethod('buyItemByType', <String, dynamic>{
dynamic result = await _channel
.invokeMethod<dynamic>('buyItemByType', <String, dynamic>{
'type': _typeInApp[1],
'sku': sku,
'oldSku': oldSku,
Expand All @@ -244,15 +262,27 @@ class FlutterInappPurchase {
PurchasedItem item = PurchasedItem.fromJSON(param);
return item;
} else if (Platform.isIOS) {
dynamic result = await _channel
.invokeMethod('buyProductWithFinishTransaction', <String, dynamic>{
'sku': sku,
});
result = json.encode(result);

Map<String, dynamic> param = json.decode(result.toString());
PurchasedItem item = PurchasedItem.fromJSON(param);
return item;
try {
dynamic result = await _channel.invokeMethod<dynamic>(
'buyProductWithFinishTransaction', <String, dynamic>{
'sku': sku,
});
result = json.encode(result);

Map<String, dynamic> param = json.decode(result.toString());
PurchasedItem item = PurchasedItem.fromJSON(param);
return item;
} catch (err) {
print('Caused err. Set additionalSuccessPurchaseListenerIOS.');
print(err);
await _addAdditionalSuccessPurchaseListenerIOS();
_purchaseSub = onAdditionalSuccessPurchaseIOS.listen((data) {
_removePurchaseListener();
Map<String, dynamic> param = json.decode(data.toString());
PurchasedItem item = PurchasedItem.fromJSON(param);
return item;
});
}
}
throw PlatformException(
code: Platform.operatingSystem, message: "platform not supported");
Expand Down Expand Up @@ -297,11 +327,12 @@ class FlutterInappPurchase {
/// This allows you to perform server-side validation before finalizing the transaction on screen.
///
/// No effect on `Android`, who does not allow this type of functionality.
@deprecated
static Future<PurchasedItem> buyProductWithoutFinishTransaction(
String sku) async {
if (Platform.isAndroid) {
dynamic result =
await _channel.invokeMethod('buyItemByType', <String, dynamic>{
dynamic result = await _channel
.invokeMethod<dynamic>('buyItemByType', <String, dynamic>{
'type': _typeInApp[0],
'sku': sku,
'oldSku': null,
Expand All @@ -311,8 +342,8 @@ class FlutterInappPurchase {
PurchasedItem item = PurchasedItem.fromJSON(param);
return item;
} else if (Platform.isIOS) {
dynamic result = await _channel
.invokeMethod('buyProductWithoutFinishTransaction', <String, dynamic>{
dynamic result = await _channel.invokeMethod<dynamic>(
'buyProductWithoutFinishTransaction', <String, dynamic>{
'sku': sku,
});
result = json.encode(result);
Expand Down Expand Up @@ -348,7 +379,7 @@ class FlutterInappPurchase {
return List<IAPItem>();
} else if (Platform.isIOS) {
dynamic result =
await _channel.invokeMethod('getAppStoreInitiatedProducts');
await _channel.invokeMethod<dynamic>('getAppStoreInitiatedProducts');

return extractItems(json.encode(result));
}
Expand Down Expand Up @@ -459,4 +490,40 @@ class FlutterInappPurchase {
},
);
}

/// Add additional success purchase listener to iOS when purchase failed
///
/// In iOS, purchase could be failed randomly. See the reference: https://github.com/dooboolab/react-native-iap/issues/307
/// To make your purchase flow confidential, use below method. Checkout how this is used in `example` project.
static Future<void> _addAdditionalSuccessPurchaseListenerIOS() async {
if (Platform.isIOS) {
if (_purchaseController == null) {
_purchaseController = new StreamController.broadcast();
}
_channel.setMethodCallHandler((MethodCall call) {
switch (call.method) {
case "iap-purchase-event":
Map<String, dynamic> result = jsonDecode(call.arguments);
_purchaseController.add(new PurchasedItem.fromJSON(result));
_removePurchaseListener();
break;
default:
throw new ArgumentError('Unknown method ${call.method}');
}
});
}
}

static Future<void> _removePurchaseListener() async {
if (_purchaseSub != null) {
_purchaseSub.cancel();
_purchaseSub = null;
}
if (_purchaseController != null) {
_purchaseController
..add(null)
..close();
_purchaseController = null;
}
}
}
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ homepage: https://github.com/dooboolab/flutter_inapp_purchase/blob/master/pubspe
environment:
sdk: '>=1.20.1 <3.0.0'
dependencies:
http: '>=0.11.3+16 <1.0.0'
http: '>=0.12.0 <1.0.0'
flutter:
sdk: flutter

Expand Down

0 comments on commit d32ba9c

Please sign in to comment.