diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index ed67d9ae..9b26938f 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -19,7 +19,7 @@ jobs: - uses: subosito/flutter-action@v1 name: Setup Flutter with: - flutter-version: '3.7.7' + flutter-version: '3.32.4' channel: 'stable' - run: flutter pub get - name: Validation diff --git a/CHANGELOG.md b/CHANGELOG.md index 29e2c5b8..30e67897 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## 10.0.0 +### Introducing Qonversion No-Codes Beta! + +**Qonversion No-Codes** is a product designed to help you **build and customize** paywall screens **without writing code**. +It allows seamless integration of pre-built subscription UI components, enabling a faster and more flexible way to design paywalls directly within your app. +See more in the [documentation](https://documentation.qonversion.io/docs/getting-started-with-no-code-screens/). + +With this update, we are **removing** deprecated **Automations**, so we encourage you to transition your paywalls to the new Qonversion No-Codes. + ## 9.3.1 * Android and iOS stability improvements. diff --git a/android/build.gradle b/android/build.gradle index 015fcda6..af8fc673 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -5,6 +5,7 @@ buildscript { ext.kotlin_version = '1.6.10' repositories { google() + mavenCentral() jcenter() } @@ -17,6 +18,7 @@ buildscript { rootProject.allprojects { repositories { google() + mavenCentral() jcenter() mavenLocal() } @@ -51,6 +53,6 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation "io.qonversion:sandwich:5.2.1" + implementation "io.qonversion:sandwich:6.0.8" implementation 'com.google.code.gson:gson:2.9.0' } diff --git a/android/src/main/kotlin/com/qonversion/flutter/sdk/qonversion_flutter_sdk/AutomationsPlugin.kt b/android/src/main/kotlin/com/qonversion/flutter/sdk/qonversion_flutter_sdk/AutomationsPlugin.kt deleted file mode 100644 index 02e0892c..00000000 --- a/android/src/main/kotlin/com/qonversion/flutter/sdk/qonversion_flutter_sdk/AutomationsPlugin.kt +++ /dev/null @@ -1,112 +0,0 @@ -package com.qonversion.flutter.sdk.qonversion_flutter_sdk - -import com.google.gson.Gson -import io.flutter.plugin.common.BinaryMessenger -import io.flutter.plugin.common.MethodChannel -import io.qonversion.sandwich.AutomationsEventListener -import io.qonversion.sandwich.AutomationsSandwich -import io.qonversion.sandwich.BridgeData - -class AutomationsPlugin(private val messenger: BinaryMessenger) : AutomationsEventListener { - private var shownScreensStreamHandler: BaseEventStreamHandler? = null - private var startedActionsStreamHandler: BaseEventStreamHandler? = null - private var failedActionsStreamHandler: BaseEventStreamHandler? = null - private var finishedActionsStreamHandler: BaseEventStreamHandler? = null - private var finishedAutomationsStreamHandler: BaseEventStreamHandler? = null - - private val automationSandwich by lazy { - AutomationsSandwich() - } - - init { - setup() - } - - companion object { - private const val EVENT_CHANNEL_SHOWN_SCREENS = "shown_screens" - private const val EVENT_CHANNEL_STARTED_ACTIONS = "started_actions" - private const val EVENT_CHANNEL_FAILED_ACTIONS = "failed_actions" - private const val EVENT_CHANNEL_FINISHED_ACTIONS = "finished_actions" - private const val EVENT_CHANNEL_FINISHED_AUTOMATIONS = "finished_automations" - } - - override fun onAutomationEvent(event: AutomationsEventListener.Event, payload: BridgeData?) { - val (data, stream) = when (event) { - AutomationsEventListener.Event.ScreenShown -> Pair(Gson().toJson(payload), shownScreensStreamHandler) - AutomationsEventListener.Event.ActionStarted -> Pair(Gson().toJson(payload), startedActionsStreamHandler) - AutomationsEventListener.Event.ActionFinished -> Pair(Gson().toJson(payload), finishedActionsStreamHandler) - AutomationsEventListener.Event.ActionFailed -> Pair(Gson().toJson(payload), failedActionsStreamHandler) - AutomationsEventListener.Event.AutomationsFinished -> Pair(payload, finishedAutomationsStreamHandler) - } - - stream?.eventSink?.success(data) - } - - fun subscribe() { - automationSandwich.setDelegate(this) - } - - fun setNotificationsToken(token: String?, result: MethodChannel.Result) { - token?.let { - automationSandwich.setNotificationToken(it) - result.success(null) - } ?: result.noNecessaryDataError() - } - - fun handleNotification(args: Map, result: MethodChannel.Result) { - @Suppress("UNCHECKED_CAST") - val data = args["notificationData"] as? Map ?: return result.noNecessaryDataError() - - if (data.isEmpty()) { - return result.noNecessaryDataError() - } - - val isQonversionNotification = automationSandwich.handleNotification(data) - result.success(isQonversionNotification) - } - - fun getNotificationCustomPayload(args: Map, result: MethodChannel.Result) { - @Suppress("UNCHECKED_CAST") - val data = args["notificationData"] as? Map ?: return result.noNecessaryDataError() - - if (data.isEmpty()) { - return result.noNecessaryDataError() - } - - val payload = automationSandwich.getNotificationCustomPayload(data) - val payloadJson = Gson().toJson(payload) - result.success(payloadJson) - } - - fun showScreen(screenId: String?, result: MethodChannel.Result) { - screenId ?: return result.noNecessaryDataError() - automationSandwich.showScreen(screenId, result.toResultListener()) - } - - fun setScreenPresentationConfig(config: Map?, screenId: String?, result: MethodChannel.Result) { - config ?: return result.noNecessaryDataError() - automationSandwich.setScreenPresentationConfig(config, screenId) - } - - private fun setup() { - val shownScreensListener = BaseListenerWrapper(messenger, EVENT_CHANNEL_SHOWN_SCREENS) - shownScreensListener.register() - shownScreensStreamHandler = shownScreensListener.eventStreamHandler - - val startedActionsListener = BaseListenerWrapper(messenger, EVENT_CHANNEL_STARTED_ACTIONS) - startedActionsListener.register() - startedActionsStreamHandler = startedActionsListener.eventStreamHandler - - val failedActionsListener = BaseListenerWrapper(messenger, EVENT_CHANNEL_FAILED_ACTIONS) - failedActionsListener.register() - failedActionsStreamHandler = failedActionsListener.eventStreamHandler - - val finishedActionsListener = BaseListenerWrapper(messenger, EVENT_CHANNEL_FINISHED_ACTIONS) - finishedActionsListener.register() - finishedActionsStreamHandler = finishedActionsListener.eventStreamHandler - - val finishedAutomationsListener = BaseListenerWrapper(messenger, EVENT_CHANNEL_FINISHED_AUTOMATIONS) - finishedAutomationsListener.register() - finishedAutomationsStreamHandler = finishedAutomationsListener.eventStreamHandler - } -} \ No newline at end of file diff --git a/android/src/main/kotlin/com/qonversion/flutter/sdk/qonversion_flutter_sdk/NoCodesPlugin.kt b/android/src/main/kotlin/com/qonversion/flutter/sdk/qonversion_flutter_sdk/NoCodesPlugin.kt new file mode 100644 index 00000000..23d9e919 --- /dev/null +++ b/android/src/main/kotlin/com/qonversion/flutter/sdk/qonversion_flutter_sdk/NoCodesPlugin.kt @@ -0,0 +1,126 @@ +package com.qonversion.flutter.sdk.qonversion_flutter_sdk + +import android.content.Context +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.MethodChannel.Result +import io.qonversion.sandwich.BridgeData +import io.qonversion.sandwich.NoCodesEventListener +import io.qonversion.sandwich.NoCodesSandwich +import com.google.gson.Gson + +class NoCodesPlugin(private val messenger: BinaryMessenger, private val context: Context) : NoCodesEventListener { + private var noCodesSandwich: NoCodesSandwich? = null + private val gson = Gson() + + // Separate event stream handlers for each event type + private var screenShownEventStreamHandler: BaseEventStreamHandler? = null + private var finishedEventStreamHandler: BaseEventStreamHandler? = null + private var actionStartedEventStreamHandler: BaseEventStreamHandler? = null + private var actionFailedEventStreamHandler: BaseEventStreamHandler? = null + private var actionFinishedEventStreamHandler: BaseEventStreamHandler? = null + private var screenFailedToLoadEventStreamHandler: BaseEventStreamHandler? = null + + companion object { + private const val SCREEN_SHOWN_EVENT_CHANNEL = "nocodes_screen_shown" + private const val FINISHED_EVENT_CHANNEL = "nocodes_finished" + private const val ACTION_STARTED_EVENT_CHANNEL = "nocodes_action_started" + private const val ACTION_FAILED_EVENT_CHANNEL = "nocodes_action_failed" + private const val ACTION_FINISHED_EVENT_CHANNEL = "nocodes_action_finished" + private const val SCREEN_FAILED_TO_LOAD_EVENT_CHANNEL = "nocodes_screen_failed_to_load" + } + + init { + setup() + } + + private fun setup() { + // Register separate event channels for each event type + val screenShownListener = BaseListenerWrapper(messenger, SCREEN_SHOWN_EVENT_CHANNEL) + screenShownListener.register() + this.screenShownEventStreamHandler = screenShownListener.eventStreamHandler + + val finishedListener = BaseListenerWrapper(messenger, FINISHED_EVENT_CHANNEL) + finishedListener.register() + this.finishedEventStreamHandler = finishedListener.eventStreamHandler + + val actionStartedListener = BaseListenerWrapper(messenger, ACTION_STARTED_EVENT_CHANNEL) + actionStartedListener.register() + this.actionStartedEventStreamHandler = actionStartedListener.eventStreamHandler + + val actionFailedListener = BaseListenerWrapper(messenger, ACTION_FAILED_EVENT_CHANNEL) + actionFailedListener.register() + this.actionFailedEventStreamHandler = actionFailedListener.eventStreamHandler + + val actionFinishedListener = BaseListenerWrapper(messenger, ACTION_FINISHED_EVENT_CHANNEL) + actionFinishedListener.register() + this.actionFinishedEventStreamHandler = actionFinishedListener.eventStreamHandler + + val screenFailedToLoadListener = BaseListenerWrapper(messenger, SCREEN_FAILED_TO_LOAD_EVENT_CHANNEL) + screenFailedToLoadListener.register() + this.screenFailedToLoadEventStreamHandler = screenFailedToLoadListener.eventStreamHandler + } + + fun initializeNoCodes(projectKey: String, result: Result) { + if (projectKey.isNotEmpty()) { + // Initialize NoCodes Sandwich + noCodesSandwich = NoCodesSandwich() + noCodesSandwich?.initialize(context, projectKey) + noCodesSandwich?.setDelegate(this) + result.success(null) + } else { + result.noNecessaryDataError() + } + } + + fun setScreenPresentationConfig(config: Map?, contextKey: String?, result: Result) { + if (config != null) { + noCodesSandwich?.setScreenPresentationConfig(config, contextKey) + result.success(null) + } else { + result.noNecessaryDataError() + } + } + + fun showNoCodesScreen(contextKey: String?, result: Result) { + if (contextKey != null) { + noCodesSandwich?.showScreen(contextKey) + result.success(null) + } else { + result.noNecessaryDataError() + } + } + + fun closeNoCodes(result: Result) { + noCodesSandwich?.close() + result.success(null) + } + + // NoCodesEventListener implementation + override fun onNoCodesEvent(event: NoCodesEventListener.Event, payload: BridgeData?) { + val eventData = mapOf("payload" to (payload ?: emptyMap())) + + // Convert to JSON string + val jsonString = gson.toJson(eventData) + + when (event) { + NoCodesEventListener.Event.ScreenShown -> { + screenShownEventStreamHandler?.eventSink?.success(jsonString) + } + NoCodesEventListener.Event.Finished -> { + finishedEventStreamHandler?.eventSink?.success(jsonString) + } + NoCodesEventListener.Event.ActionStarted -> { + actionStartedEventStreamHandler?.eventSink?.success(jsonString) + } + NoCodesEventListener.Event.ActionFailed -> { + actionFailedEventStreamHandler?.eventSink?.success(jsonString) + } + NoCodesEventListener.Event.ActionFinished -> { + actionFinishedEventStreamHandler?.eventSink?.success(jsonString) + } + NoCodesEventListener.Event.ScreenFailedToLoad -> { + screenFailedToLoadEventStreamHandler?.eventSink?.success(jsonString) + } + } + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/com/qonversion/flutter/sdk/qonversion_flutter_sdk/QonversionPlugin.kt b/android/src/main/kotlin/com/qonversion/flutter/sdk/qonversion_flutter_sdk/QonversionPlugin.kt index 63616ea9..041d6284 100644 --- a/android/src/main/kotlin/com/qonversion/flutter/sdk/qonversion_flutter_sdk/QonversionPlugin.kt +++ b/android/src/main/kotlin/com/qonversion/flutter/sdk/qonversion_flutter_sdk/QonversionPlugin.kt @@ -17,13 +17,14 @@ import io.qonversion.sandwich.QonversionEventsListener import io.qonversion.sandwich.QonversionSandwich class QonversionPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { - private var activity: Activity? = null private var application: Application? = null + private var activity: Activity? = null private var channel: MethodChannel? = null private var updatedEntitlementsStreamHandler: BaseEventStreamHandler? = null + private var noCodesPlugin: NoCodesPlugin? = null private val qonversionSandwich by lazy { - application?.let { + application?.let { QonversionSandwich( it, object : ActivityProvider { @@ -35,8 +36,6 @@ class QonversionPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { } ?: throw IllegalStateException("Failed to initialize Qonversion Sandwich. Application is null.") } - private lateinit var automationsPlugin: AutomationsPlugin - private val qonversionEventsListener: QonversionEventsListener = object : QonversionEventsListener { override fun onEntitlementsUpdated(entitlements: BridgeData) { val payload = Gson().toJson(entitlements) @@ -109,12 +108,10 @@ class QonversionPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { "isFallbackFileAccessible" -> { return isFallbackFileAccessible(result) } - "automationsSubscribe" -> { - return automationsPlugin.subscribe() - } "remoteConfigList" -> { return remoteConfigList(result) } + "closeNoCodes" -> noCodesPlugin?.closeNoCodes(result) } // Methods with args @@ -135,15 +132,10 @@ class QonversionPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { "detachUserFromRemoteConfiguration" -> detachUserFromRemoteConfiguration(args, result) "storeSdkInfo" -> storeSdkInfo(args, result) "identify" -> identify(args["userId"] as? String, result) - "automationsSetNotificationsToken" -> automationsPlugin.setNotificationsToken(args["notificationsToken"] as? String, result) - "automationsHandleNotification" -> automationsPlugin.handleNotification(args, result) - "automationsGetNotificationCustomPayload" -> automationsPlugin.getNotificationCustomPayload(args, result) - "automationsShowScreen" -> automationsPlugin.showScreen(args["screenId"] as? String, result) - "setScreenPresentationConfig" -> automationsPlugin.setScreenPresentationConfig( - args["configData"] as? Map, - args["screenId"] as? String, - result - ) + // NoCodes methods + "initializeNoCodes" -> noCodesPlugin?.initializeNoCodes(args["projectKey"] as? String ?: "", result) + "setScreenPresentationConfig" -> noCodesPlugin?.setScreenPresentationConfig(args["config"] as? Map, args["contextKey"] as? String, result) + "showNoCodesScreen" -> noCodesPlugin?.showNoCodesScreen(args["contextKey"] as? String, result) else -> result.notImplemented() } } @@ -317,6 +309,13 @@ class QonversionPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { channel = MethodChannel(messenger, METHOD_CHANNEL) channel?.setMethodCallHandler(this) + // Register NoCodes plugin + try { + noCodesPlugin = NoCodesPlugin(messenger, application) + } catch (e: Exception) { + println("Failed to initialize NoCodesPlugin: ${e.message}") + } + // Register entitlements update events val updatedEntitlementsListener = BaseListenerWrapper(messenger, EVENT_CHANNEL_UPDATED_ENTITLEMENTS) updatedEntitlementsListener.register() @@ -325,14 +324,13 @@ class QonversionPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { // Register promo purchases events. Android SDK does not generate any promo purchases yet val promoPurchasesListener = BaseListenerWrapper(messenger, EVENT_CHANNEL_PROMO_PURCHASES) promoPurchasesListener.register() - - automationsPlugin = AutomationsPlugin(messenger) } private fun tearDown() { channel?.setMethodCallHandler(null) channel = null this.updatedEntitlementsStreamHandler = null + this.noCodesPlugin = null this.application = null } } diff --git a/example/ios/Flutter/ephemeral/flutter_lldb_helper.py b/example/ios/Flutter/ephemeral/flutter_lldb_helper.py new file mode 100644 index 00000000..a88caf99 --- /dev/null +++ b/example/ios/Flutter/ephemeral/flutter_lldb_helper.py @@ -0,0 +1,32 @@ +# +# Generated file, do not edit. +# + +import lldb + +def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict): + """Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages.""" + base = frame.register["x0"].GetValueAsAddress() + page_len = frame.register["x1"].GetValueAsUnsigned() + + # Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the + # first page to see if handled it correctly. This makes diagnosing + # misconfiguration (e.g. missing breakpoint) easier. + data = bytearray(page_len) + data[0:8] = b'IHELPED!' + + error = lldb.SBError() + frame.GetThread().GetProcess().WriteMemory(base, data, error) + if not error.Success(): + print(f'Failed to write into {base}[+{page_len}]', error) + return + +def __lldb_init_module(debugger: lldb.SBDebugger, _): + target = debugger.GetDummyTarget() + # Caveat: must use BreakpointCreateByRegEx here and not + # BreakpointCreateByName. For some reasons callback function does not + # get carried over from dummy target for the later. + bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$") + bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__)) + bp.SetAutoContinue(True) + print("-- LLDB integration loaded --") diff --git a/example/ios/Flutter/ephemeral/flutter_lldbinit b/example/ios/Flutter/ephemeral/flutter_lldbinit new file mode 100644 index 00000000..e3ba6fbe --- /dev/null +++ b/example/ios/Flutter/ephemeral/flutter_lldbinit @@ -0,0 +1,5 @@ +# +# Generated file, do not edit. +# + +command script import --relative-to-command-file flutter_lldb_helper.py diff --git a/example/ios/Podfile b/example/ios/Podfile index eb8b0f9a..728c1451 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project - platform :ios, '12.0' + platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 4ee676fa..8f347fbe 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -267,12 +267,14 @@ ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/NoCodes/NoCodes.framework", "${BUILT_PRODUCTS_DIR}/Qonversion/Qonversion.framework", "${BUILT_PRODUCTS_DIR}/QonversionSandwich/QonversionSandwich.framework", "${BUILT_PRODUCTS_DIR}/qonversion_flutter/qonversion_flutter.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/NoCodes.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Qonversion.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/QonversionSandwich.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/qonversion_flutter.framework", @@ -425,7 +427,7 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -566,7 +568,7 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -602,7 +604,7 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index e67b2808..fc5ae031 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -26,6 +26,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> @@ -45,11 +46,13 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift index fd640c8b..6e7eede6 100644 --- a/example/ios/Runner/AppDelegate.swift +++ b/example/ios/Runner/AppDelegate.swift @@ -9,25 +9,6 @@ import Qonversion didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { GeneratedPluginRegistrant.register(with: self) - - if #available(iOS 10.0, *) { - UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate - } - return super.application(application, didFinishLaunchingWithOptions: launchOptions) } } - -extension AppDelegate { - override func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { - let isPushHandled: Bool = Qonversion.Automations.shared().handleNotification(response.notification.request.content.userInfo) - if !isPushHandled { - // Qonversion can not handle this push. - } - completionHandler() - } - - override func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { - completionHandler([.alert, .badge, .sound]) - } -} diff --git a/example/lib/home.dart b/example/lib/home.dart index 9588e339..7ff38aca 100644 --- a/example/lib/home.dart +++ b/example/lib/home.dart @@ -14,51 +14,10 @@ class _HomeViewState extends State { Map? _entitlements = null; Map? _products = null; - late StreamSubscription _shownScreensStream; - late StreamSubscription _startedActionsStream; - late StreamSubscription _failedActionsStream; - late StreamSubscription _finishedActionsStream; - late StreamSubscription _finishedAutomationsStream; - @override void initState() { super.initState(); _initPlatformState(); - - _shownScreensStream = - Automations.getSharedInstance().shownScreensStream.listen((event) { - // do any logic you need - }); - _startedActionsStream = - Automations.getSharedInstance().startedActionsStream.listen((event) { - // do any logic you need or track event - }); - _failedActionsStream = - Automations.getSharedInstance().failedActionsStream.listen((event) { - // do any logic you need or track event - }); - _finishedActionsStream = - Automations.getSharedInstance().finishedActionsStream.listen((event) { - if (event.type == ActionResultType.purchase) { - // do any logic you need - } - }); - _finishedAutomationsStream = Automations.getSharedInstance() - .finishedAutomationsStream - .listen((event) { - // do any logic you need or track event - }); - } - - @override - void dispose() { - _shownScreensStream.cancel(); - _startedActionsStream.cancel(); - _failedActionsStream.cancel(); - _finishedActionsStream.cancel(); - _finishedAutomationsStream.cancel(); - - super.dispose(); } @override @@ -122,6 +81,22 @@ class _HomeViewState extends State { Navigator.of(context).pushNamed('params'), ), ), + Padding( + padding: const EdgeInsets.only( + left: 8, + right: 8, + bottom: 8, + ), + child: TextButton( + child: Text('Open NoCodesView'), + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all(Colors.purple), + foregroundColor: WidgetStateProperty.all(Colors.white), + ), + onPressed: () => + Navigator.of(context).pushNamed('nocodes'), + ), + ), if (Platform.isAndroid) Padding( padding: const EdgeInsets.only( @@ -150,13 +125,20 @@ class _HomeViewState extends State { Future _initPlatformState() async { const environment = kDebugMode ? QEnvironment.sandbox : QEnvironment.production; + const projectKey = 'PV77YHL7qnGvsdmpTs7gimsxUvY-Znl2'; + final config = new QonversionConfigBuilder( - 'PV77YHL7qnGvsdmpTs7gimsxUvY-Znl2', + projectKey, QLaunchMode.subscriptionManagement) .setEnvironment(environment) .build(); Qonversion.initialize(config); Qonversion.getSharedInstance().collectAppleSearchAdsAttribution(); + + // Initialize NoCodes with the same project key using config builder + final noCodesConfig = new NoCodesConfigBuilder(projectKey).build(); + NoCodes.initialize(noCodesConfig); + _loadQonversionObjects(); } diff --git a/example/lib/main.dart b/example/lib/main.dart index de9f4889..a3f2c261 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'home.dart'; import 'params_view.dart'; import 'products_view.dart'; +import 'nocodes_view.dart'; import 'dart:async'; Future main() async { @@ -20,7 +21,8 @@ class SampleApp extends StatelessWidget { routes: { '/': (_) => HomeView(), 'products': (_) => ProductsView(), - 'params': (_) => ParamsView() + 'params': (_) => ParamsView(), + 'nocodes': (_) => NoCodesView(), }, ); } diff --git a/example/lib/nocodes_view.dart b/example/lib/nocodes_view.dart new file mode 100644 index 00000000..791bf097 --- /dev/null +++ b/example/lib/nocodes_view.dart @@ -0,0 +1,238 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:qonversion_flutter/qonversion_flutter.dart'; + +const String CONTEXT_KEY = 'your_context_key'; + +class NoCodesView extends StatefulWidget { + @override + _NoCodesViewState createState() => _NoCodesViewState(); +} + +class _NoCodesViewState extends State { + List _events = []; + + // Separate stream subscriptions for each NoCodes event type + late StreamSubscription _screenShownStream; + late StreamSubscription _finishedStream; + late StreamSubscription _actionStartedStream; + late StreamSubscription _actionFailedStream; + late StreamSubscription _actionFinishedStream; + late StreamSubscription _screenFailedToLoadStream; + + @override + void initState() { + super.initState(); + _setupNoCodesEventListeners(); + } + + @override + void dispose() { + // Dispose all stream subscriptions + _screenShownStream.cancel(); + _finishedStream.cancel(); + _actionStartedStream.cancel(); + _actionFailedStream.cancel(); + _actionFinishedStream.cancel(); + _screenFailedToLoadStream.cancel(); + super.dispose(); + } + + void _setupNoCodesEventListeners() { + // Subscribe to separate NoCodes event streams + _screenShownStream = NoCodes.getSharedInstance().screenShownStream.listen((event) { + _addEvent('Screen Shown: ${event.payload}'); + }); + + _finishedStream = NoCodes.getSharedInstance().finishedStream.listen((event) { + _addEvent('Finished: ${event.payload}'); + }); + + _actionStartedStream = NoCodes.getSharedInstance().actionStartedStream.listen((event) { + _addEvent('Action Started: ${event.payload}'); + }); + + _actionFailedStream = NoCodes.getSharedInstance().actionFailedStream.listen((event) { + _addEvent('Action Failed: ${event.payload}'); + }); + + _actionFinishedStream = NoCodes.getSharedInstance().actionFinishedStream.listen((event) { + _addEvent('Action Finished: ${event.payload}'); + }); + + _screenFailedToLoadStream = NoCodes.getSharedInstance().screenFailedToLoadStream.listen((event) { + _addEvent('Screen Failed to Load: ${event.payload}'); + NoCodes.getSharedInstance().close(); + }); + } + + void _addEvent(String event) { + setState(() { + _events.insert(0, '${DateTime.now().toString().substring(11, 19)}: $event'); + if (_events.length > 20) { + _events.removeLast(); + } + }); + } + + Future _showScreen() async { + try { + // Set presentation style config before showing screen + final config = NoCodesPresentationConfig( + animated: true, + presentationStyle: NoCodesPresentationStyle.fullScreen, + ); + + await NoCodes.getSharedInstance().setScreenPresentationConfig(config, contextKey: CONTEXT_KEY); + _addEvent('Presentation config set'); + + await NoCodes.getSharedInstance().showScreen(CONTEXT_KEY); + } catch (e) { + print('Error showing screen: $e'); + _addEvent('Error showing screen: $e'); + } + } + + Future _close() async { + try { + await NoCodes.getSharedInstance().close(); + } catch (e) { + print('Error closing screen: $e'); + _addEvent('Error closing screen: $e'); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('NoCodes Events'), + actions: [ + IconButton( + icon: Icon(Icons.clear), + onPressed: () { + setState(() { + _events.clear(); + }); + }, + ), + ], + ), + body: Column( + children: [ + // NoCodes Controls Section + Container( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + Text( + 'NoCodes Controls', + style: Theme.of(context).textTheme.headlineSmall, + ), + SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton( + onPressed: _showScreen, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + ), + child: Text('Show Screen'), + ), + ElevatedButton( + onPressed: _close, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + ), + child: Text('Close'), + ), + ], + ), + ], + ), + ), + + Divider(), + + // Events Section + Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + 'NoCodes Event Streams', + style: Theme.of(context).textTheme.headlineSmall, + ), + ), + Expanded( + child: _events.isEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.event_note, + size: 64, + color: Colors.grey, + ), + SizedBox(height: 16), + Text( + 'No events received yet.\nTry showing a NoCodes screen.', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyLarge, + ), + ], + ), + ) + : ListView.builder( + itemCount: _events.length, + itemBuilder: (context, index) { + return ListTile( + title: Text(_events[index]), + dense: true, + leading: Icon(Icons.event, size: 16), + ); + }, + ), + ), + + // Event Types Info + Container( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + Text( + 'Available Event Streams:', + style: Theme.of(context).textTheme.titleMedium, + ), + SizedBox(height: 8), + Wrap( + spacing: 8, + children: [ + _buildEventChip('Screen Shown', Colors.green), + _buildEventChip('Finished', Colors.blue), + _buildEventChip('Action Started', Colors.orange), + _buildEventChip('Action Failed', Colors.red), + _buildEventChip('Action Finished', Colors.purple), + _buildEventChip('Screen Failed', Colors.grey), + ], + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildEventChip(String label, Color color) { + return Chip( + label: Text( + label, + style: TextStyle(color: Colors.white, fontSize: 12), + ), + backgroundColor: color, + ); + } +} \ No newline at end of file diff --git a/ios/Classes/AutomationsPlugin.swift b/ios/Classes/AutomationsPlugin.swift deleted file mode 100644 index a2749df4..00000000 --- a/ios/Classes/AutomationsPlugin.swift +++ /dev/null @@ -1,120 +0,0 @@ -// -// AutomationsPlugin.swift -// qonversion_flutter -// -// Created by Maria on 18.11.2021. -// - -import QonversionSandwich - -public class AutomationsPlugin: NSObject { - private let eventChannelShownScreens = "shown_screens" - private let eventChannelStartedActions = "started_actions" - private let eventChannelFailedActions = "failed_actions" - private let eventChannelFinishedActions = "finished_actions" - private let eventChannelFinishedAutomations = "finished_automations" - - private var shownScreensStreamHandler: BaseEventStreamHandler? - private var startedActionsStreamHandler: BaseEventStreamHandler? - private var failedActionsStreamHandler: BaseEventStreamHandler? - 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 } - - let startedActionsListener = FlutterListenerWrapper(registrar, postfix: eventChannelStartedActions) - startedActionsListener.register() { self.startedActionsStreamHandler = $0 } - - let failedActionsListener = FlutterListenerWrapper(registrar, postfix: eventChannelFailedActions) - failedActionsListener.register() { self.failedActionsStreamHandler = $0 } - - let finishedActionsListener = FlutterListenerWrapper(registrar, postfix: eventChannelFinishedActions) - finishedActionsListener.register() { self.finishedActionsStreamHandler = $0 } - - let finishedAutomationsListener = FlutterListenerWrapper(registrar, postfix: eventChannelFinishedAutomations) - finishedAutomationsListener.register() { self.finishedAutomationsStreamHandler = $0 } - } - - public func subscribe() { - automationSandwich.subscribe(self) - } - - public func setNotificationsToken(_ token: String?, _ result: @escaping FlutterResult) { - guard let token = token else { - result(FlutterError.noNecessaryData) - return - } - - automationSandwich.setNotificationToken(token) - result(nil) - } - - public func handleNotification(_ args: [AnyHashable: Any], _ result: @escaping FlutterResult) { - guard let notificationData = args["notificationData"] as? [AnyHashable: Any] else { - return result(FlutterError.noNecessaryData) - } - - let isPushHandled: Bool = automationSandwich.handleNotification(notificationData) - result(isPushHandled) - } - - public func getNotificationCustomPayload(_ args: [AnyHashable: Any], _ result: @escaping FlutterResult) { - guard let notificationData = args["notificationData"] as? [AnyHashable: Any] else { - return result(FlutterError.noNecessaryData) - } - - let customPayload: [AnyHashable: Any]? = automationSandwich.getNotificationCustomPayload(notificationData) - result(customPayload?.toJson()) - } - - public func showScreen(_ screenId: String?, _ result: @escaping FlutterResult) { - guard let screenId = screenId else { - return result(FlutterError.noNecessaryData) - } - - automationSandwich.showScreen(screenId) { data, error in - if let error = error { - return result(FlutterError.sandwichError(error)) - } - - result(data) - } - } - - public func setScreenPresentationConfig(_ configData: [String: Any]?, _ screenId: String?, _ result: @escaping FlutterResult) { - guard let configData = configData else { - result(FlutterError.noNecessaryData) - return - } - - automationSandwich.setScreenPresentationConfig(configData, forScreenId:screenId) - result(nil) - } -} - -extension AutomationsPlugin: AutomationsEventListener { - - 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/BaseListenerWrapper.swift b/ios/Classes/BaseListenerWrapper.swift index fc968742..91d042f1 100644 --- a/ios/Classes/BaseListenerWrapper.swift +++ b/ios/Classes/BaseListenerWrapper.swift @@ -38,12 +38,15 @@ class FlutterListenerWrapper: NSObject where T: EventStreamHandler { #endif eventStreamHandler = T() + + let channelName = "qonversion_flutter_\(eventChannelPostfix)" + if let codec = codec { - eventChannel = FlutterEventChannel(name: "qonversion_flutter_\(eventChannelPostfix)", + eventChannel = FlutterEventChannel(name: channelName, binaryMessenger: messenger, codec: codec) } else { - eventChannel = FlutterEventChannel(name: "qonversion_flutter_\(eventChannelPostfix)", + eventChannel = FlutterEventChannel(name: channelName, binaryMessenger: messenger) } diff --git a/ios/Classes/NoCodesPlugin.swift b/ios/Classes/NoCodesPlugin.swift new file mode 100644 index 00000000..c1cb83d7 --- /dev/null +++ b/ios/Classes/NoCodesPlugin.swift @@ -0,0 +1,145 @@ +// +// NoCodesPlugin.swift +// qonversion_flutter +// +// Created by Assistant on 08.05.2025. +// Copyright © 2025 Qonversion Inc. All rights reserved. +// + +import Foundation +import Flutter +import QonversionSandwich + +public class NoCodesPlugin: NSObject { + // Event type constants + private let eventScreenShown = "nocodes_screen_shown" + private let eventFinished = "nocodes_finished" + private let eventActionStarted = "nocodes_action_started" + private let eventActionFailed = "nocodes_action_failed" + private let eventActionFinished = "nocodes_action_finished" + private let eventScreenFailedToLoad = "nocodes_screen_failed_to_load" + private var screenShownEventStreamHandler: BaseEventStreamHandler? + private var finishedEventStreamHandler: BaseEventStreamHandler? + private var actionStartedEventStreamHandler: BaseEventStreamHandler? + private var actionFailedEventStreamHandler: BaseEventStreamHandler? + private var actionFinishedEventStreamHandler: BaseEventStreamHandler? + private var screenFailedToLoadEventStreamHandler: BaseEventStreamHandler? + private var noCodesSandwich: NoCodesSandwich? + + public func register(_ registrar: FlutterPluginRegistrar) { + + // Register separate event channels for each event type + let screenShownListener = FlutterListenerWrapper(registrar, postfix: eventScreenShown) + screenShownListener.register() { eventStreamHandler in + self.screenShownEventStreamHandler = eventStreamHandler + } + + let finishedListener = FlutterListenerWrapper(registrar, postfix: eventFinished) + finishedListener.register() { eventStreamHandler in + self.finishedEventStreamHandler = eventStreamHandler + } + + let actionStartedListener = FlutterListenerWrapper(registrar, postfix: eventActionStarted) + actionStartedListener.register() { eventStreamHandler in + self.actionStartedEventStreamHandler = eventStreamHandler + } + + let actionFailedListener = FlutterListenerWrapper(registrar, postfix: eventActionFailed) + actionFailedListener.register() { eventStreamHandler in + self.actionFailedEventStreamHandler = eventStreamHandler + } + + let actionFinishedListener = FlutterListenerWrapper(registrar, postfix: eventActionFinished) + actionFinishedListener.register() { eventStreamHandler in + self.actionFinishedEventStreamHandler = eventStreamHandler + } + + let screenFailedToLoadListener = FlutterListenerWrapper(registrar, postfix: eventScreenFailedToLoad) + screenFailedToLoadListener.register() { eventStreamHandler in + self.screenFailedToLoadEventStreamHandler = eventStreamHandler + } + + } + + public func initialize(_ args: [String: Any]?, _ result: @escaping FlutterResult) { + guard let args = args, + let projectKey = args["projectKey"] as? String else { + return result(FlutterError.noNecessaryData) + } + + if noCodesSandwich == nil { + noCodesSandwich = NoCodesSandwich(noCodesEventListener: self) + } + + noCodesSandwich?.initialize(projectKey: projectKey) + result(nil) + } + + @MainActor public func setScreenPresentationConfig(_ args: [String: Any]?, _ result: @escaping FlutterResult) { + guard let args = args, + let configData = args["config"] as? [String: Any] else { + return result(FlutterError.noNecessaryData) + } + + let contextKey = args["contextKey"] as? String + + noCodesSandwich?.setScreenPresentationConfig(configData, forContextKey: contextKey) + result(nil) + } + + @MainActor public func showScreen(_ args: [String: Any]?, _ result: @escaping FlutterResult) { + guard let args = args, + let contextKey = args["contextKey"] as? String else { + return result(FlutterError.noNecessaryData) + } + + noCodesSandwich?.showScreen(contextKey) + result(nil) + } + + @MainActor public func close(_ result: @escaping FlutterResult) { + noCodesSandwich?.close() + result(nil) + } +} + +extension NoCodesPlugin: NoCodesEventListener { + public func noCodesDidTrigger(event: String, payload: [String: Any]?) { + + let eventData: [String: Any] = [ + "payload": payload ?? [:] + ] + + // Convert to JSON string + guard let jsonData = try? JSONSerialization.data(withJSONObject: eventData), + let jsonString = String(data: jsonData, encoding: .utf8) else { + print("NoCodesPlugin: Failed to serialize event data to JSON") + return + } + + DispatchQueue.main.async { + switch event { + case self.eventScreenShown: + self.screenShownEventStreamHandler?.eventSink?(jsonString) + + case self.eventFinished: + self.finishedEventStreamHandler?.eventSink?(jsonString) + + case self.eventActionStarted: + self.actionStartedEventStreamHandler?.eventSink?(jsonString) + + case self.eventActionFailed: + self.actionFailedEventStreamHandler?.eventSink?(jsonString) + + case self.eventActionFinished: + self.actionFinishedEventStreamHandler?.eventSink?(jsonString) + + case self.eventScreenFailedToLoad: + self.screenFailedToLoadEventStreamHandler?.eventSink?(jsonString) + + default: + print("NoCodesPlugin: unknown event type: \(event)") + } + } + } +} diff --git a/ios/Classes/SwiftQonversionPlugin.swift b/ios/Classes/SwiftQonversionPlugin.swift index 64717aa0..36b3268e 100644 --- a/ios/Classes/SwiftQonversionPlugin.swift +++ b/ios/Classes/SwiftQonversionPlugin.swift @@ -10,7 +10,7 @@ public class SwiftQonversionPlugin: NSObject, FlutterPlugin { var updatedEntitlementsStreamHandler: BaseEventStreamHandler? var promoPurchasesStreamHandler: BaseEventStreamHandler? var qonversionSandwich: QonversionSandwich? - private var automationsPlugin: AutomationsPlugin? + var noCodesPlugin: NoCodesPlugin? public static func register(with registrar: FlutterPluginRegistrar) { let messenger: FlutterBinaryMessenger @@ -35,11 +35,12 @@ public class SwiftQonversionPlugin: NSObject, FlutterPlugin { let sandwichInstance = QonversionSandwich.init(qonversionEventListener: instance) instance.qonversionSandwich = sandwichInstance - instance.automationsPlugin = AutomationsPlugin() - instance.automationsPlugin?.register(registrar) + // Initialize NoCodesPlugin and register it + instance.noCodesPlugin = NoCodesPlugin() + instance.noCodesPlugin?.register(registrar) } - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + @MainActor public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { // MARK: - Calls without arguments @@ -87,12 +88,12 @@ public class SwiftQonversionPlugin: NSObject, FlutterPlugin { case "isFallbackFileAccessible": return isFallbackFileAccessible(result) - case "automationsSubscribe": - automationsPlugin?.subscribe() - return result(nil) - case "remoteConfigList": return remoteConfigList(result) + + case "closeNoCodes": + noCodesPlugin?.close(result) + return result(nil) default: break @@ -153,24 +154,16 @@ public class SwiftQonversionPlugin: NSObject, FlutterPlugin { case "detachUserFromRemoteConfiguration": return detachUserFromRemoteConfiguration(args, result) - case "automationsSetNotificationsToken": - automationsPlugin?.setNotificationsToken(args["notificationsToken"] as? String, result) - return - - case "automationsHandleNotification": - automationsPlugin?.handleNotification(args, result) + case "initializeNoCodes": + noCodesPlugin?.initialize(args, result) return - case "automationsGetNotificationCustomPayload": - automationsPlugin?.getNotificationCustomPayload(args, result) + case "setScreenPresentationConfig": + noCodesPlugin?.setScreenPresentationConfig(args, result) return - case "automationsShowScreen": - automationsPlugin?.showScreen(args["screenId"] as? String, result) - return - - case "setScreenPresentationConfig": - automationsPlugin?.setScreenPresentationConfig(args["configData"] as? [String: Any], args["screenId"] as? String, result) + case "showNoCodesScreen": + noCodesPlugin?.showScreen(args, result) return default: diff --git a/ios/qonversion_flutter.podspec b/ios/qonversion_flutter.podspec index 7928d6ee..0cd0bc4c 100644 --- a/ios/qonversion_flutter.podspec +++ b/ios/qonversion_flutter.podspec @@ -4,7 +4,7 @@ # Pod::Spec.new do |s| s.name = 'qonversion_flutter' - s.version = '5.0.0' + s.version = '9.3.0' s.summary = 'Flutter Qonversion SDK' s.description = <<-DESC Powerful yet simple subscription analytics @@ -15,8 +15,8 @@ Pod::Spec.new do |s| s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.dependency 'Flutter' - s.platform = :ios, '9.0' - s.dependency "QonversionSandwich", "5.2.1" + s.platform = :ios, '13.0' + s.dependency "QonversionSandwich", "6.0.8" # 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/qonversion_flutter.dart b/lib/qonversion_flutter.dart index 13990d95..f075cd54 100644 --- a/lib/qonversion_flutter.dart +++ b/lib/qonversion_flutter.dart @@ -1,8 +1,3 @@ -export 'src/automations.dart'; -export 'src/dto/automations/action_result.dart'; -export 'src/dto/automations/action_result_type.dart'; -export 'src/dto/automations/event.dart'; -export 'src/dto/automations/event_type.dart'; export 'src/dto/attribution_provider.dart'; export 'src/dto/eligibility.dart'; export 'src/dto/entitlement.dart'; @@ -30,8 +25,6 @@ export 'src/dto/remote_config_list.dart'; export 'src/dto/remote_configuration_source.dart'; export 'src/dto/remote_configuration_source_type.dart'; export 'src/dto/remote_configuration_assignment_type.dart'; -export 'src/dto/screen_presentation_config.dart'; -export 'src/dto/screen_presentation_style.dart'; export 'src/dto/user.dart'; export 'src/dto/user_properties.dart'; export 'src/dto/user_property.dart'; @@ -52,3 +45,7 @@ export 'src/dto/store_product/product_store_details.dart'; export 'src/qonversion.dart'; export 'src/qonversion_config.dart'; export 'src/qonversion_config_builder.dart'; +export 'src/nocodes/nocodes.dart'; +export 'src/nocodes/nocodes_config.dart'; +export 'src/nocodes/nocodes_events.dart'; +export 'src/nocodes/presentation_config.dart'; diff --git a/lib/src/automations.dart b/lib/src/automations.dart deleted file mode 100644 index 1b02c2a8..00000000 --- a/lib/src/automations.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'package:qonversion_flutter/qonversion_flutter.dart'; -import 'package:qonversion_flutter/src/internal/automations_internal.dart'; - -abstract class Automations { - static Automations? _backingInstance; - - /// Use this variable to get a current initialized instance of the Qonversion Automations. - /// Please, use Automations only after calling [Qonversion.initialize]. - /// Otherwise, trying to access the variable will cause an exception. - /// - /// Returns current initialized instance of the Qonversion Automations. - /// Throws [Exception] if Qonversion has not been initialized. - static Automations getSharedInstance() { - Automations? instance = _backingInstance; - - if (instance == null) { - try { - Qonversion.getSharedInstance(); - } catch (e) { - throw new Exception("Qonversion has not been initialized. " + - "Automations should be used after Qonversion is initialized."); - } - - instance = new AutomationsInternal(); - _backingInstance = instance; - } - - return instance; - } - - /// Called when Automations' screen is shown - /// [screenId] shown screen Id - Stream get shownScreensStream; - - /// Called when Automations flow starts executing an action - /// [actionResult] action that is being executed - Stream get startedActionsStream; - - /// Called when Automations flow fails executing an action - /// [actionResult] failed action - Stream get failedActionsStream; - - /// Called when Automations flow finishes executing an action - /// [actionResult] executed action. - /// For instance, if the user made a purchase then action.type = ActionResultType.purchase. - /// You can use the [Qonversion.checkEntitlements] method to get available entitlements. - Stream get finishedActionsStream; - - /// Called when Automations flow is finished and the Automations screen is closed - Stream get finishedAutomationsStream; - - /// Set push token to Qonversion to enable Qonversion push notifications - /// [token] Firebase device token for Android. APNs device token for iOS - @Deprecated("Consider removing this method calls. Qonversion is not working with push notifications anymore") - Future setNotificationsToken(String token); - - /// [notificationData] notification payload data - /// See [Firebase RemoteMessage data](https://pub.dev/documentation/firebase_messaging_platform_interface/latest/firebase_messaging_platform_interface/RemoteMessage/data.html) - /// See [APNs notification data](https://developer.apple.com/documentation/usernotifications/unnotificationcontent/1649869-userinfo) - /// Returns true when a push notification was received from Qonversion. Otherwise returns false, so you need to handle the notification yourself - @Deprecated("Consider removing this method calls as they aren't needed anymore") - Future handleNotification(Map notificationData); - - /// Get parsed custom payload, which you added to the notification in the dashboard - /// [notificationData] notification payload data - /// See [Firebase RemoteMessage data](https://pub.dev/documentation/firebase_messaging_platform_interface/latest/firebase_messaging_platform_interface/RemoteMessage/data.html) - /// See [APNs notification data](https://developer.apple.com/documentation/usernotifications/unnotificationcontent/1649869-userinfo) - /// Returns a map with custom payload from the notification or null if it's not provided. - @Deprecated("Consider removing this method calls. Qonversion is not working with push notifications anymore") - Future?> getNotificationCustomPayload(Map notificationData); - - /// Show the screen using its ID. - /// [screenId] Identifier of the screen which must be shown - Future showScreen(String screenId); - - /// Set the configuration of screen representation. - /// [config] a configuration to apply. - /// [screenId] identifier of screen, to which a config should be applied. - /// If not provided, the config is used for all the screens. - Future setScreenPresentationConfig(QScreenPresentationConfig config, [String? screenId]); -} diff --git a/lib/src/dto/automations/action_result.dart b/lib/src/dto/automations/action_result.dart deleted file mode 100644 index 5a70948f..00000000 --- a/lib/src/dto/automations/action_result.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; -import 'package:qonversion_flutter/src/dto/qonversion_error.dart'; -import 'package:qonversion_flutter/src/internal/mapper.dart'; -import 'action_result_type.dart'; - -part 'action_result.g.dart'; - -@JsonSerializable() -class ActionResult { - @JsonKey(name: "type", defaultValue: ActionResultType.unknown) - final ActionResultType type; - - @JsonKey(name: "value") - final Map? parameters; - - @JsonKey( - name: "error", - fromJson: QMapper.qonversionErrorFromJson, - ) - final QError? error; - - const ActionResult(this.type, this.parameters, this.error); - - factory ActionResult.fromJson(Map json) => - _$ActionResultFromJson(json); - - Map toJson() => _$ActionResultToJson(this); -} diff --git a/lib/src/dto/automations/action_result.g.dart b/lib/src/dto/automations/action_result.g.dart deleted file mode 100644 index ae5ecd23..00000000 --- a/lib/src/dto/automations/action_result.g.dart +++ /dev/null @@ -1,33 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'action_result.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -ActionResult _$ActionResultFromJson(Map json) => ActionResult( - $enumDecodeNullable(_$ActionResultTypeEnumMap, json['type']) ?? - ActionResultType.unknown, - (json['value'] as Map?)?.map( - (k, e) => MapEntry(k, e as String), - ), - QMapper.qonversionErrorFromJson(json['error']), - ); - -Map _$ActionResultToJson(ActionResult instance) => - { - 'type': _$ActionResultTypeEnumMap[instance.type]!, - 'value': instance.parameters, - 'error': instance.error, - }; - -const _$ActionResultTypeEnumMap = { - ActionResultType.unknown: 'unknown', - ActionResultType.url: 'url', - ActionResultType.deepLink: 'deeplink', - ActionResultType.navigation: 'navigate', - ActionResultType.purchase: 'purchase', - ActionResultType.restore: 'restore', - ActionResultType.close: 'close', -}; diff --git a/lib/src/dto/automations/action_result_type.dart b/lib/src/dto/automations/action_result_type.dart deleted file mode 100644 index be982bd7..00000000 --- a/lib/src/dto/automations/action_result_type.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; - -enum ActionResultType { - @JsonValue('unknown') - unknown, - @JsonValue('url') - url, - @JsonValue('deeplink') - deepLink, - @JsonValue('navigate') - navigation, - @JsonValue('purchase') - purchase, - @JsonValue('restore') - restore, - @JsonValue('close') - close, -} diff --git a/lib/src/dto/automations/event.dart b/lib/src/dto/automations/event.dart deleted file mode 100644 index df8861ce..00000000 --- a/lib/src/dto/automations/event.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; -import 'package:qonversion_flutter/src/internal/mapper.dart'; - -import 'event_type.dart'; - -part 'event.g.dart'; - -@JsonSerializable() -class AutomationsEvent { - @JsonKey( - name: 'type', - unknownEnumValue: AutomationsEventType.unknown, - ) - final AutomationsEventType type; - - @JsonKey( - name: 'timestamp', - fromJson: QMapper.dateTimeFromSecondsTimestamp, - ) - final DateTime date; - - const AutomationsEvent(this.type, this.date); - - factory AutomationsEvent.fromJson(Map json) => - _$AutomationsEventFromJson(json); - - Map toJson() => _$AutomationsEventToJson(this); -} diff --git a/lib/src/dto/automations/event.g.dart b/lib/src/dto/automations/event.g.dart deleted file mode 100644 index 3d30902e..00000000 --- a/lib/src/dto/automations/event.g.dart +++ /dev/null @@ -1,42 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'event.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -AutomationsEvent _$AutomationsEventFromJson(Map json) => - AutomationsEvent( - $enumDecode(_$AutomationsEventTypeEnumMap, json['type'], - unknownValue: AutomationsEventType.unknown), - QMapper.dateTimeFromSecondsTimestamp(json['timestamp'] as num), - ); - -Map _$AutomationsEventToJson(AutomationsEvent instance) => - { - 'type': _$AutomationsEventTypeEnumMap[instance.type]!, - 'timestamp': instance.date.toIso8601String(), - }; - -const _$AutomationsEventTypeEnumMap = { - AutomationsEventType.unknown: 'unknown', - AutomationsEventType.trialStarted: 'trial_started', - AutomationsEventType.trialConverted: 'trial_converted', - AutomationsEventType.trialCanceled: 'trial_canceled', - AutomationsEventType.trialBillingRetry: 'trial_billing_retry_entered', - AutomationsEventType.subscriptionStarted: 'subscription_started', - AutomationsEventType.subscriptionRenewed: 'subscription_renewed', - AutomationsEventType.subscriptionRefunded: 'subscription_refunded', - AutomationsEventType.subscriptionCanceled: 'subscription_canceled', - AutomationsEventType.subscriptionBillingRetry: - 'subscription_billing_retry_entered', - AutomationsEventType.inAppPurchase: 'in_app_purchase', - AutomationsEventType.subscriptionUpgraded: 'subscription_upgraded', - AutomationsEventType.trialStillActive: 'trial_still_active', - AutomationsEventType.trialExpired: 'trial_expired', - AutomationsEventType.subscriptionExpired: 'subscription_expired', - AutomationsEventType.subscriptionDowngraded: 'subscription_downgraded', - AutomationsEventType.subscriptionProductChanged: - 'subscription_product_changed', -}; diff --git a/lib/src/dto/automations/event_type.dart b/lib/src/dto/automations/event_type.dart deleted file mode 100644 index 035defaf..00000000 --- a/lib/src/dto/automations/event_type.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; - -enum AutomationsEventType { - @JsonValue("unknown") - unknown, - @JsonValue("trial_started") - trialStarted, - @JsonValue("trial_converted") - trialConverted, - @JsonValue("trial_canceled") - trialCanceled, - @JsonValue("trial_billing_retry_entered") - trialBillingRetry, - @JsonValue("subscription_started") - subscriptionStarted, - @JsonValue("subscription_renewed") - subscriptionRenewed, - @JsonValue("subscription_refunded") - subscriptionRefunded, - @JsonValue("subscription_canceled") - subscriptionCanceled, - @JsonValue("subscription_billing_retry_entered") - subscriptionBillingRetry, - @JsonValue("in_app_purchase") - inAppPurchase, - @JsonValue("subscription_upgraded") - subscriptionUpgraded, - @JsonValue("trial_still_active") - trialStillActive, - @JsonValue("trial_expired") - trialExpired, - @JsonValue("subscription_expired") - subscriptionExpired, - @JsonValue("subscription_downgraded") - subscriptionDowngraded, - @JsonValue("subscription_product_changed") - subscriptionProductChanged, -} diff --git a/lib/src/dto/screen_presentation_config.dart b/lib/src/dto/screen_presentation_config.dart deleted file mode 100644 index f5ca7431..00000000 --- a/lib/src/dto/screen_presentation_config.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; -import 'screen_presentation_style.dart'; - -part 'screen_presentation_config.g.dart'; - -@JsonSerializable(createFactory: false) -class QScreenPresentationConfig { - /// Describes how screens will be displayed. - /// For mode details see the enum description. - final QScreenPresentationStyle presentationStyle; - - /// iOS only. For Android consider using [QScreenPresentationStyle.noAnimation]. - /// Describes whether should transaction be animated or not. - /// Default value is true. - @JsonKey(toJson: animatedToJson) - final bool animated; - - QScreenPresentationConfig(this.presentationStyle, [this.animated = true]); - - Map toJson() => _$QScreenPresentationConfigToJson(this); -} - -String animatedToJson(bool animated) { - return animated ? '1' : '0'; -} \ No newline at end of file diff --git a/lib/src/dto/screen_presentation_config.g.dart b/lib/src/dto/screen_presentation_config.g.dart deleted file mode 100644 index 9a05f94e..00000000 --- a/lib/src/dto/screen_presentation_config.g.dart +++ /dev/null @@ -1,22 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'screen_presentation_config.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -Map _$QScreenPresentationConfigToJson( - QScreenPresentationConfig instance) => - { - 'presentationStyle': - _$QScreenPresentationStyleEnumMap[instance.presentationStyle]!, - 'animated': animatedToJson(instance.animated), - }; - -const _$QScreenPresentationStyleEnumMap = { - QScreenPresentationStyle.push: 'Push', - QScreenPresentationStyle.fullScreen: 'FullScreen', - QScreenPresentationStyle.popover: 'Popover', - QScreenPresentationStyle.noAnimation: 'NoAnimation', -}; diff --git a/lib/src/dto/screen_presentation_style.dart b/lib/src/dto/screen_presentation_style.dart deleted file mode 100644 index cff624eb..00000000 --- a/lib/src/dto/screen_presentation_style.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; -import 'package:qonversion_flutter/qonversion_flutter.dart'; - -enum QScreenPresentationStyle { - /// on Android - default screen transaction animation will be used. - /// on iOS - not a modal presentation. This style pushes a controller to a current navigation stack. - /// For iOS NavigationController on the top of the stack is required. - @JsonValue('Push') - push, - - /// on Android - screen will move from bottom to top. - /// on iOS - UIModalPresentationFullScreen analog. - @JsonValue('FullScreen') - fullScreen, - - /// iOS only - UIModalPresentationPopover analog - @JsonValue('Popover') - popover, - - /// Android only - screen will appear/disappear without any animation - /// For iOS consider providing the [QScreenPresentationConfig.animated] flag. - @JsonValue('NoAnimation') - noAnimation, -} diff --git a/lib/src/dto/sku_details/sku_details.g.dart b/lib/src/dto/sku_details/sku_details.g.dart index cd67027b..b4d728fc 100644 --- a/lib/src/dto/sku_details/sku_details.g.dart +++ b/lib/src/dto/sku_details/sku_details.g.dart @@ -6,9 +6,7 @@ part of 'sku_details.dart'; // JsonSerializableGenerator // ************************************************************************** -// ignore: deprecated_member_use_from_same_package SkuDetailsWrapper _$SkuDetailsWrapperFromJson(Map json) => - // ignore: deprecated_member_use_from_same_package SkuDetailsWrapper( description: json['description'] as String, freeTrialPeriod: json['freeTrialPeriod'] as String, @@ -30,7 +28,6 @@ SkuDetailsWrapper _$SkuDetailsWrapperFromJson(Map json) => originalJson: json['originalJson'] as String, ); -// ignore: deprecated_member_use_from_same_package Map _$SkuDetailsWrapperToJson(SkuDetailsWrapper instance) => { 'description': instance.description, diff --git a/lib/src/internal/automations_internal.dart b/lib/src/internal/automations_internal.dart deleted file mode 100644 index 31460601..00000000 --- a/lib/src/internal/automations_internal.dart +++ /dev/null @@ -1,116 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter/services.dart'; -import 'package:qonversion_flutter/qonversion_flutter.dart'; - -import 'constants.dart'; - -class AutomationsInternal implements Automations { - final MethodChannel _channel = MethodChannel('qonversion_plugin'); - - final _shownScreensEventChannel = - EventChannel('qonversion_flutter_shown_screens'); - final _startedActionsEventChannel = - EventChannel('qonversion_flutter_started_actions'); - final _failedActionsEventChannel = - EventChannel('qonversion_flutter_failed_actions'); - final _finishedActionsEventChannel = - EventChannel('qonversion_flutter_finished_actions'); - final _finishedAutomationsEventChannel = - EventChannel('qonversion_flutter_finished_automations'); - - @override - Stream get shownScreensStream => _shownScreensEventChannel - .receiveBroadcastStream() - .cast() - .map((event) { - final Map decodedEvent = jsonDecode(event); - return decodedEvent["screenId"]; - }); - - @override - Stream get startedActionsStream => - _startedActionsEventChannel - .receiveBroadcastStream() - .cast() - .map((event) { - return _handleActionEvent(event); - }); - - @override - Stream get failedActionsStream => - _failedActionsEventChannel - .receiveBroadcastStream() - .cast() - .map((event) { - return _handleActionEvent(event); - }); - - @override - Stream get finishedActionsStream => - _finishedActionsEventChannel - .receiveBroadcastStream() - .cast() - .map((event) { - return _handleActionEvent(event); - }); - - @override - Stream get finishedAutomationsStream => - _finishedAutomationsEventChannel.receiveBroadcastStream().cast(); - - AutomationsInternal() { - _channel.invokeMethod(Constants.mSubscribeAutomations); - } - - @override - Future setNotificationsToken(String token) { - return _channel.invokeMethod(Constants.mSetNotificationsToken, - {Constants.kNotificationsToken: token}); - } - - @override - Future handleNotification(Map notificationData) async { - try { - final bool rawResult = await _channel.invokeMethod( - Constants.mHandleNotification, - {Constants.kNotificationData: notificationData}) as bool; - return rawResult; - } catch (e) { - return false; - } - } - - @override - Future?> getNotificationCustomPayload(Map notificationData) async { - try { - final String? rawResult = await _channel.invokeMethod( - Constants.mGetNotificationCustomPayload, - {Constants.kNotificationData: notificationData}) as String?; - - final Map? result = rawResult == null ? null : jsonDecode(rawResult); - return result; - } catch (e) { - return null; - } - } - - @override - Future showScreen(String screenId) { - return _channel.invokeMethod(Constants.mShowScreen, - {Constants.kScreenId: screenId}); - } - - @override - Future setScreenPresentationConfig(QScreenPresentationConfig config, [String? screenId]) { - final Map configData = config.toJson(); - return _channel.invokeMethod(Constants.mSetScreenPresentationConfig, - {Constants.kScreenId: screenId, Constants.kConfigData: configData}); - } - - static ActionResult _handleActionEvent(String event) { - final Map decodedEvent = jsonDecode(event); - - return ActionResult.fromJson(decodedEvent); - } -} diff --git a/lib/src/internal/constants.dart b/lib/src/internal/constants.dart index 9669ceb8..088a56d7 100644 --- a/lib/src/internal/constants.dart +++ b/lib/src/internal/constants.dart @@ -21,11 +21,7 @@ class Constants { static const kEntitlements = 'entitlements'; static const kProperty = 'property'; static const kValue = 'value'; - static const kNotificationsToken = 'notificationsToken'; - static const kNotificationData = 'notificationData'; static const kLifetime = 'lifetime'; - static const kScreenId = 'screenId'; - static const kConfigData = 'configData'; static const kExperimentId = 'experimentId'; static const kGroupId = 'groupId'; static const kRemoteConfigurationId = 'remoteConfigurationId'; @@ -36,6 +32,7 @@ class Constants { static const kIncludeEmptyContextKey = 'includeEmptyContextKey'; static const kDiscountId = 'discountId'; static const kPromoOffer = 'promoOffer'; + static const kConfig = 'config'; // MethodChannel methods names static const mInitialize = 'initialize'; @@ -72,12 +69,10 @@ class Constants { static const mDetachUserFromRemoteConfiguration = 'detachUserFromRemoteConfiguration'; static const mCollectAppleSearchAdsAttribution = 'collectAppleSearchAdsAttribution'; static const mPresentCodeRedemptionSheet = 'presentCodeRedemptionSheet'; - static const mSubscribeAutomations = 'automationsSubscribe'; - static const mSetNotificationsToken = 'automationsSetNotificationsToken'; - static const mHandleNotification = 'automationsHandleNotification'; - static const mGetNotificationCustomPayload = 'automationsGetNotificationCustomPayload'; - static const mShowScreen = 'automationsShowScreen'; + static const mInitializeNoCodes = 'initializeNoCodes'; static const mSetScreenPresentationConfig = 'setScreenPresentationConfig'; + static const mShowNoCodesScreen = 'showNoCodesScreen'; + static const mCloseNoCodes = 'closeNoCodes'; // Numeric constants static const skuDetailsPriceRatio = 1000000; diff --git a/lib/src/internal/qonversion_internal.dart b/lib/src/internal/qonversion_internal.dart index 5e6879e3..fc0b7799 100644 --- a/lib/src/internal/qonversion_internal.dart +++ b/lib/src/internal/qonversion_internal.dart @@ -11,7 +11,7 @@ import 'package:qonversion_flutter/src/internal/utils/string.dart'; import 'constants.dart'; class QonversionInternal implements Qonversion { - static const String _sdkVersion = "9.3.1"; + static const String _sdkVersion = "10.0.0"; final MethodChannel _channel = MethodChannel('qonversion_plugin'); diff --git a/lib/src/nocodes/nocodes.dart b/lib/src/nocodes/nocodes.dart new file mode 100644 index 00000000..d763d64d --- /dev/null +++ b/lib/src/nocodes/nocodes.dart @@ -0,0 +1,103 @@ +import 'dart:async'; +import 'nocodes_events.dart'; +import 'nocodes_config.dart'; +import 'nocodes_internal.dart'; +import 'presentation_config.dart'; + +/// Main No-Codes API class +/// +/// **Platform Support:** +/// - ✅ iOS: Full support +/// - ✅ Android: Full support +/// - ❌ macOS: Not supported (returns empty streams and no-op methods) +abstract class NoCodes { + static NoCodes? _backingInstance; + + /// Use this variable to get a current initialized instance of the No-Codes SDK. + /// Please, use the property only after calling [NoCodes.initialize]. + /// Otherwise, trying to access the variable will cause an exception. + /// + /// Returns current initialized instance of the No-Codes SDK. + /// Throws exception if the instance has not been initialized + static NoCodes getSharedInstance() { + NoCodes? instance = _backingInstance; + + if (instance == null) { + throw new Exception("NoCodes has not been initialized. You should call " + + "the initialize method before accessing the shared instance of NoCodes."); + } + + return instance; + } + + /// An entry point to use No-Codes SDK. Call to initialize No-Codes SDK with required config. + /// The function is the best way to set additional configs you need to use No-Codes SDK. + /// + /// **Platform Support:** iOS and Android. On macOS, this will initialize but functionality will be limited. + /// + /// [config] a config that contains key SDK settings. + /// Call [NoCodesConfigBuilder.build] to configure and create a [NoCodesConfig] instance. + /// Returns initialized instance of the No-Codes SDK. + static NoCodes initialize(NoCodesConfig config) { + NoCodes instance = NoCodesInternal(config); + _backingInstance = instance; + return instance; + } + + /// Initialize No-Codes with project key (for backward compatibility) + /// + /// **Platform Support:** iOS and Android. On macOS, this will initialize but functionality will be limited. + static Future initializeWithProjectKey(String projectKey) async { + final config = NoCodesConfig(projectKey); + initialize(config); + } + + /// Stream of screen shown events + /// + /// **Platform Support:** iOS and Android. Returns empty stream on macOS. + Stream get screenShownStream; + + /// Stream of finished events + /// + /// **Platform Support:** iOS and Android. Returns empty stream on macOS. + Stream get finishedStream; + + /// Stream of action started events + /// + /// **Platform Support:** iOS and Android. Returns empty stream on macOS. + Stream get actionStartedStream; + + /// Stream of action failed events + /// + /// **Platform Support:** iOS and Android. Returns empty stream on macOS. + Stream get actionFailedStream; + + /// Stream of action finished events + /// + /// **Platform Support:** iOS and Android. Returns empty stream on macOS. + Stream get actionFinishedStream; + + /// Stream of screen failed to load events + /// + /// **Platform Support:** iOS and Android. Returns empty stream on macOS. + Stream get screenFailedToLoadStream; + + /// Set screen presentation configuration + /// + /// **Platform Support:** iOS and Android. No-op on macOS. + Future setScreenPresentationConfig( + NoCodesPresentationConfig config, { + String? contextKey, + }); + + /// Show No-Codes screen with context key + /// + /// **Platform Support:** iOS and Android. No-op on macOS. + Future showScreen(String contextKey); + + /// Close No-Codes screen + /// + /// **Platform Support:** iOS and Android. No-op on macOS. + Future close(); + +} \ No newline at end of file diff --git a/lib/src/nocodes/nocodes_config.dart b/lib/src/nocodes/nocodes_config.dart new file mode 100644 index 00000000..7b00a2c0 --- /dev/null +++ b/lib/src/nocodes/nocodes_config.dart @@ -0,0 +1,20 @@ +/// Configuration for NoCodes initialization +class NoCodesConfig { + final String projectKey; + + const NoCodesConfig(this.projectKey); +} + +/// Builder for NoCodes configuration +class NoCodesConfigBuilder { + final String projectKey; + + NoCodesConfigBuilder(this.projectKey); + + /// Generate [NoCodesConfig] instance with all the provided configurations. + /// + /// Returns the complete [NoCodesConfig] instance. + NoCodesConfig build() { + return NoCodesConfig(projectKey); + } +} \ No newline at end of file diff --git a/lib/src/nocodes/nocodes_events.dart b/lib/src/nocodes/nocodes_events.dart new file mode 100644 index 00000000..4c46e324 --- /dev/null +++ b/lib/src/nocodes/nocodes_events.dart @@ -0,0 +1,154 @@ +/// Base class for all No-Codes events +abstract class NoCodesEvent { + const NoCodesEvent(); +} + +/// Event when No-Codes screen is shown +class NoCodesScreenShownEvent extends NoCodesEvent { + final Map? payload; + + const NoCodesScreenShownEvent({this.payload}); + + factory NoCodesScreenShownEvent.fromMap(Map map) { + return NoCodesScreenShownEvent( + payload: map['payload'] as Map?, + ); + } + + Map toMap() { + return { + 'type': 'nocodes_screen_shown', + 'payload': payload, + }; + } + + @override + String toString() { + return 'NoCodesScreenShownEvent(payload: $payload)'; + } +} + +/// Event when NoCodes flow is finished +class NoCodesFinishedEvent extends NoCodesEvent { + final Map? payload; + + const NoCodesFinishedEvent({this.payload}); + + factory NoCodesFinishedEvent.fromMap(Map map) { + return NoCodesFinishedEvent( + payload: map['payload'] as Map?, + ); + } + + Map toMap() { + return { + 'type': 'nocodes_finished', + 'payload': payload, + }; + } + + @override + String toString() { + return 'NoCodesFinishedEvent(payload: $payload)'; + } +} + +/// Event when NoCodes action is started +class NoCodesActionStartedEvent extends NoCodesEvent { + final Map? payload; + + const NoCodesActionStartedEvent({this.payload}); + + factory NoCodesActionStartedEvent.fromMap(Map map) { + return NoCodesActionStartedEvent( + payload: map['payload'] as Map?, + ); + } + + Map toMap() { + return { + 'type': 'nocodes_action_started', + 'payload': payload, + }; + } + + @override + String toString() { + return 'NoCodesActionStartedEvent(payload: $payload)'; + } +} + +/// Event when NoCodes action failed +class NoCodesActionFailedEvent extends NoCodesEvent { + final Map? payload; + + const NoCodesActionFailedEvent({this.payload}); + + factory NoCodesActionFailedEvent.fromMap(Map map) { + return NoCodesActionFailedEvent( + payload: map['payload'] as Map?, + ); + } + + Map toMap() { + return { + 'type': 'nocodes_action_failed', + 'payload': payload, + }; + } + + @override + String toString() { + return 'NoCodesActionFailedEvent(payload: $payload)'; + } +} + +/// Event when NoCodes action is finished +class NoCodesActionFinishedEvent extends NoCodesEvent { + final Map? payload; + + const NoCodesActionFinishedEvent({this.payload}); + + factory NoCodesActionFinishedEvent.fromMap(Map map) { + return NoCodesActionFinishedEvent( + payload: map['payload'] as Map?, + ); + } + + Map toMap() { + return { + 'type': 'nocodes_action_finished', + 'payload': payload, + }; + } + + @override + String toString() { + return 'NoCodesActionFinishedEvent(payload: $payload)'; + } +} + +/// Event when NoCodes screen failed to load +class NoCodesScreenFailedToLoadEvent extends NoCodesEvent { + final Map? payload; + + const NoCodesScreenFailedToLoadEvent({this.payload}); + + factory NoCodesScreenFailedToLoadEvent.fromMap(Map map) { + return NoCodesScreenFailedToLoadEvent( + payload: map['payload'] as Map?, + ); + } + + Map toMap() { + return { + 'type': 'nocodes_screen_failed_to_load', + 'payload': payload, + }; + } + + @override + String toString() { + return 'NoCodesScreenFailedToLoadEvent(payload: $payload)'; + } +} \ No newline at end of file diff --git a/lib/src/nocodes/nocodes_internal.dart b/lib/src/nocodes/nocodes_internal.dart new file mode 100644 index 00000000..72c409ac --- /dev/null +++ b/lib/src/nocodes/nocodes_internal.dart @@ -0,0 +1,155 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:flutter/services.dart'; +import 'nocodes_events.dart'; +import 'nocodes_config.dart'; +import 'nocodes.dart'; +import 'presentation_config.dart'; +import 'dart:convert'; +import '../internal/constants.dart'; + +class NoCodesInternal implements NoCodes { + final MethodChannel _channel = MethodChannel('qonversion_plugin'); + + // Separate event channels for each event type + final EventChannel _screenShownEventChannel = EventChannel('qonversion_flutter_nocodes_screen_shown'); + final EventChannel _finishedEventChannel = EventChannel('qonversion_flutter_nocodes_finished'); + final EventChannel _actionStartedEventChannel = EventChannel('qonversion_flutter_nocodes_action_started'); + final EventChannel _actionFailedEventChannel = EventChannel('qonversion_flutter_nocodes_action_failed'); + final EventChannel _actionFinishedEventChannel = EventChannel('qonversion_flutter_nocodes_action_finished'); + final EventChannel _screenFailedToLoadEventChannel = EventChannel('qonversion_flutter_nocodes_screen_failed_to_load'); + + NoCodesInternal(NoCodesConfig config) { + _initialize(config); + } + + void _initialize(NoCodesConfig config) { + // NoCodes is not supported on macOS + if (Platform.isMacOS) { + return; + } + + final args = { + Constants.kProjectKey: config.projectKey, + }; + _channel.invokeMethod(Constants.mInitializeNoCodes, args); + } + + @override + Stream get screenShownStream { + if (Platform.isMacOS) { + return Stream.empty(); + } + return _screenShownEventChannel + .receiveBroadcastStream() + .cast() + .map((event) { + final Map decodedEvent = jsonDecode(event); + return NoCodesScreenShownEvent.fromMap(decodedEvent); + }); + } + + @override + Stream get finishedStream { + if (Platform.isMacOS) { + return Stream.empty(); + } + return _finishedEventChannel + .receiveBroadcastStream() + .cast() + .map((event) { + final Map decodedEvent = jsonDecode(event); + return NoCodesFinishedEvent.fromMap(decodedEvent); + }); + } + + @override + Stream get actionStartedStream { + if (Platform.isMacOS) { + return Stream.empty(); + } + return _actionStartedEventChannel + .receiveBroadcastStream() + .cast() + .map((event) { + final Map decodedEvent = jsonDecode(event); + return NoCodesActionStartedEvent.fromMap(decodedEvent); + }); + } + + @override + Stream get actionFailedStream { + if (Platform.isMacOS) { + return Stream.empty(); + } + return _actionFailedEventChannel + .receiveBroadcastStream() + .cast() + .map((event) { + final Map decodedEvent = jsonDecode(event); + return NoCodesActionFailedEvent.fromMap(decodedEvent); + }); + } + + @override + Stream get actionFinishedStream { + if (Platform.isMacOS) { + return Stream.empty(); + } + return _actionFinishedEventChannel + .receiveBroadcastStream() + .cast() + .map((event) { + final Map decodedEvent = jsonDecode(event); + return NoCodesActionFinishedEvent.fromMap(decodedEvent); + }); + } + + @override + Stream get screenFailedToLoadStream { + if (Platform.isMacOS) { + return Stream.empty(); + } + return _screenFailedToLoadEventChannel + .receiveBroadcastStream() + .cast() + .map((event) { + final Map decodedEvent = jsonDecode(event); + return NoCodesScreenFailedToLoadEvent.fromMap(decodedEvent); + }); + } + + @override + Future setScreenPresentationConfig( + NoCodesPresentationConfig config, { + String? contextKey, + }) async { + if (Platform.isMacOS) { + return; + } + + final args = { + Constants.kConfig: config.toMap(), + if (contextKey != null) Constants.kContextKey: contextKey, + }; + await _channel.invokeMethod(Constants.mSetScreenPresentationConfig, args); + } + + @override + Future showScreen(String contextKey) async { + if (Platform.isMacOS) { + return; + } + + await _channel.invokeMethod(Constants.mShowNoCodesScreen, {Constants.kContextKey: contextKey}); + } + + @override + Future close() async { + if (Platform.isMacOS) { + return; + } + + await _channel.invokeMethod(Constants.mCloseNoCodes); + } +} \ No newline at end of file diff --git a/lib/src/nocodes/presentation_config.dart b/lib/src/nocodes/presentation_config.dart new file mode 100644 index 00000000..639c425e --- /dev/null +++ b/lib/src/nocodes/presentation_config.dart @@ -0,0 +1,72 @@ +/// Presentation style for NoCodes screens +enum NoCodesPresentationStyle { + push, + fullScreen, + popover, +} + +/// Configuration for NoCodes screen presentation +class NoCodesPresentationConfig { + static const kPush = 'Push'; + static const kFullScreen = 'FullScreen'; + static const kPopover = 'Popover'; + static const kAnimated = 'animated'; + static const kPresentationStyle = 'presentationStyle'; + + final bool animated; + final NoCodesPresentationStyle presentationStyle; + + const NoCodesPresentationConfig({ + this.animated = true, + this.presentationStyle = NoCodesPresentationStyle.fullScreen, + }); + + factory NoCodesPresentationConfig.fromMap(Map map) { + final presentationStyleString = map[kPresentationStyle] as String?; + NoCodesPresentationStyle presentationStyle; + + switch (presentationStyleString) { + case kPush: + presentationStyle = NoCodesPresentationStyle.push; + break; + case kFullScreen: + presentationStyle = NoCodesPresentationStyle.fullScreen; + break; + case kPopover: + presentationStyle = NoCodesPresentationStyle.popover; + break; + default: + presentationStyle = NoCodesPresentationStyle.fullScreen; + } + + return NoCodesPresentationConfig( + animated: map[kAnimated] as bool? ?? true, + presentationStyle: presentationStyle, + ); + } + + Map toMap() { + String presentationStyleString; + switch (presentationStyle) { + case NoCodesPresentationStyle.push: + presentationStyleString = kPush; + break; + case NoCodesPresentationStyle.fullScreen: + presentationStyleString = kFullScreen; + break; + case NoCodesPresentationStyle.popover: + presentationStyleString = kPopover; + break; + } + + return { + kAnimated: animated, + kPresentationStyle: presentationStyleString, + }; + } + + @override + String toString() { + return 'NoCodesPresentationConfig(animated: $animated, presentationStyle: $presentationStyle)'; + } +} \ No newline at end of file diff --git a/macos/qonversion_flutter.podspec b/macos/qonversion_flutter.podspec index dadbca64..c03367db 100644 --- a/macos/qonversion_flutter.podspec +++ b/macos/qonversion_flutter.podspec @@ -4,7 +4,7 @@ # Pod::Spec.new do |s| s.name = 'qonversion_flutter' - s.version = '5.0.0' + s.version = '9.3.0' s.summary = 'Flutter Qonversion SDK' s.description = <<-DESC Powerful yet simple subscription analytics @@ -16,7 +16,7 @@ Pod::Spec.new do |s| s.source_files = 'Classes/**/*' s.dependency 'FlutterMacOS' s.platform = :osx, '10.12' - s.dependency "QonversionSandwich", "5.2.1" + s.dependency "QonversionSandwich", "6.0.8" s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } s.swift_version = '5.0' diff --git a/pubspec.lock b/pubspec.lock index fedc4779..97276368 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -29,18 +29,18 @@ packages: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.13.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" build: dependency: transitive description: @@ -109,10 +109,10 @@ packages: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -125,10 +125,10 @@ packages: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" code_builder: dependency: transitive description: @@ -141,10 +141,10 @@ packages: dependency: "direct main" description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.1" convert: dependency: transitive description: @@ -173,10 +173,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.3" file: dependency: transitive description: @@ -279,18 +279,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: @@ -311,10 +311,10 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -327,10 +327,10 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.0" mime: dependency: transitive description: @@ -351,10 +351,10 @@ packages: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" pool: dependency: transitive description: @@ -399,7 +399,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_gen: dependency: transitive description: @@ -420,26 +420,26 @@ packages: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" stack_trace: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" stream_transform: dependency: transitive description: @@ -452,26 +452,26 @@ packages: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.4" timing: dependency: transitive description: @@ -500,10 +500,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "15.0.0" watcher: dependency: transitive description: @@ -537,5 +537,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.5.0 <4.0.0" + dart: ">=3.7.0-0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/pubspec.yaml b/pubspec.yaml index c29b83f8..422905e5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: qonversion_flutter 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. -version: 9.3.1 +version: 10.0.0 homepage: 'https://qonversion.io' repository: 'https://github.com/qonversion/flutter-sdk' @@ -11,7 +11,7 @@ environment: dependencies: flutter: sdk: flutter - json_annotation: ^4.0.1 + json_annotation: ^4.9.0 collection: ^1.15.0 dev_dependencies: