diff --git a/android/build.gradle b/android/build.gradle index 9359e6a2..2c050adb 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -41,6 +41,6 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'io.qonversion.sandwich:sandwich:0.0.11' + implementation 'io.qonversion.sandwich:sandwich:0.0.14' implementation 'com.google.code.gson:gson:2.8.6' } diff --git a/android/src/main/kotlin/com/qonversion/flutter/sdk/qonversion_flutter_sdk/QonversionFlutterSdkPlugin.kt b/android/src/main/kotlin/com/qonversion/flutter/sdk/qonversion_flutter_sdk/QonversionFlutterSdkPlugin.kt index c0bb5553..dbbf389d 100644 --- a/android/src/main/kotlin/com/qonversion/flutter/sdk/qonversion_flutter_sdk/QonversionFlutterSdkPlugin.kt +++ b/android/src/main/kotlin/com/qonversion/flutter/sdk/qonversion_flutter_sdk/QonversionFlutterSdkPlugin.kt @@ -4,14 +4,6 @@ import android.app.Activity import android.app.Application import androidx.annotation.NonNull import com.google.gson.Gson -import com.google.gson.JsonSyntaxException -import com.qonversion.android.sdk.* -import com.qonversion.android.sdk.dto.QLaunchResult -import com.qonversion.android.sdk.dto.QPermission -import com.qonversion.android.sdk.dto.QPermissionsCacheLifetime -import com.qonversion.android.sdk.dto.eligibility.QEligibility -import com.qonversion.android.sdk.dto.offerings.QOfferings -import com.qonversion.android.sdk.dto.products.QProduct import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding @@ -128,8 +120,8 @@ class QonversionFlutterSdkPlugin : MethodCallHandler, FlutterPlugin, ActivityAwa "purchaseProduct" -> purchaseProduct(args, result) "updatePurchase" -> updatePurchase(args, result) "updatePurchaseWithProduct" -> updatePurchaseWithProduct(args, result) - "setProperty" -> setProperty(args, result) - "setUserProperty" -> setUserProperty(args, result) + "setDefinedUserProperty" -> setDefinedUserProperty(args, result) + "setCustomUserProperty" -> setCustomUserProperty(args, result) "addAttributionData" -> addAttributionData(args, result) "checkTrialIntroEligibility" -> checkTrialIntroEligibility(args, result) "storeSdkInfo" -> storeSdkInfo(args, result) @@ -221,7 +213,7 @@ class QonversionFlutterSdkPlugin : MethodCallHandler, FlutterPlugin, ActivityAwa qonversionSandwich.products(result.toResultListener()) } - private fun setProperty(args: Map, result: Result) { + private fun setDefinedUserProperty(args: Map, result: Result) { val rawProperty = args["property"] as? String ?: return result.noProperty() val value = args["value"] as? String ?: return result.noPropertyValue() @@ -229,7 +221,7 @@ class QonversionFlutterSdkPlugin : MethodCallHandler, FlutterPlugin, ActivityAwa result.success(null) } - private fun setUserProperty(args: Map, result: Result) { + private fun setCustomUserProperty(args: Map, result: Result) { val property = args["property"] as? String ?: return result.noProperty() val value = args["value"] as? String ?: return result.noPropertyValue() diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 0d1cc586..e941e1d4 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -276,6 +276,7 @@ "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", "${BUILT_PRODUCTS_DIR}/PromisesObjC/FBLPromises.framework", "${BUILT_PRODUCTS_DIR}/Qonversion/Qonversion.framework", + "${BUILT_PRODUCTS_DIR}/QonversionSandwich/QonversionSandwich.framework", "${BUILT_PRODUCTS_DIR}/flutter_local_notifications/flutter_local_notifications.framework", "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", "${BUILT_PRODUCTS_DIR}/qonversion_flutter/qonversion_flutter.framework", @@ -290,6 +291,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBLPromises.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Qonversion.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/QonversionSandwich.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_local_notifications.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/qonversion_flutter.framework", @@ -400,7 +402,10 @@ ); INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -537,7 +542,10 @@ ); INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -569,7 +577,10 @@ ); INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", diff --git a/ios/Classes/AutomationsPlugin.swift b/ios/Classes/AutomationsPlugin.swift index 6c8cc386..517843a2 100644 --- a/ios/Classes/AutomationsPlugin.swift +++ b/ios/Classes/AutomationsPlugin.swift @@ -5,7 +5,7 @@ // Created by Maria on 18.11.2021. // -import Qonversion +import QonversionSandwich public class AutomationsPlugin: NSObject { private let eventChannelShownScreens = "shown_screens" @@ -20,6 +20,8 @@ public class AutomationsPlugin: NSObject { private var finishedActionsStreamHandler: BaseEventStreamHandler? private var finishedAutomationsStreamHandler: BaseEventStreamHandler? + private var automationSandwich = AutomationsSandwich() + public func register(_ registrar: FlutterPluginRegistrar) { let shownScreensListener = FlutterListenerWrapper(registrar, postfix: eventChannelShownScreens) shownScreensListener.register() { self.shownScreensStreamHandler = $0 } @@ -36,31 +38,29 @@ public class AutomationsPlugin: NSObject { let finishedAutomationsListener = FlutterListenerWrapper(registrar, postfix: eventChannelFinishedAutomations) finishedAutomationsListener.register() { self.finishedAutomationsStreamHandler = $0 } - Qonversion.Automations.setDelegate(self) + automationSandwich.subscribe(self) } } -extension AutomationsPlugin: Qonversion.AutomationsDelegate { - public func automationsDidShowScreen(_ screenID: String) { - shownScreensStreamHandler?.eventSink?(screenID) - } - - public func automationsDidStartExecuting(actionResult: Qonversion.ActionResult) { - let payload = actionResult.toMap().toJson() - startedActionsStreamHandler?.eventSink?(payload) - } - - public func automationsDidFailExecuting(actionResult: Qonversion.ActionResult) { - let payload = actionResult.toMap().toJson() - failedActionsStreamHandler?.eventSink?(payload) - } - - public func automationsDidFinishExecuting(actionResult: Qonversion.ActionResult) { - let payload = actionResult.toMap().toJson() - finishedActionsStreamHandler?.eventSink?(payload) - } +extension AutomationsPlugin: AutomationsEventListener { - public func automationsFinished() { - finishedAutomationsStreamHandler?.eventSink?(nil) + public func automationDidTrigger(event: String, payload: [String: Any]?) { + guard let resultEvent = AutomationsEvent(rawValue: event) else { return } + + let handler: BaseEventStreamHandler? + switch (resultEvent) { + case .screenShown: + handler = shownScreensStreamHandler + case .actionStarted: + handler = startedActionsStreamHandler + case .actionFailed: + handler = failedActionsStreamHandler + case .actionFinished: + handler = finishedActionsStreamHandler + case .automationsFinished: + handler = finishedAutomationsStreamHandler + } + + handler?.eventSink?(payload?.toJson()) } } diff --git a/ios/Classes/Constants.swift b/ios/Classes/Constants.swift deleted file mode 100644 index aaa50652..00000000 --- a/ios/Classes/Constants.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// Constants.swift -// qonversion_flutter -// -// Created by Maria on 30.06.2021. -// - -struct ProductFields { - static let id = "id" - static let storeId = "store_id" - static let type = "type" - static let duration = "duration" - static let skProduct = "sk_product" - static let prettyPrice = "pretty_price" - static let trialDuration = "trial_duration" - static let offeringId = "offering_id" -} diff --git a/ios/Classes/Extensions.swift b/ios/Classes/Extensions.swift new file mode 100644 index 00000000..fded152f --- /dev/null +++ b/ios/Classes/Extensions.swift @@ -0,0 +1,19 @@ +// +// Extensions.swift +// qonversion_flutter +// +// Created by Kamo Spertsyan on 05.09.2022. +// + +import Foundation + +extension Dictionary { + func toJson() -> String? { + guard let jsonData = try? JSONSerialization.data(withJSONObject: self, + options: []) else { + return nil + } + + return String(data: jsonData, encoding: .utf8) + } +} diff --git a/ios/Classes/FlutterError+Custom.swift b/ios/Classes/FlutterError+Custom.swift index bcf779ca..66f0f472 100644 --- a/ios/Classes/FlutterError+Custom.swift +++ b/ios/Classes/FlutterError+Custom.swift @@ -11,6 +11,8 @@ import FlutterMacOS import Flutter #endif +import QonversionSandwich + extension FlutterError { static private let passValidValue = "Please make sure you pass a valid value" @@ -26,10 +28,6 @@ extension FlutterError { message: "Could not find userID", details: passValidValue) - static let noAutoTrackPurchases = FlutterError(code: "3", - message: "Could not find autoTrackPurchases boolean value", - details: passValidValue) - static let noData = FlutterError(code: "4", message: "Could not find data", details: passValidValue) @@ -38,26 +36,22 @@ extension FlutterError { message: "Could not find provider", details: passValidValue) - static func failedToGetProducts(_ error: NSError) -> FlutterError { - return mapQonversionError(error, errorCode: "7", errorMessage: "Failed to get products") + static func failedToGetProducts(_ error: SandwichError) -> FlutterError { + return mapSandwichError(error, errorCode: "7", errorMessage: "Failed to get products") } static let noProductId = FlutterError(code: "8", message: "Could not find productId value", details: "Please provide valid productId") - - static let noProduct = FlutterError(code: "ProductNotProvided", - message: "Could not find product", - details: "Please provide a valid product") - - static func qonversionError(_ error: NSError) -> FlutterError { - return mapQonversionError(error, errorCode: "9") + + static func sandwichError(_ error: SandwichError) -> FlutterError { + return mapSandwichError(error, errorCode: "9") } - static func parsingError(_ description: String) -> FlutterError { - return FlutterError(code: "12", - message: "Arguments Parsing Error", - details: description) + static func purchaseError(_ error: SandwichError) -> FlutterError { + let isCancelled = error.additionalInfo["isCancelled"] as? Bool ?? false + let code = isCancelled ? "PurchaseCancelledByUser" : "9" + return mapSandwichError(error, errorCode: code) } static let noProperty = FlutterError(code: "13", @@ -67,11 +61,7 @@ extension FlutterError { static let noPropertyValue = FlutterError(code: "14", message: "Could not find property value", details: passValidValue) - - static func offeringsError(_ error: NSError) -> FlutterError { - return mapQonversionError(error, errorCode: "Offerings", errorMessage: "Could not get offerings") - } - + static let noSdkInfo = FlutterError(code: "15", message: "Could not find sdk info", details: passValidValue) @@ -79,37 +69,26 @@ extension FlutterError { static let noLifetime = FlutterError(code: "16", message: "Could not find lifetime", details: passValidValue) - - static func promoPurchaseError(_ productId: String) -> FlutterError { - return FlutterError (code: "PromoPurchase", - message: "Could not find completion block for Product ID: \(productId)", - details: passValidValue) - } - static func jsonSerializationError(_ description: String) -> FlutterError { - return FlutterError(code: "JSONSerialization", - message: "JSON Serialization Error", - details: description) - - } + static let noOfferingId = FlutterError(code: "17", + message: "Could not find offeringId value", + details: "Please provide valid offeringId") - static func noProductIdField(_ description: String) -> FlutterError { - return FlutterError(code: "NoProductIdField", - message: "Could not find qonversionId in Product", - details: description) - } + static let serializationError = FlutterError(code: "18", + message: "Failed to serialize response from native bridge", + details: "") - private static func mapQonversionError(_ error: NSError, errorCode: String, errorMessage: String? = nil) -> FlutterError { + private static func mapSandwichError(_ error: SandwichError, errorCode: String, errorMessage: String? = nil) -> FlutterError { var message = "" if let errorMessage = errorMessage { message = errorMessage + ". " } - message += error.localizedDescription + message += error.details var details = "Qonversion Error Code: \(error.code)" - if let additionalMessage = error.userInfo[NSDebugDescriptionErrorKey] { + if let additionalMessage = error.additionalMessage { details = "\(details). Additional Message: \(additionalMessage)" } diff --git a/ios/Classes/Mapper.swift b/ios/Classes/Mapper.swift deleted file mode 100644 index 9d5e2356..00000000 --- a/ios/Classes/Mapper.swift +++ /dev/null @@ -1,248 +0,0 @@ -// -// Mapper.swift -// qonversion_flutter -// -// Created by Ilya Virnik on 11/22/20. -// - -import Qonversion - -enum ParsingError: Error { - case runtimeError(String) -} - -struct PurchaseResult { - let permissions: [String : Qonversion.Permission] - let error: NSError? - let isCancelled: Bool - - func toMap() -> [String: Any?] { - return [ - "permissions": permissions.mapValues { $0.toMap() }, - "error": error?.toMap(), - "is_cancelled": isCancelled, - ] - } -} - -extension NSError { - func toMap() -> [String: Any?] { - let errorMap = [ - "code": code, - "description": localizedDescription, - "additionalMessage": userInfo[NSDebugDescriptionErrorKey]] - - return errorMap - } -} - -extension Date { - func toMilliseconds() -> Double { - return timeIntervalSince1970 * 1000 - } -} - -extension String { - func toData() -> Data { - let len = count / 2 - var data = Data(capacity: len) - var i = startIndex - for _ in 0.. [String: Any] { - return [ - "uid": uid, - "timestamp": NSNumber(value: timestamp).intValue * 1000, - "products": products.mapValues { $0.toMap() }, - "permissions": permissions.mapValues { $0.toMap() }, - "user_products": userPoducts.mapValues { $0.toMap() }, - ] - } -} - -extension Qonversion.Product { - func toMap() -> [String: Any?] { - return [ - ProductFields.id: qonversionID, - ProductFields.storeId: storeID, - ProductFields.type: type.rawValue, - ProductFields.duration: duration.rawValue, - ProductFields.skProduct: skProduct?.toMap(), - ProductFields.prettyPrice: prettyPrice, - ProductFields.trialDuration: trialDuration.rawValue, - ProductFields.offeringId: offeringID - ] - } -} - -extension Qonversion.Permission { - func toMap() -> [String: Any?] { - return [ - "id": permissionID, - "associated_product": productID, - "renew_state": renewState.rawValue, - "started_timestamp": startedDate.timeIntervalSince1970 * 1000, - "expiration_timestamp": expirationDate?.timeIntervalSince1970 != nil ? expirationDate!.timeIntervalSince1970 * 1000 : nil, - "active": isActive, - ] - } -} - -extension Qonversion.Property { - static func fromString(_ string: String) throws -> Self { - switch string { - case "Email": - return .email - - case "Name": - return .name - - case "AppsFlyerUserId": - return .appsFlyerUserID - - case "AdjustAdId": - return .adjustUserID - - case "KochavaDeviceId": - return .kochavaDeviceID - - case "CustomUserId": - return .userID - - case "FirebaseAppInstanceId": - return .firebaseAppInstanceId - - default: - throw ParsingError.runtimeError("Could not parse Qonversion.Property") - } - } -} - -extension Qonversion.Offerings { - func toMap() -> [String: Any?] { - return [ - "main": main?.toMap(), - "available_offerings": availableOfferings.map { $0.toMap() } - ] - } -} - -extension Qonversion.Offering { - func toMap() -> [String: Any?] { - return [ - "id": identifier, - "tag": tag.rawValue, - "products": products.map { $0.toMap() } - ] - } -} - -extension Qonversion.IntroEligibility { - func toMap() -> [String: Any?] { - return ["status": status.rawValue] - } -} - -// MARK: - JSON Encoding -extension Dictionary { - func toJson() -> String? { - guard let jsonData = try? JSONSerialization.data(withJSONObject: self, - options: []) else { - return nil - } - - return String(data: jsonData, encoding: .utf8) - } - - func toProduct() -> Qonversion.Product? { - guard let data = self as? [String: Any], - let id = data[ProductFields.id] as? String - else { return nil } - - let product = Qonversion.Product() - - product.qonversionID = id - - product.storeID = data[ProductFields.storeId] as? String ?? "" - - if let type = data[ProductFields.type] as? Int { - product.type = Qonversion.ProductType(rawValue: type) ?? Qonversion.ProductType.unknown - } - - if let duration = data[ProductFields.duration] as? Int { - product.duration = Qonversion.ProductDuration(rawValue: duration) ?? Qonversion.ProductDuration.durationUnknown - } - - product.prettyPrice = data[ProductFields.prettyPrice] as? String ?? "" - - product.offeringID = data[ProductFields.offeringId] as? String - - if let trialDuration = data[ProductFields.trialDuration] as? Int { - product.trialDuration = Qonversion.TrialDuration(rawValue: trialDuration) ?? Qonversion.TrialDuration.notAvailable - } - - return product - } -} - -extension Qonversion.ActionResult { - func toMap() -> [String: Any?] { - let nsError = error as NSError? - - return ["action_type": type.rawValue, - "parameters": parameters, - "error": nsError?.toMap()] - } -} - -extension QONAutomationsEvent { - func toMap() -> [String: Any?] { - return ["event_type": type.rawValue, - "date": date.toMilliseconds()] - } -} - -extension Qonversion.PermissionsCacheLifetime { - static func fromString(_ string: String) throws -> Self { - switch string { - case "Week": - return .week; - - case "TwoWeeks": - return .twoWeeks; - - case "Month": - return .month; - - case "TwoMonths": - return .twoMonth; - - case "ThreeMonths": - return .threeMonth; - - case "SixMonths": - return .sixMonth; - - case "Year": - return .year; - - case "Unlimited": - return .unlimited; - - default: - throw ParsingError.runtimeError("Could not parse Qonversion.PermissionsCacheLifetime"); - } - } -} diff --git a/ios/Classes/SKProduct+toMap.swift b/ios/Classes/SKProduct+toMap.swift deleted file mode 100644 index 8d779be1..00000000 --- a/ios/Classes/SKProduct+toMap.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// SKProduct+toMap.swift -// qonversion_flutter -// -// Created by Ilya Virnik on 12/2/20. -// - -import StoreKit - -extension SKProduct { - func toMap() -> [String: Any?] { - var map: [String: Any?] = [ - "localizedDescription": localizedDescription, - "localizedTitle": localizedTitle, - "productIdentifier": productIdentifier, - "price": price.description, - "priceLocale": priceLocale.toMap() - ] - - if #available(iOS 11.2, macOS 10.13.2, *) { - map["subscriptionPeriod"] = subscriptionPeriod?.toMap() - map["introductoryPrice"] = introductoryPrice?.toMap() - } - - if #available(iOS 12.0, macOS 10.14, *) { - map["subscriptionGroupIdentifier"] = subscriptionGroupIdentifier - } - - return map - } -} - -extension Locale { - func toMap() -> [String: Any?] { - return [ - "currencySymbol": currencySymbol, - "currencyCode": currencyCode - ] - } -} - -@available(iOS 11.2, macOS 10.13.2, *) -extension SKProductSubscriptionPeriod { - func toMap() -> [String: Any] { - return [ - "numberOfUnits": numberOfUnits, - "unit": unit.rawValue - ] - } -} - -@available(iOS 11.2, macOS 10.13.2, *) -extension SKProductDiscount { - func toMap() -> [String: Any] { - return [ - "price": price.description, - "numberOfPeriods": numberOfPeriods, - "subscriptionPeriod": subscriptionPeriod.toMap(), - "paymentMode": paymentMode.rawValue, - "priceLocale": priceLocale.toMap() - ] - } -} - diff --git a/ios/Classes/SwiftQonversionFlutterSdkPlugin.swift b/ios/Classes/SwiftQonversionFlutterSdkPlugin.swift index 657d4eaf..4d6c0c35 100644 --- a/ios/Classes/SwiftQonversionFlutterSdkPlugin.swift +++ b/ios/Classes/SwiftQonversionFlutterSdkPlugin.swift @@ -4,12 +4,12 @@ import FlutterMacOS import Flutter #endif -import Qonversion +import QonversionSandwich public class SwiftQonversionFlutterSdkPlugin: NSObject, FlutterPlugin { var deferredPurchasesStreamHandler: BaseEventStreamHandler? var promoPurchasesStreamHandler: BaseEventStreamHandler? - var promoPurchasesExecutionBlocks = [String: Qonversion.PromoPurchaseCompletionHandler]() + var qonversionSandwich: QonversionSandwich? private var automationsPlugin: AutomationsPlugin? private var shownScreensStreamHandler: BaseEventStreamHandler? @@ -27,12 +27,14 @@ public class SwiftQonversionFlutterSdkPlugin: NSObject, FlutterPlugin { // Register deferred purchases events let purchasesListener = FlutterListenerWrapper(registrar, postfix: "updated_purchases") purchasesListener.register() { instance.deferredPurchasesStreamHandler = $0 } - Qonversion.setPurchasesDelegate(instance) // Register promo purchases events let promoPurchasesListener = FlutterListenerWrapper(registrar, postfix: "promo_purchases") promoPurchasesListener.register() { instance.promoPurchasesStreamHandler = $0 } - Qonversion.setPromoPurchasesDelegate(instance) + + // Register sandwich + let sandwichInstance = QonversionSandwich.init(qonversionEventListener: instance) + instance.qonversionSandwich = sandwichInstance instance.automationsPlugin = AutomationsPlugin() instance.automationsPlugin?.register(registrar) @@ -53,18 +55,18 @@ public class SwiftQonversionFlutterSdkPlugin: NSObject, FlutterPlugin { return restore(result) case "setDebugMode": - Qonversion.setDebugMode() + qonversionSandwich?.setDebugMode() return result(nil) case "setAdvertisingID": - Qonversion.setAdvertisingID() + qonversionSandwich?.setAdvertisingId() return result(nil) case "offerings": return offerings(result) case "logout": - Qonversion.logout() + qonversionSandwich?.logout() return result(nil) default: @@ -85,7 +87,7 @@ public class SwiftQonversionFlutterSdkPlugin: NSObject, FlutterPlugin { return purchase(args["productId"] as? String, result) case "purchaseProduct": - return purchaseProduct(args["product"] as? String, result) + return purchaseProduct(args["productId"] as? String, args["offeringId"] as? String, result) case "promoPurchase": return promoPurchase(args["productId"] as? String, result) @@ -93,11 +95,11 @@ public class SwiftQonversionFlutterSdkPlugin: NSObject, FlutterPlugin { case "addAttributionData": return addAttributionData(args, result) - case "setProperty": - return setProperty(args, result) + case "setDefinedUserProperty": + return setDefinedUserProperty(args, result) - case "setUserProperty": - return setUserProperty(args, result) + case "setCustomUserProperty": + return setCustomUserProperty(args, result) case "checkTrialIntroEligibility": return checkTrialIntroEligibility(args, result) @@ -130,15 +132,8 @@ public class SwiftQonversionFlutterSdkPlugin: NSObject, FlutterPlugin { guard let apiKey = apiKey, !apiKey.isEmpty else { return result(FlutterError.noApiKey) } - - Qonversion.launch(withKey: apiKey) { launchResult, error in - if let nsError = error as NSError? { - return result(FlutterError.qonversionError(nsError)) - } - - let resultMap = launchResult.toMap() - result(resultMap) - } + + qonversionSandwich?.launch(projectKey: apiKey, completion: getDefaultCompletion(result)) } private func identify(_ userId: String?, _ result: @escaping FlutterResult) { @@ -147,20 +142,18 @@ public class SwiftQonversionFlutterSdkPlugin: NSObject, FlutterPlugin { return } - Qonversion.identify(userId) + qonversionSandwich?.identify(userId) result(nil) } private func products(_ result: @escaping FlutterResult) { - Qonversion.products { (products, error) in - if let nsError = error as NSError? { - return result(FlutterError.failedToGetProducts(nsError)) + qonversionSandwich?.products({ products, error in + if let error = error { + return result(FlutterError.failedToGetProducts(error)) } - let productsMap = products.mapValues { $0.toMap() } - - result(productsMap) - } + result(products) + }) } private func purchase(_ productId: String?, _ result: @escaping FlutterResult) { @@ -168,41 +161,18 @@ public class SwiftQonversionFlutterSdkPlugin: NSObject, FlutterPlugin { return result(FlutterError.noProductId) } - Qonversion.purchase(productId) { (permissions, error, isCancelled) in - let nsError = error as NSError? - let purchaseResult = PurchaseResult(permissions: permissions, - error: nsError, - isCancelled: isCancelled) - result(purchaseResult.toMap()) - } + qonversionSandwich?.purchase(productId, completion: getPurchaseCompletion(result)) } - private func purchaseProduct(_ jsonProduct: String?, _ result: @escaping FlutterResult) { - guard let jsonProduct = jsonProduct else { - return result(FlutterError.noProduct) + private func purchaseProduct(_ productId: String?, _ offeringId: String?, _ result: @escaping FlutterResult) { + guard let productId = productId else { + return result(FlutterError.noProductId) } - - do { - let data = Data(jsonProduct.utf8) - if let jsonMap = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] { - - guard let product = jsonMap.toProduct() else { - let errorMessage = "Failed to deserialize Qonversion Product. There is no qonversionId" - return result(FlutterError.noProductIdField(errorMessage)) - } - - Qonversion.purchaseProduct(product) { (permissions, error, isCancelled) in - let nsError = error as NSError? - let purchaseResult = PurchaseResult(permissions: permissions, - error: nsError, - isCancelled: isCancelled) - result(purchaseResult.toMap()) - } - } - } catch let error as NSError { - let errorMessage = "Failed to deserialize Qonversion Product: \(error.localizedDescription)" - result(FlutterError.jsonSerializationError(errorMessage)) + guard let offeringId = offeringId else { + return result(FlutterError.noOfferingId) } + + qonversionSandwich?.purchaseProduct(productId, offeringId, completion: getPurchaseCompletion(result)) } private func promoPurchase(_ productId: String?, _ result: @escaping FlutterResult) { @@ -210,59 +180,22 @@ public class SwiftQonversionFlutterSdkPlugin: NSObject, FlutterPlugin { return result(FlutterError.noProductId) } - if let executionBlock = promoPurchasesExecutionBlocks[productId] { - promoPurchasesExecutionBlocks.removeValue(forKey: productId) - - executionBlock { (permissions, error, isCancelled) in - let nsError = error as NSError? - let purchaseResult = PurchaseResult(permissions: permissions, - error: nsError, - isCancelled: isCancelled) - result(purchaseResult.toMap()) - } - } else { - result(FlutterError.promoPurchaseError(productId)) - } + qonversionSandwich?.promoPurchase(productId, completion: getPurchaseCompletion(result)) } private func checkPermissions(_ result: @escaping FlutterResult) { - Qonversion.checkPermissions { (permissions, error) in - if let nsError = error as NSError? { - return result(FlutterError.qonversionError(nsError)) - } - - let permissionsDict = permissions.mapValues { $0.toMap() } - result(permissionsDict) - } + qonversionSandwich?.checkPermissions(getDefaultCompletion(result)) } private func restore(_ result: @escaping FlutterResult) { - Qonversion.restore { (permissions, error) in - if let nsError = error as NSError? { - return result(FlutterError.qonversionError(nsError)) - } - - let permissionsDict = permissions.mapValues { $0.toMap() } - result(permissionsDict) - } + qonversionSandwich?.restore(getDefaultCompletion(result)) } private func offerings(_ result: @escaping FlutterResult) { - Qonversion.offerings { offerings, error in - if let nsError = error as NSError? { - result(FlutterError.offeringsError(nsError)) - } - - guard let offerings = offerings else { - return result(nil) - } - - - result(offerings.toMap().toJson()) - } + qonversionSandwich?.offerings(getJsonCompletion(result)) } - private func setProperty(_ args: [String: Any], _ result: @escaping FlutterResult) { + private func setDefinedUserProperty(_ args: [String: Any], _ result: @escaping FlutterResult) { guard let rawProperty = args["property"] as? String else { return result(FlutterError.noProperty) } @@ -271,21 +204,11 @@ public class SwiftQonversionFlutterSdkPlugin: NSObject, FlutterPlugin { return result(FlutterError.noPropertyValue) } - do { - let property = try Qonversion.Property.fromString(rawProperty) - - Qonversion.setProperty(property, value: value) - result(nil) - } catch ParsingError.runtimeError(let message) { - result(FlutterError.parsingError(message)) - } catch { - if let nsError = error as NSError? { - result(FlutterError.qonversionError(nsError)) - } - } + qonversionSandwich?.setDefinedProperty(rawProperty, value: value) + result(nil) } - - private func setUserProperty(_ args: [String: Any], _ result: @escaping FlutterResult) { + + private func setCustomUserProperty(_ args: [String: Any], _ result: @escaping FlutterResult) { guard let property = args["property"] as? String else { return result(FlutterError.noProperty) } @@ -294,8 +217,7 @@ public class SwiftQonversionFlutterSdkPlugin: NSObject, FlutterPlugin { return result(FlutterError.noPropertyValue) } - Qonversion.setUserProperty(property, value: value) - + qonversionSandwich?.setCustomProperty(property, value: value) result(nil) } @@ -304,33 +226,22 @@ public class SwiftQonversionFlutterSdkPlugin: NSObject, FlutterPlugin { return result(FlutterError.noData) } - Qonversion.checkTrialIntroEligibility(forProductIds: ids) { eligibilities, error in - if let nsError = error as NSError? { - return result(FlutterError.qonversionError(nsError)) - } - - result(eligibilities.mapValues { $0.toMap() }.toJson()) - } + qonversionSandwich?.checkTrialIntroEligibility(ids, completion: getJsonCompletion(result)) } private func storeSdkInfo(_ args: [String: Any], _ result: @escaping FlutterResult) { guard let version = args["version"] as? String, - let source = args["source"] as? String, - let sourceKey = args["sourceKey"] as? String, - let versionKey = args["versionKey"] as? String + let source = args["source"] as? String else { return result(FlutterError.noSdkInfo) } - let defaults = UserDefaults.standard - defaults.set(version, forKey: versionKey) - defaults.set(source, forKey: sourceKey) - + qonversionSandwich?.storeSdkInfo(source: source, version: version) result(nil) } - + private func addAttributionData(_ args: [String: Any], _ result: @escaping FlutterResult) { - guard let data = args["data"] as? [AnyHashable: Any] else { + guard let data = args["data"] as? [String: Any] else { return result(FlutterError.noData) } @@ -338,25 +249,12 @@ public class SwiftQonversionFlutterSdkPlugin: NSObject, FlutterPlugin { return result(FlutterError.noProvider) } - // Using appsFlyer by default since there are only 2 cases in an enum yet. - var castedProvider = Qonversion.AttributionProvider.appsFlyer - - switch provider { - case "branch": - castedProvider = Qonversion.AttributionProvider.branch - default: - break - } - - Qonversion.addAttributionData(data, from: castedProvider) - + qonversionSandwich?.addAttributionData(sourceKey: provider, value: data) result(nil) } private func setAppleSearchAdsAttributionEnabled(_ enable: Bool, _ result: @escaping FlutterResult) { - if enable { - Qonversion.setAppleSearchAdsAttributionEnabled(enable) - } + qonversionSandwich?.setAppleSearchAdsAttributionEnabled(enable) result(nil) } @@ -365,18 +263,8 @@ public class SwiftQonversionFlutterSdkPlugin: NSObject, FlutterPlugin { return result(FlutterError.noLifetime) } - do { - let lifetime = try Qonversion.PermissionsCacheLifetime.fromString(rawLifetime) - - Qonversion.setPermissionsCacheLifetime(lifetime) - result(nil) - } catch ParsingError.runtimeError(let message) { - result(FlutterError.parsingError(message)) - } catch { - if let nsError = error as NSError? { - result(FlutterError.qonversionError(nsError)) - } - } + qonversionSandwich?.setPermissionsCacheLifetime(rawLifetime) + result(nil) } private func setNotificationsToken(_ token: String?, _ result: @escaping FlutterResult) { @@ -384,8 +272,8 @@ public class SwiftQonversionFlutterSdkPlugin: NSObject, FlutterPlugin { result(FlutterError.noArgs) return } - let tokenData = token.toData() - Qonversion.setNotificationsToken(tokenData) + + qonversionSandwich?.setNotificationToken(token) result(nil) } @@ -394,23 +282,55 @@ public class SwiftQonversionFlutterSdkPlugin: NSObject, FlutterPlugin { return result(FlutterError.noData) } - let isPushHandled: Bool = Qonversion.handleNotification(notificationData) + let isPushHandled: Bool = qonversionSandwich?.handleNotification(notificationData) ?? false result(isPushHandled) } -} + + private func getDefaultCompletion(_ result: @escaping FlutterResult) -> BridgeCompletion { + return { data, error in + if let error = error { + return result(FlutterError.sandwichError(error)) + } + + result(data) + } + } + + private func getJsonCompletion(_ result: @escaping FlutterResult) -> BridgeCompletion { + return { data, error in + if let error = error { + return result(FlutterError.sandwichError(error)) + } -extension SwiftQonversionFlutterSdkPlugin: Qonversion.PurchasesDelegate { - public func qonversionDidReceiveUpdatedPermissions(_ permissions: [String : Qonversion.Permission]) { - let payload = permissions.mapValues { $0.toMap() }.toJson() - - deferredPurchasesStreamHandler?.eventSink?(payload) + guard let data = data else { + return result(nil) + } + + guard let jsonData = data.toJson() else { + return result(FlutterError.serializationError) + } + + result(jsonData) + } + } + + private func getPurchaseCompletion(_ result: @escaping FlutterResult) -> BridgeCompletion { + return { data, error in + if let error = error { + return result(FlutterError.purchaseError(error)) + } + + result(data) + } } } -extension SwiftQonversionFlutterSdkPlugin: QNPromoPurchasesDelegate { - public func shouldPurchasePromoProduct(withIdentifier productID: String, executionBlock: @escaping Qonversion.PromoPurchaseCompletionHandler) { - promoPurchasesExecutionBlocks[productID] = executionBlock - - promoPurchasesStreamHandler?.eventSink?(productID) +extension SwiftQonversionFlutterSdkPlugin: QonversionEventListener { + public func shouldPurchasePromoProduct(with productId: String) { + promoPurchasesStreamHandler?.eventSink?(productId) + } + + public func qonversionDidReceiveUpdatedPermissions(_ permissions: [String : Any]) { + deferredPurchasesStreamHandler?.eventSink?(permissions) } } diff --git a/ios/qonversion_flutter.podspec b/ios/qonversion_flutter.podspec index 0a83c99a..1d6d18f4 100644 --- a/ios/qonversion_flutter.podspec +++ b/ios/qonversion_flutter.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |s| s.source_files = 'Classes/**/*' s.dependency 'Flutter' s.platform = :ios, '9.0' - s.dependency 'Qonversion', '2.20.0' + s.dependency 'QonversionSandwich', '0.0.14' # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } diff --git a/lib/src/constants.dart b/lib/src/constants.dart index 21ace53e..4cfbd1ab 100644 --- a/lib/src/constants.dart +++ b/lib/src/constants.dart @@ -31,8 +31,8 @@ class Constants { static const mUpdatePurchaseWithProduct = 'updatePurchaseWithProduct'; static const mCheckPermissions = 'checkPermissions'; static const mRestore = 'restore'; - static const mSetProperty = 'setProperty'; - static const mSetUserProperty = 'setUserProperty'; + static const mSetDefinedUserProperty = 'setDefinedUserProperty'; + static const mSetCustomUserProperty = 'setCustomUserProperty'; static const mSetPermissionsCacheLifetime = 'setPermissionsCacheLifetime'; static const mSyncPurchases = 'syncPurchases'; static const mAddAttributionData = 'addAttributionData'; diff --git a/lib/src/qonversion.dart b/lib/src/qonversion.dart index 022f794b..2a4f5383 100644 --- a/lib/src/qonversion.dart +++ b/lib/src/qonversion.dart @@ -250,7 +250,7 @@ class Qonversion { /// /// See more in [documentation](https://documentation.qonversion.io/docs/user-properties) static Future setProperty(QUserProperty property, String value) => - _channel.invokeMethod(Constants.mSetProperty, { + _channel.invokeMethod(Constants.mSetDefinedUserProperty, { Constants.kProperty: StringUtils.capitalize(describeEnum(property)), Constants.kValue: value, }); @@ -262,7 +262,7 @@ class Qonversion { /// /// See more in [documentation](https://documentation.qonversion.io/docs/user-properties) static Future setUserProperty(String property, String value) => - _channel.invokeMethod(Constants.mSetUserProperty, { + _channel.invokeMethod(Constants.mSetCustomUserProperty, { Constants.kProperty: property, Constants.kValue: value, }); diff --git a/macos/Classes/Extensions.swift b/macos/Classes/Extensions.swift new file mode 100644 index 00000000..2505afee --- /dev/null +++ b/macos/Classes/Extensions.swift @@ -0,0 +1,19 @@ +// +// Extensions.swift +// qonversion_flutter +// +// Created by Kamo Spertsyan on 05.09.2022. +// + +import Foundation + +extension Dictionary { + func toJson() -> String? { + guard let jsonData = try? JSONSerialization.data(withJSONObject: self, + options: []) else { + return nil + } + + return String(data: jsonData, encoding: .utf8) + } +} diff --git a/macos/Classes/FlutterError+Custom.swift b/macos/Classes/FlutterError+Custom.swift index 20622cd7..ae4213af 100644 --- a/macos/Classes/FlutterError+Custom.swift +++ b/macos/Classes/FlutterError+Custom.swift @@ -11,6 +11,8 @@ import FlutterMacOS import Flutter #endif +import QonversionSandwich + extension FlutterError { static private let passValidValue = "Please make sure you pass a valid value" @@ -26,10 +28,6 @@ extension FlutterError { message: "Could not find userID", details: passValidValue) - static let noAutoTrackPurchases = FlutterError(code: "3", - message: "Could not find autoTrackPurchases boolean value", - details: passValidValue) - static let noData = FlutterError(code: "4", message: "Could not find data", details: passValidValue) @@ -38,26 +36,22 @@ extension FlutterError { message: "Could not find provider", details: passValidValue) - static func failedToGetProducts(_ description: String) -> FlutterError { - return FlutterError(code: "7", - message: "Failed to get products", - details: description) + static func failedToGetProducts(_ error: SandwichError) -> FlutterError { + return mapSandwichError(error, errorCode: "7", errorMessage: "Failed to get products") } static let noProductId = FlutterError(code: "8", message: "Could not find productId value", details: "Please provide valid productId") - - static func qonversionError(_ description: String) -> FlutterError { - return FlutterError(code: "9", - message: "Qonversion Error", - details: description) + + static func sandwichError(_ error: SandwichError) -> FlutterError { + return mapSandwichError(error, errorCode: "9") } - static func parsingError(_ description: String) -> FlutterError { - return FlutterError(code: "12", - message: "Arguments Parsing Error", - details: description) + static func purchaseError(_ error: SandwichError) -> FlutterError { + let isCancelled = error.additionalInfo["isCancelled"] + let code = isCancelled != nil ? "PurchaseCancelledByUser" : "9" + return mapSandwichError(error, errorCode: code) } static let noProperty = FlutterError(code: "13", @@ -67,13 +61,7 @@ extension FlutterError { static let noPropertyValue = FlutterError(code: "14", message: "Could not find property value", details: passValidValue) - - static func offeringsError(_ description: String) -> FlutterError { - return FlutterError(code: "OFFERINGS", - message: "Could not get offerings", - details: description) - } - + static let noSdkInfo = FlutterError(code: "15", message: "Could not find sdk info", details: passValidValue) @@ -81,5 +69,50 @@ extension FlutterError { static let noLifetime = FlutterError(code: "16", message: "Could not find lifetime", details: passValidValue) - + + static let noOfferingId = FlutterError(code: "17", + message: "Could not find offeringId value", + details: "Please provide valid offeringId") + + static let serializationError = FlutterError(code: "18", + message: "Failed to serialize response from native bridge", + details: "") + + private static func mapQonversionError(_ error: NSError, errorCode: String, errorMessage: String? = nil) -> FlutterError { + var message = "" + + if let errorMessage = errorMessage { + message = errorMessage + ". " + } + message += error.localizedDescription + + var details = "Qonversion Error Code: \(error.code)" + + if let additionalMessage = error.userInfo[NSDebugDescriptionErrorKey] { + details = "\(details). Additional Message: \(additionalMessage)" + } + + return FlutterError(code: errorCode, + message: message, + details: details) + } + + private static func mapSandwichError(_ error: SandwichError, errorCode: String, errorMessage: String? = nil) -> FlutterError { + var message = "" + + if let errorMessage = errorMessage { + message = errorMessage + ". " + } + message += error.details + + var details = "Qonversion Error Code: \(error.code)" + + if let additionalMessage = error.additionalMessage { + details = "\(details). Additional Message: \(additionalMessage)" + } + + return FlutterError(code: errorCode, + message: message, + details: details) + } } diff --git a/macos/Classes/Mapper.swift b/macos/Classes/Mapper.swift deleted file mode 100644 index 2b366479..00000000 --- a/macos/Classes/Mapper.swift +++ /dev/null @@ -1,162 +0,0 @@ -// -// Mapper.swift -// qonversion_flutter -// -// Created by Ilya Virnik on 11/22/20. -// - -import Qonversion - -enum ParsingError: Error { - case runtimeError(String) -} - -struct PurchaseResult { - let permissions: [String : Qonversion.Permission] - let error: Error? - let isCancelled: Bool - - func toMap() -> [String: Any?] { - return [ - "permissions": permissions.mapValues { $0.toMap() }, - "error": error?.localizedDescription, - "is_cancelled": isCancelled, - ] - } -} - -extension Qonversion.LaunchResult { - func toMap() -> [String: Any] { - return [ - "uid": uid, - "timestamp": NSNumber(value: timestamp).intValue * 1000, - "products": products.mapValues { $0.toMap() }, - "permissions": permissions.mapValues { $0.toMap() }, - "user_products": userPoducts.mapValues { $0.toMap() }, - ] - } -} - -extension Qonversion.Product { - func toMap() -> [String: Any?] { - return [ - "id": qonversionID, - "store_id": storeID, - "type": type.rawValue, - "duration": duration.rawValue, - "sk_product": skProduct?.toMap(), - "pretty_price": prettyPrice, - "trial_duration": trialDuration.rawValue - ] - } -} - -extension Qonversion.Permission { - func toMap() -> [String: Any?] { - return [ - "id": permissionID, - "associated_product": productID, - "renew_state": renewState.rawValue, - "started_timestamp": startedDate.timeIntervalSince1970 * 1000, - "expiration_timestamp": expirationDate?.timeIntervalSince1970 != nil ? expirationDate!.timeIntervalSince1970 * 1000 : nil, - "active": isActive, - ] - } -} - -extension Qonversion.Property { - static func fromString(_ string: String) throws -> Self { - switch string { - case "Email": - return .email - - case "Name": - return .name - - case "AppsFlyerUserId": - return .appsFlyerUserID - - case "AdjustAdId": - return .adjustUserID - - case "KochavaDeviceId": - return .kochavaDeviceID - - case "FirebaseAppInstanceId": - return .firebaseAppInstanceId - - default: - throw ParsingError.runtimeError("Could not parse Qonversion.Property") - } - } -} - -extension Qonversion.Offerings { - func toMap() -> [String: Any?] { - return [ - "main": main?.toMap(), - "available_offerings": availableOfferings.map { $0.toMap() } - ] - } -} - -extension Qonversion.Offering { - func toMap() -> [String: Any?] { - return [ - "id": identifier, - "tag": tag.rawValue, - "products": products.map { $0.toMap() } - ] - } -} - -extension Qonversion.IntroEligibility { - func toMap() -> [String: Any?] { - return ["status": status.rawValue] - } -} - -extension Qonversion.PermissionsCacheLifetime { - static func fromString(_ string: String) throws -> Self { - switch string { - case "Week": - return .week; - - case "TwoWeeks": - return .twoWeeks; - - case "Month": - return .month; - - case "TwoMonths": - return .twoMonth; - - case "ThreeMonths": - return .threeMonth; - - case "SixMonths": - return .sixMonth; - - case "Year": - return .year; - - case "Unlimited": - return .unlimited; - - default: - throw ParsingError.runtimeError("Could not parse Qonversion.PermissionsCacheLifetime"); - } - } -} - -// MARK: - JSON Encoding -extension Dictionary { - func toJson() -> String? { - guard let jsonData = try? JSONSerialization.data(withJSONObject: self, - options: []) else { - return nil - } - - return String(data: jsonData, encoding: .utf8) - } -} diff --git a/macos/Classes/SKProduct+toMap.swift b/macos/Classes/SKProduct+toMap.swift deleted file mode 100644 index 246c56c2..00000000 --- a/macos/Classes/SKProduct+toMap.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// SKProduct+toMap.swift -// qonversion_flutter -// -// Created by Ilya Virnik on 12/2/20. -// - -import StoreKit - -extension SKProduct { - func toMap() -> [String: Any?] { - var map: [String: Any?] = [ - "localizedDescription": localizedDescription, - "localizedTitle": localizedTitle, - "productIdentifier": productIdentifier, - "price": price.description, - "priceLocale": priceLocale.toMap() - ] - - if #available(iOS 11.2, macOS 10.13.2, *) { - map["subscriptionPeriod"] = subscriptionPeriod?.toMap() - map["introductoryPrice"] = introductoryPrice?.toMap() - } - - if #available(iOS 12.0, macOS 10.14, *) { - map["subscriptionGroupIdentifier"] = subscriptionGroupIdentifier - } - - return map - } -} - -extension Locale { - func toMap() -> [String: Any?] { - return [ - "currencySymbol": currencySymbol, - "currencyCode": currencyCode - ] - } -} - -@available(iOS 11.2, macOS 10.13.2, *) -extension SKProductSubscriptionPeriod { - func toMap() -> [String: Any] { - return [ - "numberOfUnits": numberOfUnits, - "unit": unit.rawValue - ] - } -} - -@available(iOS 11.2, macOS 10.13.2, *) -extension SKProductDiscount { - func toMap() -> [String: Any] { - return [ - "price": price.description, - "numberOfPeriods": numberOfPeriods, - "subscriptionPeriod": subscriptionPeriod.toMap(), - "paymentMode": paymentMode.rawValue, - ] - } -} - diff --git a/macos/Classes/SwiftQonversionFlutterSdkPlugin.swift b/macos/Classes/SwiftQonversionFlutterSdkPlugin.swift index e765af97..0fa08727 100644 --- a/macos/Classes/SwiftQonversionFlutterSdkPlugin.swift +++ b/macos/Classes/SwiftQonversionFlutterSdkPlugin.swift @@ -4,11 +4,12 @@ import FlutterMacOS import Flutter #endif -import Qonversion +import QonversionSandwich public class SwiftQonversionFlutterSdkPlugin: NSObject, FlutterPlugin { - var purchasesEventStreamHandler: BaseEventStreamHandler? - + var deferredPurchasesStreamHandler: BaseEventStreamHandler? + var qonversionSandwich: QonversionSandwich? + public static func register(with registrar: FlutterPluginRegistrar) { let messenger: FlutterBinaryMessenger #if canImport(FlutterMacOS) @@ -19,82 +20,80 @@ public class SwiftQonversionFlutterSdkPlugin: NSObject, FlutterPlugin { let channel = FlutterMethodChannel(name: "qonversion_flutter_sdk", binaryMessenger: messenger) let instance = SwiftQonversionFlutterSdkPlugin() registrar.addMethodCallDelegate(instance, channel: channel) - - // Register events listeners + + // Register deferred purchases events let purchasesListener = FlutterListenerWrapper(registrar, postfix: "updated_purchases") - purchasesListener.register() { instance.purchasesEventStreamHandler = $0 } - - // Setting delegate as soon as plugin is registered - Qonversion.setPurchasesDelegate(instance) + purchasesListener.register() { instance.deferredPurchasesStreamHandler = $0 } + + // Register sandwich + let sandwichInstance = QonversionSandwich.init(qonversionEventListener: instance) + instance.qonversionSandwich = sandwichInstance } - + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - + // MARK: - Calls without arguments - + switch (call.method) { case "products": return products(result) - + case "checkPermissions": return checkPermissions(result) - + case "restore": return restore(result) - + case "setDebugMode": - Qonversion.setDebugMode() + qonversionSandwich?.setDebugMode() return result(nil) case "setAdvertisingID": - Qonversion.setAdvertisingID() + qonversionSandwich?.setAdvertisingId() return result(nil) - + case "offerings": return offerings(result) - + case "logout": - Qonversion.logout() + qonversionSandwich?.logout() return result(nil) - + default: break } - + // MARK: - Calls with arguments - + guard let args = call.arguments as? [String: Any] else { return result(FlutterError.noArgs) } - + switch call.method { case "launch": return launch(with: args["key"] as? String, result) - + case "purchase": return purchase(args["productId"] as? String, result) - + case "purchaseProduct": - return purchaseProduct(args["product"] as? String, result) - - case "setUserId": - return setUserId(args["userId"] as? String, result) - + return purchaseProduct(args["productId"] as? String, args["offeringId"] as? String, result) + case "addAttributionData": return addAttributionData(args, result) - - case "setProperty": - return setProperty(args, result) - - case "setUserProperty": - return setUserProperty(args, result) - + + case "setDefinedUserProperty": + return setDefinedUserProperty(args, result) + + case "setCustomUserProperty": + return setCustomUserProperty(args, result) + case "checkTrialIntroEligibility": return checkTrialIntroEligibility(args, result) - + case "storeSdkInfo": return storeSdkInfo(args, result) - + case "identify": return identify(args["userId"] as? String, result) @@ -105,218 +104,121 @@ public class SwiftQonversionFlutterSdkPlugin: NSObject, FlutterPlugin { return result(FlutterMethodNotImplemented) } } - + private func launch(with apiKey: String?, _ result: @escaping FlutterResult) { guard let apiKey = apiKey, !apiKey.isEmpty else { return result(FlutterError.noApiKey) } - - Qonversion.launch(withKey: apiKey) { launchResult, error in - if let error = error { - return result(FlutterError.qonversionError(error.localizedDescription)) - } - - let resultMap = launchResult.toMap() - result(resultMap) - } + + qonversionSandwich?.launch(projectKey: apiKey, completion: getDefaultCompletion(result)) } - + private func identify(_ userId: String?, _ result: @escaping FlutterResult) { guard let userId = userId else { result(FlutterError.noUserId) return } - - Qonversion.identify(userId) + + qonversionSandwich?.identify(userId) result(nil) } - + private func products(_ result: @escaping FlutterResult) { - Qonversion.products { (products, error) in + qonversionSandwich?.products({ products, error in if let error = error { - return result(FlutterError.failedToGetProducts(error.localizedDescription)) + return result(FlutterError.failedToGetProducts(error)) } - - let productsMap = products.mapValues { $0.toMap() } - - result(productsMap) - } + + result(products) + }) } - + private func purchase(_ productId: String?, _ result: @escaping FlutterResult) { guard let productId = productId else { return result(FlutterError.noProductId) } - - Qonversion.purchase(productId) { (permissions, error, isCancelled) in - let purchaseResult = PurchaseResult(permissions: permissions, - error: error, - isCancelled: isCancelled) - result(purchaseResult.toMap()) - } + + qonversionSandwich?.purchase(productId, completion: getPurchaseCompletion(result)) } - - private func purchaseProduct(_ jsonProduct: String?, _ result: @escaping FlutterResult) { - guard let jsonProduct = jsonProduct else { - return result(FlutterError.noProduct) + + private func purchaseProduct(_ productId: String?, _ offeringId: String?, _ result: @escaping FlutterResult) { + guard let productId = productId else { + return result(FlutterError.noProductId) } - - do { - let data = Data(jsonProduct.utf8) - if let jsonMap = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] { - - guard let product = jsonMap.toProduct() else { - let errorMessage = "Failed to deserialize Qonversion Product. There is no qonversionId" - return result(FlutterError.noProductIdField(errorMessage)) - } - - Qonversion.purchaseProduct(product) { (permissions, error, isCancelled) in - let nsError = error as NSError? - let purchaseResult = PurchaseResult(permissions: permissions, - error: nsError, - isCancelled: isCancelled) - result(purchaseResult.toMap()) - } - } - } catch let error as NSError { - let errorMessage = "Failed to deserialize Qonversion Product: \(error.localizedDescription)" - result(FlutterError.jsonSerializationError(errorMessage)) + guard let offeringId = offeringId else { + return result(FlutterError.noOfferingId) } + + qonversionSandwich?.purchaseProduct(productId, offeringId, completion: getPurchaseCompletion(result)) } - + private func checkPermissions(_ result: @escaping FlutterResult) { - Qonversion.checkPermissions { (permissions, error) in - if let error = error { - return result(FlutterError.qonversionError(error.localizedDescription)) - } - - let permissionsDict = permissions.mapValues { $0.toMap() } - result(permissionsDict) - } + qonversionSandwich?.checkPermissions(getDefaultCompletion(result)) } - + private func restore(_ result: @escaping FlutterResult) { - Qonversion.restore { (permissions, error) in - if let error = error { - return result(FlutterError.qonversionError(error.localizedDescription)) - } - - let permissionsDict = permissions.mapValues { $0.toMap() } - result(permissionsDict) - } + qonversionSandwich?.restore(getDefaultCompletion(result)) } - + private func offerings(_ result: @escaping FlutterResult) { - Qonversion.offerings { offerings, error in - if let error = error { - result(FlutterError.offeringsError(error.localizedDescription)) - } - - guard let offerings = offerings else { - return result(nil) - } - - - result(offerings.toMap().toJson()) - } + qonversionSandwich?.offerings(getJsonCompletion(result)) } - - private func setUserId(_ userId: String?, _ result: @escaping FlutterResult) { - guard let userId = userId else { - result(FlutterError.noUserId) - return - } - - Qonversion.setUserID(userId) - result(nil) - } - - private func setProperty(_ args: [String: Any], _ result: @escaping FlutterResult) { + + private func setDefinedUserProperty(_ args: [String: Any], _ result: @escaping FlutterResult) { guard let rawProperty = args["property"] as? String else { return result(FlutterError.noProperty) } - + guard let value = args["value"] as? String else { return result(FlutterError.noPropertyValue) } - - do { - let property = try Qonversion.Property.fromString(rawProperty) - - Qonversion.setProperty(property, value: value) - result(nil) - } catch ParsingError.runtimeError(let message) { - result(FlutterError.parsingError(message)) - } catch { - result(FlutterError.qonversionError(error.localizedDescription)) - } + + qonversionSandwich?.setDefinedProperty(rawProperty, value: value) + result(nil) } - - private func setUserProperty(_ args: [String: Any], _ result: @escaping FlutterResult) { + + private func setCustomUserProperty(_ args: [String: Any], _ result: @escaping FlutterResult) { guard let property = args["property"] as? String else { return result(FlutterError.noProperty) } - + guard let value = args["value"] as? String else { return result(FlutterError.noPropertyValue) } - - Qonversion.setUserProperty(property, value: value) - + + qonversionSandwich?.setCustomProperty(property, value: value) result(nil) } - + private func checkTrialIntroEligibility(_ args: [String: Any], _ result: @escaping FlutterResult) { guard let ids = args["ids"] as? [String] else { return result(FlutterError.noData) } - - Qonversion.checkTrialIntroEligibility(forProductIds: ids) { eligibilities, error in - if let error = error { - return result(FlutterError.qonversionError(error.localizedDescription)) - } - - result(eligibilities.mapValues { $0.toMap() }.toJson()) - } + + qonversionSandwich?.checkTrialIntroEligibility(ids, completion: getJsonCompletion(result)) } - + private func storeSdkInfo(_ args: [String: Any], _ result: @escaping FlutterResult) { guard let version = args["version"] as? String, - let source = args["source"] as? String, - let sourceKey = args["sourceKey"] as? String, - let versionKey = args["versionKey"] as? String + let source = args["source"] as? String else { return result(FlutterError.noSdkInfo) } - - let defaults = UserDefaults.standard - defaults.set(version, forKey: versionKey) - defaults.set(source, forKey: sourceKey) - + + qonversionSandwich?.storeSdkInfo(source: source, version: version) result(nil) } - + private func addAttributionData(_ args: [String: Any], _ result: @escaping FlutterResult) { - guard let data = args["data"] as? [AnyHashable: Any] else { + guard let data = args["data"] as? [String: Any] else { return result(FlutterError.noData) } - + guard let provider = args["provider"] as? String else { return result(FlutterError.noProvider) } - - // Using appsFlyer by default since there are only 2 cases in an enum yet. - var castedProvider = Qonversion.AttributionProvider.appsFlyer - - switch provider { - case "branch": - castedProvider = Qonversion.AttributionProvider.branch - default: - break - } - - Qonversion.addAttributionData(data, from: castedProvider) - + + qonversionSandwich?.addAttributionData(sourceKey: provider, value: data) result(nil) } @@ -325,25 +227,55 @@ public class SwiftQonversionFlutterSdkPlugin: NSObject, FlutterPlugin { return result(FlutterError.noLifetime) } - do { - let lifetime = try Qonversion.PermissionsCacheLifetime.fromString(rawLifetime) + qonversionSandwich?.setPermissionsCacheLifetime(rawLifetime) + result(nil) + } + + private func getDefaultCompletion(_ result: @escaping FlutterResult) -> BridgeCompletion { + return { data, error in + if let error = error { + return result(FlutterError.sandwichError(error)) + } + + result(data) + } + } + + private func getJsonCompletion(_ result: @escaping FlutterResult) -> BridgeCompletion { + return { data, error in + if let error = error { + return result(FlutterError.sandwichError(error)) + } + + guard let data = data else { + return result(nil) + } - Qonversion.setPermissionsCacheLifetime(lifetime) - result(nil) - } catch ParsingError.runtimeError(let message) { - result(FlutterError.parsingError(message)) - } catch { - if let nsError = error as NSError? { - result(FlutterError.qonversionError(nsError)) + guard let jsonData = data.toJson() else { + return result(FlutterError.serializationError) } + + result(jsonData) + } + } + + private func getPurchaseCompletion(_ result: @escaping FlutterResult) -> BridgeCompletion { + return { data, error in + if let error = error { + return result(FlutterError.purchaseError(error)) + } + + result(data) } } } -extension SwiftQonversionFlutterSdkPlugin: Qonversion.PurchasesDelegate { - public func qonversionDidReceiveUpdatedPermissions(_ permissions: [String : Qonversion.Permission]) { - let payload = permissions.mapValues { $0.toMap() }.toJson() - - purchasesEventStreamHandler?.eventSink?(payload) +extension SwiftQonversionFlutterSdkPlugin: QonversionEventListener { + public func shouldPurchasePromoProduct(with productId: String) { + // Promo purchases are not supported on MacOS. + } + + public func qonversionDidReceiveUpdatedPermissions(_ permissions: [String : Any]) { + deferredPurchasesStreamHandler?.eventSink?(permissions) } } diff --git a/macos/qonversion_flutter.podspec b/macos/qonversion_flutter.podspec index 3d373be5..f71755e1 100644 --- a/macos/qonversion_flutter.podspec +++ b/macos/qonversion_flutter.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |s| s.source_files = 'Classes/**/*' s.dependency 'FlutterMacOS' s.platform = :osx, '10.12' - s.dependency 'Qonversion', '2.20.0' + s.dependency 'QonversionSandwich', '0.0.14' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } s.swift_version = '5.0' s.static_framework = true