Skip to content

Commit e687a60

Browse files
Merge pull request #73 from qonversion/feature/promoPurchases
Add support for promo purchases on iOS
2 parents f997628 + a4e42d3 commit e687a60

File tree

8 files changed

+141
-39
lines changed

8 files changed

+141
-39
lines changed

android/src/main/kotlin/com/qonversion/flutter/sdk/qonversion_flutter_sdk/BaseListenerWrapper.kt

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,13 @@ package com.qonversion.flutter.sdk.qonversion_flutter_sdk
33
import io.flutter.plugin.common.BinaryMessenger
44
import io.flutter.plugin.common.EventChannel
55

6-
abstract class BaseListenerWrapper(
7-
val binaryMessenger: BinaryMessenger
6+
class BaseListenerWrapper internal constructor(
7+
private val binaryMessenger: BinaryMessenger,
8+
private val eventChannelPostfix: String
89
) {
910

10-
protected abstract val eventChannelPostfix: String
11-
12-
protected var eventChannel: EventChannel? = null
13-
protected var eventStreamHandler: BaseEventStreamHandler? = null
11+
private var eventChannel: EventChannel? = null
12+
var eventStreamHandler: BaseEventStreamHandler? = null
1413

1514
fun register() {
1615
eventStreamHandler =

android/src/main/kotlin/com/qonversion/flutter/sdk/qonversion_flutter_sdk/QonversionFlutterSdkPlugin.kt

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,10 @@ import com.qonversion.android.sdk.dto.offerings.QOfferings
1919
import com.qonversion.android.sdk.dto.products.QProduct
2020

2121
/** QonversionFlutterSdkPlugin */
22-
class QonversionFlutterSdkPlugin internal constructor(registrar: Registrar): MethodCallHandler, BaseListenerWrapper(registrar.messenger()) {
22+
class QonversionFlutterSdkPlugin internal constructor(registrar: Registrar): MethodCallHandler {
2323
private val activity: Activity = registrar.activity()
2424
private val application: Application = activity.application
25-
26-
override val eventChannelPostfix = "updated_purchases"
25+
private var deferredPurchasesStreamHandler: BaseEventStreamHandler? = null
2726

2827
companion object {
2928
@JvmStatic
@@ -32,8 +31,14 @@ class QonversionFlutterSdkPlugin internal constructor(registrar: Registrar): Met
3231
val instance = QonversionFlutterSdkPlugin(registrar)
3332
channel.setMethodCallHandler(instance)
3433

35-
// Register Updated Purchases Event Channel
36-
instance.register()
34+
// Register deferred purchases events
35+
val purchasesListener = BaseListenerWrapper(registrar.messenger(), "updated_purchases")
36+
purchasesListener.register()
37+
instance.deferredPurchasesStreamHandler = purchasesListener.eventStreamHandler
38+
39+
// Register promo purchases events. Android SDK does not generate any promo purchases yet
40+
val promoPurchasesListener = BaseListenerWrapper(registrar.messenger(), "promo_purchases")
41+
promoPurchasesListener.register()
3742
}
3843
}
3944

@@ -127,7 +132,7 @@ class QonversionFlutterSdkPlugin internal constructor(registrar: Registrar): Met
127132
override fun onPermissionsUpdate(permissions: Map<String, QPermission>) {
128133
val payload = Gson().toJson(permissions.mapValues { it.value.toMap() })
129134

130-
eventStreamHandler?.eventSink?.success(payload)
135+
deferredPurchasesStreamHandler?.eventSink?.success(payload)
131136
}
132137
})
133138
}

example/lib/products_view.dart

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,42 @@ class ProductsView extends StatefulWidget {
1111
class _ProductsViewState extends State<ProductsView> {
1212
var _products = <String, QProduct>{};
1313
QOfferings _offerings;
14-
StreamSubscription<Map<String, QPermission>> _subscription;
14+
StreamSubscription<Map<String, QPermission>> _deferredPurchasesStream;
15+
StreamSubscription<String> _promoPurchasesStream;
1516

1617
@override
1718
void initState() {
1819
super.initState();
1920
_loadProducts();
2021
_loadOfferings();
2122

22-
_subscription =
23+
_deferredPurchasesStream =
2324
Qonversion.updatedPurchasesStream.listen((event) => print(event));
25+
26+
_promoPurchasesStream =
27+
Qonversion.promoPurchasesStream.listen((promoPurchaseId) async {
28+
try {
29+
final permissions = await Qonversion.promoPurchase(promoPurchaseId);
30+
// Get Qonversion product by App Store ID
31+
final qProduct = _products.values.firstWhere(
32+
(element) => element.storeId == promoPurchaseId,
33+
orElse: () => null);
34+
// Get permission by Qonversion product
35+
final permission = permissions.values.firstWhere(
36+
(element) => element.productId == qProduct?.qonversionId,
37+
orElse: () => null);
38+
39+
print(permission?.isActive);
40+
} catch (e) {
41+
print(e);
42+
}
43+
});
2444
}
2545

2646
@override
2747
void dispose() {
28-
_subscription.cancel();
48+
_deferredPurchasesStream.cancel();
49+
_promoPurchasesStream.cancel();
2950

3051
super.dispose();
3152
}
@@ -108,8 +129,12 @@ class _ProductsViewState extends State<ProductsView> {
108129
color: Colors.blue,
109130
textColor: Colors.white,
110131
onPressed: () async {
111-
final res = await Qonversion.purchase(product.qonversionId);
112-
print(res[product.qonversionId]?.isActive);
132+
final permissions = await Qonversion.purchase(product.qonversionId);
133+
final permission = permissions.values.firstWhere(
134+
(element) => element.productId == product.qonversionId,
135+
orElse: () => null);
136+
137+
print(permission?.isActive);
113138
},
114139
),
115140
),

ios/Classes/FlutterError+Custom.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,10 @@ extension FlutterError {
7777
static let noSdkInfo = FlutterError(code: "15",
7878
message: "Could not find sdk info",
7979
details: passValidValue)
80+
81+
static func promoPurchaseError(_ productId: String) -> FlutterError {
82+
return FlutterError (code: "PromoPurchase",
83+
message: "Could not find completion block for Product ID: \(productId)",
84+
details: passValidValue)
85+
}
8086
}

ios/Classes/SwiftQonversionFlutterSdkPlugin.swift

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import Flutter
77
import Qonversion
88

99
public class SwiftQonversionFlutterSdkPlugin: NSObject, FlutterPlugin {
10-
var purchasesEventStreamHandler: BaseEventStreamHandler?
10+
var deferredPurchasesStreamHandler: BaseEventStreamHandler?
11+
var promoPurchasesStreamHandler: BaseEventStreamHandler?
12+
var promoPurchasesExecutionBlocks = [String: Qonversion.PromoPurchaseCompletionHandler]()
1113

1214
public static func register(with registrar: FlutterPluginRegistrar) {
1315
let messenger: FlutterBinaryMessenger
@@ -20,12 +22,15 @@ public class SwiftQonversionFlutterSdkPlugin: NSObject, FlutterPlugin {
2022
let instance = SwiftQonversionFlutterSdkPlugin()
2123
registrar.addMethodCallDelegate(instance, channel: channel)
2224

23-
// Register events listeners
25+
// Register deferred purchases events
2426
let purchasesListener = FlutterListenerWrapper<BaseEventStreamHandler>(registrar, postfix: "updated_purchases")
25-
purchasesListener.register() { instance.purchasesEventStreamHandler = $0 }
26-
27-
// Setting delegate as soon as plugin is registered
27+
purchasesListener.register() { instance.deferredPurchasesStreamHandler = $0 }
2828
Qonversion.setPurchasesDelegate(instance)
29+
30+
// Register promo purchases events
31+
let promoPurchasesListener = FlutterListenerWrapper<BaseEventStreamHandler>(registrar, postfix: "promo_purchases")
32+
promoPurchasesListener.register() { instance.promoPurchasesStreamHandler = $0 }
33+
Qonversion.setPromoPurchasesDelegate(instance)
2934
}
3035

3136
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
@@ -78,6 +83,9 @@ public class SwiftQonversionFlutterSdkPlugin: NSObject, FlutterPlugin {
7883
case "purchase":
7984
return purchase(args["productId"] as? String, result)
8085

86+
case "promoPurchase":
87+
return promoPurchase(args["productId"] as? String, result)
88+
8189
case "setUserId":
8290
return setUserId(args["userId"] as? String, result)
8391

@@ -154,6 +162,25 @@ public class SwiftQonversionFlutterSdkPlugin: NSObject, FlutterPlugin {
154162
}
155163
}
156164

165+
private func promoPurchase(_ productId: String?, _ result: @escaping FlutterResult) {
166+
guard let productId = productId else {
167+
return result(FlutterError.noProductId)
168+
}
169+
170+
if let executionBlock = promoPurchasesExecutionBlocks[productId] {
171+
promoPurchasesExecutionBlocks.removeValue(forKey: productId)
172+
173+
executionBlock { (permissions, error, isCancelled) in
174+
let purchaseResult = PurchaseResult(permissions: permissions,
175+
error: error,
176+
isCancelled: isCancelled)
177+
result(purchaseResult.toMap())
178+
}
179+
} else {
180+
result(FlutterError.promoPurchaseError(productId))
181+
}
182+
}
183+
157184
private func checkPermissions(_ result: @escaping FlutterResult) {
158185
Qonversion.checkPermissions { (permissions, error) in
159186
if let error = error {
@@ -295,6 +322,14 @@ extension SwiftQonversionFlutterSdkPlugin: Qonversion.PurchasesDelegate {
295322
public func qonversionDidReceiveUpdatedPermissions(_ permissions: [String : Qonversion.Permission]) {
296323
let payload = permissions.mapValues { $0.toMap() }.toJson()
297324

298-
purchasesEventStreamHandler?.eventSink?(payload)
325+
deferredPurchasesStreamHandler?.eventSink?(payload)
326+
}
327+
}
328+
329+
extension SwiftQonversionFlutterSdkPlugin: QNPromoPurchasesDelegate {
330+
public func shouldPurchasePromoProduct(withIdentifier productID: String, executionBlock: @escaping Qonversion.PromoPurchaseCompletionHandler) {
331+
promoPurchasesExecutionBlocks[productID] = executionBlock
332+
333+
promoPurchasesStreamHandler?.eventSink?(productID)
299334
}
300335
}

lib/src/constants.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class Constants {
2020
static const mLaunch = 'launch';
2121
static const mProducts = 'products';
2222
static const mPurchase = 'purchase';
23+
static const mPromoPurchase = 'promoPurchase';
2324
static const mUpdatePurchase = 'updatePurchase';
2425
static const mCheckPermissions = 'checkPermissions';
2526
static const mRestore = 'restore';

lib/src/qonversion.dart

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,13 @@ class Qonversion {
1919
static const String _sdkVersion = "3.0.1";
2020

2121
static const MethodChannel _channel = MethodChannel('qonversion_flutter_sdk');
22+
2223
static const _purchasesEventChannel =
2324
EventChannel('qonversion_flutter_updated_purchases');
2425

26+
static const _promoPurchasesEventChannel =
27+
EventChannel('qonversion_flutter_promo_purchases');
28+
2529
/// Yields an event each time a deferred transaction happens
2630
static Stream<Map<String, QPermission>> get updatedPurchasesStream =>
2731
_purchasesEventChannel
@@ -34,6 +38,12 @@ class Qonversion {
3438
.map((key, value) => MapEntry(key, QPermission.fromJson(value)));
3539
});
3640

41+
/// Yields an event each time a promo transaction happens on iOS.
42+
/// Returns App Store product ID
43+
static Stream<String> get promoPurchasesStream => _promoPurchasesEventChannel
44+
.receiveBroadcastStream()
45+
.cast<String>();
46+
3747
/// Initializes Qonversion SDK with the given API key.
3848
/// You can get one in your account on qonversion.io.
3949
static Future<QLaunchResult> launch(
@@ -94,17 +104,7 @@ class Qonversion {
94104
final rawResult = await _channel
95105
.invokeMethod(Constants.mPurchase, {Constants.kProductId: productId});
96106

97-
final resultMap = Map<String, dynamic>.from(rawResult);
98-
99-
final error = resultMap[Constants.kError];
100-
if (error != null) {
101-
throw QPurchaseException(
102-
error,
103-
isUserCancelled: resultMap[Constants.kIsCancelled] ?? false,
104-
);
105-
}
106-
107-
return QMapper.permissionsFromJson(resultMap[Constants.kPermissions]);
107+
return _handlePurchaseResult(rawResult);
108108
}
109109

110110
/// Android only. Returns `null` if called on iOS.
@@ -130,6 +130,22 @@ class Qonversion {
130130
return QMapper.permissionsFromJson(rawResult);
131131
}
132132

133+
/// iOS only. Returns `null` if called on Android.
134+
/// Starts a promo purchase process with App Store [productId].
135+
///
136+
/// Throws `QPurchaseException` in case of error in purchase flow.
137+
static Future<Map<String, QPermission>?> promoPurchase(
138+
String productId) async {
139+
if (!Platform.isIOS) {
140+
return null;
141+
}
142+
143+
final rawResult = await _channel.invokeMethod(
144+
Constants.mPromoPurchase, {Constants.kProductId: productId});
145+
146+
return _handlePurchaseResult(rawResult);
147+
}
148+
133149
/// You need to call the checkPermissions method at the start of your app to check if a user has the required permission.
134150
///
135151
/// This method will check the user receipt and will return the current permissions.
@@ -244,4 +260,19 @@ class Qonversion {
244260
"source": "flutter",
245261
"sourceKey": Constants.sourceKey
246262
});
263+
264+
static Map<String, QPermission> _handlePurchaseResult(
265+
Map<dynamic, dynamic> rawResult) {
266+
final resultMap = Map<String, dynamic>.from(rawResult);
267+
268+
final error = resultMap[Constants.kError];
269+
if (error != null) {
270+
throw QPurchaseException(
271+
error,
272+
isUserCancelled: resultMap[Constants.kIsCancelled] ?? false,
273+
);
274+
}
275+
276+
return QMapper.permissionsFromJson(resultMap[Constants.kPermissions]);
277+
}
247278
}

pubspec.lock

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ packages:
2121
name: args
2222
url: "https://pub.dartlang.org"
2323
source: hosted
24-
version: "2.0.0"
24+
version: "2.1.0"
2525
async:
2626
dependency: transitive
2727
description:
@@ -91,7 +91,7 @@ packages:
9191
name: built_value
9292
url: "https://pub.dartlang.org"
9393
source: hosted
94-
version: "8.0.5"
94+
version: "8.0.6"
9595
characters:
9696
dependency: transitive
9797
description:
@@ -161,7 +161,7 @@ packages:
161161
name: dart_style
162162
url: "https://pub.dartlang.org"
163163
source: hosted
164-
version: "2.0.0"
164+
version: "2.0.1"
165165
fake_async:
166166
dependency: transitive
167167
description:
@@ -325,7 +325,7 @@ packages:
325325
name: shelf
326326
url: "https://pub.dartlang.org"
327327
source: hosted
328-
version: "1.1.0"
328+
version: "1.1.4"
329329
shelf_web_socket:
330330
dependency: transitive
331331
description:
@@ -428,7 +428,7 @@ packages:
428428
name: web_socket_channel
429429
url: "https://pub.dartlang.org"
430430
source: hosted
431-
version: "2.0.0"
431+
version: "2.1.0"
432432
yaml:
433433
dependency: transitive
434434
description:

0 commit comments

Comments
 (0)