Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add additional success purchase listener ios #58

Merged
merged 4 commits into from
Jan 29, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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