Skip to content

Commit 9b9c68a

Browse files
Merge pull request #348 from qonversion/release/9.1.0
Release 9.1.0
2 parents 8f5b4f2 + 57e9249 commit 9b9c68a

16 files changed

+191
-32
lines changed

Diff for: CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 9.1.0
2+
* Added option to set context keys, quantity and other options for purchases. Context keys will allow you to associate the purchase with remote configuration if the product info was loaded from there.
3+
* Deprecated old purchase functions. Use new one instead.
4+
15
## 9.0.2
26
* Fixed bug with `checkEntitlements` calls on Android when the callback might not have been called after subscription state changes during the app session.
37

Diff for: android/build.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,6 @@ android {
5151

5252
dependencies {
5353
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
54-
implementation "io.qonversion.sandwich:sandwich:5.0.4"
54+
implementation "io.qonversion.sandwich:sandwich:5.1.2"
5555
implementation 'com.google.code.gson:gson:2.9.0'
5656
}

Diff for: android/src/main/kotlin/com/qonversion/flutter/sdk/qonversion_flutter_sdk/QonversionPlugin.kt

+11-11
Original file line numberDiff line numberDiff line change
@@ -181,29 +181,29 @@ class QonversionPlugin : MethodCallHandler, FlutterPlugin, ActivityAware {
181181

182182
private fun purchase(args: Map<String, Any>, result: Result) {
183183
val productId = args["productId"] as? String ?: return result.noNecessaryDataError()
184-
val offerId = args["offerId"] as? String
185-
val applyOffer = args["applyOffer"] as? Boolean
186-
187-
qonversionSandwich.purchase(productId, offerId, applyOffer, result.toJsonResultListener())
188-
}
189-
190-
private fun updatePurchase(args: Map<String, Any>, result: Result) {
191-
val newProductId = args["newProductId"] as? String ?: return result.noNecessaryDataError()
192-
val oldProductId = args["oldProductId"] as? String ?: return result.noNecessaryDataError()
184+
val oldProductId = args["oldProductId"] as? String
193185
val offerId = args["offerId"] as? String
194186
val applyOffer = args["applyOffer"] as? Boolean
195187
val updatePolicyKey = args["updatePolicyKey"] as? String
196188

197-
qonversionSandwich.updatePurchase(
198-
newProductId,
189+
@Suppress("UNCHECKED_CAST")
190+
val contextKeys = args["contextKeys"] as? List<String>
191+
192+
qonversionSandwich.purchase(
193+
productId,
199194
offerId,
200195
applyOffer,
201196
oldProductId,
202197
updatePolicyKey,
198+
contextKeys,
203199
result.toJsonResultListener()
204200
)
205201
}
206202

203+
private fun updatePurchase(args: Map<String, Any>, result: Result) {
204+
purchase(args, result)
205+
}
206+
207207
private fun checkEntitlements(result: Result) {
208208
qonversionSandwich.checkEntitlements(result.toJsonResultListener())
209209
}

Diff for: example/ios/Runner.xcodeproj/project.pbxproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
archiveVersion = 1;
44
classes = {
55
};
6-
objectVersion = 46;
6+
objectVersion = 50;
77
objects = {
88

99
/* Begin PBXBuildFile section */

Diff for: ios/Classes/SwiftQonversionPlugin.swift

+13-6
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ public class SwiftQonversionPlugin: NSObject, FlutterPlugin {
109109
return initialize(args, result)
110110

111111
case "purchase":
112-
return purchase(args["productId"] as? String, result)
112+
return purchase(args["productId"] as? String, quantity: args["quantity"] as? Int, contextKeys: args["contextKeys"] as? [String], result)
113113

114114
case "promoPurchase":
115115
return promoPurchase(args["productId"] as? String, result)
@@ -207,13 +207,16 @@ public class SwiftQonversionPlugin: NSObject, FlutterPlugin {
207207
private func products(_ result: @escaping FlutterResult) {
208208
qonversionSandwich?.products(getDefaultCompletion(result))
209209
}
210-
211-
private func purchase(_ productId: String?, _ result: @escaping FlutterResult) {
210+
211+
private func purchase(_ productId: String?, quantity: Int?, contextKeys: [String]?, _ result: @escaping FlutterResult) {
212212
guard let productId = productId else {
213213
return result(FlutterError.noNecessaryData)
214214
}
215+
216+
let contextKeys = contextKeys ?? []
217+
let quantity = quantity ?? 1
215218

216-
qonversionSandwich?.purchase(productId, completion: getJsonCompletion(result))
219+
qonversionSandwich?.purchase(productId, quantity:quantity, contextKeys:contextKeys, completion: getJsonCompletion(result))
217220
}
218221

219222
private func promoPurchase(_ productId: String?, _ result: @escaping FlutterResult) {
@@ -405,10 +408,14 @@ extension SwiftQonversionPlugin: QonversionEventListener {
405408
guard let jsonData = entitlements.toJson() else {
406409
return
407410
}
408-
updatedEntitlementsStreamHandler?.eventSink?(jsonData)
411+
DispatchQueue.main.async {
412+
self.updatedEntitlementsStreamHandler?.eventSink?(jsonData)
413+
}
409414
}
410415

411416
public func shouldPurchasePromoProduct(with productId: String) {
412-
promoPurchasesStreamHandler?.eventSink?(productId)
417+
DispatchQueue.main.async {
418+
self.promoPurchasesStreamHandler?.eventSink?(productId)
419+
}
413420
}
414421
}

Diff for: ios/qonversion_flutter.podspec

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Pod::Spec.new do |s|
1616
s.source_files = 'Classes/**/*'
1717
s.dependency 'Flutter'
1818
s.platform = :ios, '9.0'
19-
s.dependency "QonversionSandwich", "5.0.4"
19+
s.dependency "QonversionSandwich", "5.1.2"
2020

2121
# Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported.
2222
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' }

Diff for: lib/qonversion_flutter.dart

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export 'src/dto/offerings.dart';
1515
export 'src/dto/product.dart';
1616
export 'src/dto/product_type.dart';
1717
export 'src/dto/purchase_model.dart';
18+
export 'src/dto/purchase_options.dart';
19+
export 'src/dto/purchase_options_builder.dart';
1820
export 'src/dto/purchase_update_model.dart';
1921
export 'src/dto/purchase_update_policy.dart';
2022
export 'src/dto/purchase_exception.dart';

Diff for: lib/src/dto/purchase_options.dart

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import 'product.dart';
2+
import 'purchase_update_policy.dart';
3+
import 'purchase_options_builder.dart';
4+
5+
/// Purchase options that may be used to modify purchase process.
6+
/// To create an instance, use [QPurchaseOptionsBuilder] class.
7+
class QPurchaseOptions {
8+
final String? offerId;
9+
final bool applyOffer;
10+
final QProduct? oldProduct;
11+
final QPurchaseUpdatePolicy? updatePolicy;
12+
final List<String>? contextKeys;
13+
final int quantity;
14+
15+
QPurchaseOptions(this.offerId, this.applyOffer, this.oldProduct, this.updatePolicy, this.contextKeys, this.quantity);
16+
}

Diff for: lib/src/dto/purchase_options_builder.dart

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import 'product.dart';
2+
import 'purchase_options.dart';
3+
import 'purchase_update_policy.dart';
4+
import 'store_product/product_offer_details.dart';
5+
import 'store_product/product_store_details.dart';
6+
7+
class QPurchaseOptionsBuilder {
8+
String? _offerId;
9+
bool _applyOffer = true;
10+
QProduct? _oldProduct;
11+
QPurchaseUpdatePolicy? _updatePolicy;
12+
List<String>? _contextKeys;
13+
int _quantity = 1;
14+
15+
/// Android only.
16+
/// Set the offer to the purchase.
17+
/// If [offer] is not specified, then the default offer will be applied. To know how we choose
18+
/// the default offer, see [QProductStoreDetails.defaultSubscriptionOfferDetails].
19+
///
20+
/// [offer] concrete offer which you'd like to purchase.
21+
/// Returns builder instance for chain calls.
22+
QPurchaseOptionsBuilder setOffer(QProductOfferDetails offer) {
23+
_offerId = offer.offerId;
24+
return this;
25+
}
26+
27+
/// Android only.
28+
/// Set the offer Id to the purchase.
29+
/// If [offerId] is not specified, then the default offer will be applied. To know how we choose
30+
/// the default offer, see [QProductStoreDetails.defaultSubscriptionOfferDetails].
31+
///
32+
/// [offerId] concrete offer Id which you'd like to purchase.
33+
/// Returns builder instance for chain calls.
34+
QPurchaseOptionsBuilder setOfferId(String offerId) {
35+
_offerId = offerId;
36+
return this;
37+
}
38+
39+
/// Android only.
40+
/// Call this function to remove any intro/trial offer from the purchase (use only a bare base plan).
41+
/// Returns builder instance for chain calls.
42+
QPurchaseOptionsBuilder removeOffer() {
43+
_applyOffer = false;
44+
return this;
45+
}
46+
47+
/// Android only.
48+
/// Set Qonversion product from which the upgrade/downgrade
49+
/// will be initialized.
50+
///
51+
/// [oldProduct] Qonversion product from which the upgrade/downgrade
52+
/// will be initialized.
53+
/// Returns builder instance for chain calls.
54+
QPurchaseOptionsBuilder setOldProduct(QProduct oldProduct) {
55+
_oldProduct = oldProduct;
56+
return this;
57+
}
58+
59+
/// Android only.
60+
/// Set the update policy for the purchase.
61+
/// If the [updatePolicy] is not provided, then default one
62+
/// will be selected - [QPurchaseUpdatePolicy.withTimeProration].
63+
/// [updatePolicy] update policy for the purchase.
64+
/// Returns builder instance for chain calls.
65+
QPurchaseOptionsBuilder setUpdatePolicy(QPurchaseUpdatePolicy updatePolicy) {
66+
_updatePolicy = updatePolicy;
67+
return this;
68+
}
69+
70+
/// Set the context keys associated with a purchase.
71+
///
72+
/// [contextKeys] context keys for the purchase.
73+
/// Returns builder instance for chain calls.
74+
QPurchaseOptionsBuilder setContextKeys(List<String> contextKeys) {
75+
_contextKeys = contextKeys;
76+
return this;
77+
}
78+
79+
/// iOS only.
80+
/// Set quantity of product purchasing. Use for consumable in-app products.
81+
///
82+
/// [quantity] quantity of product purchasing.
83+
/// Returns builder instance for chain calls.
84+
QPurchaseOptionsBuilder setQuantity(int quantity) {
85+
_quantity = quantity;
86+
return this;
87+
}
88+
89+
/// Generate [QPurchaseOptions] instance with all the provided options.
90+
///
91+
/// Returns the complete [QPurchaseOptions] instance.
92+
QPurchaseOptions build() {
93+
return QPurchaseOptions(_offerId, _applyOffer, _oldProduct, _updatePolicy, _contextKeys, _quantity);
94+
}
95+
96+
}

Diff for: lib/src/internal/constants.dart

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ class Constants {
1414
static const kOfferId = 'offerId';
1515
static const kApplyOffer = 'applyOffer';
1616
static const kOfferingId = 'offeringId';
17-
static const kNewProductId = 'newProductId';
1817
static const kOldProductId = 'oldProductId';
1918
static const kUpdatePolicyKey = 'updatePolicyKey';
2019
static const kError = 'error';
@@ -32,6 +31,8 @@ class Constants {
3231
static const kRemoteConfigurationId = 'remoteConfigurationId';
3332
static const kContextKey = 'contextKey';
3433
static const kContextKeys = 'contextKeys';
34+
static const kPurchaseContextKeys = 'contextKeys';
35+
static const kPurchaseQuantity = 'quantity';
3536
static const kIncludeEmptyContextKey = 'includeEmptyContextKey';
3637

3738
// MethodChannel methods names

Diff for: lib/src/internal/qonversion_internal.dart

+23-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import 'package:qonversion_flutter/src/internal/utils/string.dart';
1111
import 'constants.dart';
1212

1313
class QonversionInternal implements Qonversion {
14-
static const String _sdkVersion = "9.0.2";
14+
static const String _sdkVersion = "9.1.0";
1515

1616
final MethodChannel _channel = MethodChannel('qonversion_plugin');
1717

@@ -79,6 +79,27 @@ class QonversionInternal implements Qonversion {
7979
}
8080
}
8181

82+
@override
83+
Future<Map<String, QEntitlement>> purchaseProduct(QProduct product, QPurchaseOptions purchaseOptions) async {
84+
try {
85+
final rawResult = await _channel
86+
.invokeMethod(Constants.mPurchase, {
87+
Constants.kProductId: product.qonversionId,
88+
Constants.kOldProductId: purchaseOptions.oldProduct?.qonversionId,
89+
Constants.kOfferId: purchaseOptions.offerId,
90+
Constants.kApplyOffer: purchaseOptions.applyOffer,
91+
Constants.kUpdatePolicyKey: purchaseOptions.updatePolicy,
92+
Constants.kPurchaseContextKeys: purchaseOptions.contextKeys,
93+
Constants.kPurchaseQuantity: purchaseOptions.quantity,
94+
});
95+
96+
final result = QMapper.entitlementsFromJson(rawResult);
97+
return result;
98+
} on PlatformException catch (e) {
99+
throw _convertPurchaseException(e);
100+
}
101+
}
102+
82103
@override
83104
Future<Map<String, QEntitlement>?> updatePurchase(QPurchaseUpdateModel purchaseUpdateModel) async {
84105
if (!Platform.isAndroid) {
@@ -89,7 +110,7 @@ class QonversionInternal implements Qonversion {
89110
final updatePolicy = purchaseUpdateModel.updatePolicy;
90111

91112
final rawResult = await _channel.invokeMethod(Constants.mUpdatePurchase, {
92-
Constants.kNewProductId: purchaseUpdateModel.productId,
113+
Constants.kProductId: purchaseUpdateModel.productId,
93114
Constants.kOfferId: purchaseUpdateModel.offerId,
94115
Constants.kApplyOffer: purchaseUpdateModel.applyOffer,
95116
Constants.kOldProductId: purchaseUpdateModel.oldProductId,

Diff for: lib/src/qonversion.dart

+7
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@ abstract class Qonversion {
6060
/// See [Making Purchases](https://documentation.qonversion.io/docs/making-purchases)
6161
Future<Map<String, QEntitlement>> purchase(QPurchaseModel purchaseModel);
6262

63+
/// Make a purchase and validate it through server-to-server using Qonversion's Backend
64+
/// [product] product to purchase.
65+
/// [options] additional options for the purchase process.
66+
/// Returns the promise with the user entitlements including the ones obtained by the purchase.
67+
/// Throws [QPurchaseException] in case of error in purchase flow.
68+
Future<Map<String, QEntitlement>> purchaseProduct(QProduct product, QPurchaseOptions purchaseOptions);
69+
6370
/// Android only. Returns `null` if called on iOS.
6471
///
6572
/// Update (upgrade/downgrade) subscription on Google Play Store and validate it through server-to-server using Qonversion's Backend

Diff for: macos/Classes/SwiftQonversionPlugin.swift

+11-6
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public class SwiftQonversionPlugin: NSObject, FlutterPlugin {
9090
return initialize(args, result)
9191

9292
case "purchase":
93-
return purchase(args["productId"] as? String, result)
93+
return purchase(args["productId"] as? String, quantity: args["quantity"] as? Int, contextKeys: args["contextKeys"] as? [String], result)
9494

9595
case "remoteConfig":
9696
return remoteConfig(args["contextKey"] as? String, result)
@@ -165,13 +165,16 @@ public class SwiftQonversionPlugin: NSObject, FlutterPlugin {
165165
private func products(_ result: @escaping FlutterResult) {
166166
qonversionSandwich?.products(getDefaultCompletion(result))
167167
}
168-
169-
private func purchase(_ productId: String?, _ result: @escaping FlutterResult) {
168+
169+
private func purchase(_ productId: String?, quantity: Int?, contextKeys: [String]?, _ result: @escaping FlutterResult) {
170170
guard let productId = productId else {
171171
return result(FlutterError.noNecessaryData)
172172
}
173-
174-
qonversionSandwich?.purchase(productId, completion: getJsonCompletion(result))
173+
174+
let contextKeys = contextKeys ?? []
175+
let quantity = quantity ?? 1
176+
177+
qonversionSandwich?.purchase(productId, quantity:quantity, contextKeys:contextKeys, completion: getJsonCompletion(result))
175178
}
176179

177180
private func checkEntitlements(_ result: @escaping FlutterResult) {
@@ -343,7 +346,9 @@ extension SwiftQonversionPlugin: QonversionEventListener {
343346
guard let jsonData = entitlements.toJson() else {
344347
return
345348
}
346-
updatedEntitlementsStreamHandler?.eventSink?(jsonData)
349+
DispatchQueue.main.async {
350+
self.updatedEntitlementsStreamHandler?.eventSink?(jsonData)
351+
}
347352
}
348353

349354
public func shouldPurchasePromoProduct(with productId: String) {

Diff for: macos/qonversion_flutter.podspec

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Pod::Spec.new do |s|
1616
s.source_files = 'Classes/**/*'
1717
s.dependency 'FlutterMacOS'
1818
s.platform = :osx, '10.12'
19-
s.dependency "QonversionSandwich", "5.0.4"
19+
s.dependency "QonversionSandwich", "5.1.2"
2020

2121
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
2222
s.swift_version = '5.0'

Diff for: pubspec.lock

+1-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ packages:
9191
name: built_value
9292
url: "https://pub.dartlang.org"
9393
source: hosted
94-
version: "8.6.1"
94+
version: "8.8.0"
9595
characters:
9696
dependency: transitive
9797
description:

Diff for: pubspec.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: qonversion_flutter
22
description: Flutter plugin to implement in-app subscriptions and purchases. Validate user receipts and manage cross-platform access to paid content on your app. Android & iOS.
3-
version: 9.0.2
3+
version: 9.1.0
44
homepage: 'https://qonversion.io'
55
repository: 'https://github.com/qonversion/flutter-sdk'
66

0 commit comments

Comments
 (0)