From 0b3dc57cc789519a54b8325fe1779cf42ce4909a Mon Sep 17 00:00:00 2001 From: Surik Date: Wed, 25 Jun 2025 16:57:43 +0300 Subject: [PATCH 01/10] Updated dependencies --- .../Flutter/ephemeral/flutter_lldb_helper.py | 32 ++++++++ .../ios/Flutter/ephemeral/flutter_lldbinit | 5 ++ .../xcshareddata/xcschemes/Runner.xcscheme | 3 + lib/src/dto/sku_details/sku_details.g.dart | 3 - pubspec.lock | 76 +++++++++---------- pubspec.yaml | 2 +- 6 files changed, 79 insertions(+), 42 deletions(-) create mode 100644 example/ios/Flutter/ephemeral/flutter_lldb_helper.py create mode 100644 example/ios/Flutter/ephemeral/flutter_lldbinit 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 0000000..a88caf9 --- /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 0000000..e3ba6fb --- /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/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index e67b280..fc5ae03 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/lib/src/dto/sku_details/sku_details.g.dart b/lib/src/dto/sku_details/sku_details.g.dart index cd67027..b4d728f 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/pubspec.lock b/pubspec.lock index fedc477..9727636 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 35d4c09..dc75cf7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: From 434fa71a9062da46a195afb696996a5404bab43a Mon Sep 17 00:00:00 2001 From: Surik Date: Wed, 2 Jul 2025 14:48:08 +0300 Subject: [PATCH 02/10] Added nocodes functionality and removed automations --- android/build.gradle | 4 +- .../AutomationsPlugin.kt | 112 --------- .../qonversion_flutter_sdk/NoCodesPlugin.kt | 126 ++++++++++ .../QonversionPlugin.kt | 34 ++- example/ios/Podfile | 2 +- example/ios/Runner.xcodeproj/project.pbxproj | 8 +- example/ios/Runner/AppDelegate.swift | 19 -- example/lib/home.dart | 66 ++--- example/lib/main.dart | 4 +- example/lib/nocodes_view.dart | 227 ++++++++++++++++++ ios/Classes/AutomationsPlugin.swift | 120 --------- ios/Classes/BaseEventStreamHandler.swift | 3 + ios/Classes/BaseListenerWrapper.swift | 12 +- ios/Classes/NoCodesPlugin.swift | 158 ++++++++++++ ios/Classes/SwiftQonversionPlugin.swift | 37 ++- ios/qonversion_flutter.podspec | 6 +- lib/qonversion_flutter.dart | 11 +- lib/src/automations.dart | 81 ------- lib/src/dto/automations/action_result.dart | 28 --- lib/src/dto/automations/action_result.g.dart | 33 --- .../dto/automations/action_result_type.dart | 18 -- lib/src/dto/automations/event.dart | 28 --- lib/src/dto/automations/event.g.dart | 42 ---- lib/src/dto/automations/event_type.dart | 38 --- lib/src/dto/screen_presentation_config.dart | 25 -- lib/src/dto/screen_presentation_config.g.dart | 22 -- lib/src/dto/screen_presentation_style.dart | 24 -- lib/src/internal/automations_internal.dart | 116 --------- lib/src/internal/constants.dart | 10 - lib/src/nocodes/nocodes.dart | 110 +++++++++ lib/src/nocodes/nocodes_config.dart | 20 ++ lib/src/nocodes/nocodes_events.dart | 154 ++++++++++++ lib/src/nocodes/nocodes_internal.dart | 168 +++++++++++++ lib/src/nocodes/presentation_config.dart | 66 +++++ macos/qonversion_flutter.podspec | 4 +- 35 files changed, 1120 insertions(+), 816 deletions(-) delete mode 100644 android/src/main/kotlin/com/qonversion/flutter/sdk/qonversion_flutter_sdk/AutomationsPlugin.kt create mode 100644 android/src/main/kotlin/com/qonversion/flutter/sdk/qonversion_flutter_sdk/NoCodesPlugin.kt create mode 100644 example/lib/nocodes_view.dart delete mode 100644 ios/Classes/AutomationsPlugin.swift create mode 100644 ios/Classes/NoCodesPlugin.swift delete mode 100644 lib/src/automations.dart delete mode 100644 lib/src/dto/automations/action_result.dart delete mode 100644 lib/src/dto/automations/action_result.g.dart delete mode 100644 lib/src/dto/automations/action_result_type.dart delete mode 100644 lib/src/dto/automations/event.dart delete mode 100644 lib/src/dto/automations/event.g.dart delete mode 100644 lib/src/dto/automations/event_type.dart delete mode 100644 lib/src/dto/screen_presentation_config.dart delete mode 100644 lib/src/dto/screen_presentation_config.g.dart delete mode 100644 lib/src/dto/screen_presentation_style.dart delete mode 100644 lib/src/internal/automations_internal.dart create mode 100644 lib/src/nocodes/nocodes.dart create mode 100644 lib/src/nocodes/nocodes_config.dart create mode 100644 lib/src/nocodes/nocodes_events.dart create mode 100644 lib/src/nocodes/nocodes_internal.dart create mode 100644 lib/src/nocodes/presentation_config.dart diff --git a/android/build.gradle b/android/build.gradle index f85d5dd..91e088a 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:sandwich:5.2.0" + implementation "io.qonversion:sandwich:6.0.6" 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 02e0892..0000000 --- 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 0000000..f797e05 --- /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.error("NoNecessaryDataError", "Could not find necessary arguments", "Make sure you pass correct call arguments") + } + } + + fun setScreenPresentationConfig(config: Map?, contextKey: String?, result: Result) { + if (config != null) { + noCodesSandwich?.setScreenPresentationConfig(config, contextKey) + result.success(null) + } else { + result.error("NoNecessaryDataError", "Could not find necessary arguments", "Make sure you pass correct call arguments") + } + } + + fun showNoCodesScreen(contextKey: String?, result: Result) { + if (contextKey != null) { + noCodesSandwich?.showScreen(contextKey) + result.success(null) + } else { + result.error("NoNecessaryDataError", "Could not find necessary arguments", "Make sure you pass correct call arguments") + } + } + + 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 63616ea..bdb3755 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,9 +108,6 @@ class QonversionPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { "isFallbackFileAccessible" -> { return isFallbackFileAccessible(result) } - "automationsSubscribe" -> { - return automationsPlugin.subscribe() - } "remoteConfigList" -> { return remoteConfigList(result) } @@ -135,15 +131,11 @@ 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) + "closeNoCodes" -> noCodesPlugin?.closeNoCodes(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/Podfile b/example/ios/Podfile index eb8b0f9..728c145 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 4ee676f..8f347fb 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/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift index fd640c8..6e7eede 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 9588e33..7ff38ac 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 de9f488..a3f2c26 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 0000000..89c0410 --- /dev/null +++ b/example/lib/nocodes_view.dart @@ -0,0 +1,227 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:qonversion_flutter/qonversion_flutter.dart'; + +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 { + await NoCodes.getSharedInstance().showScreen('kamo_test'); + } 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 a2749df..0000000 --- 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/BaseEventStreamHandler.swift b/ios/Classes/BaseEventStreamHandler.swift index dd7d002..232efcd 100644 --- a/ios/Classes/BaseEventStreamHandler.swift +++ b/ios/Classes/BaseEventStreamHandler.swift @@ -17,11 +17,14 @@ class BaseEventStreamHandler: NSObject, EventStreamHandler { var eventSink: FlutterEventSink? public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { + print("BaseEventStreamHandler: onListen called for \(type(of: self))") eventSink = events + print("BaseEventStreamHandler: eventSink set - \(eventSink != nil ? "not nil" : "nil")") return nil } public func onCancel(withArguments arguments: Any?) -> FlutterError? { + print("BaseEventStreamHandler: onCancel called for \(type(of: self))") eventSink = nil return nil } diff --git a/ios/Classes/BaseListenerWrapper.swift b/ios/Classes/BaseListenerWrapper.swift index fc96874..6928a45 100644 --- a/ios/Classes/BaseListenerWrapper.swift +++ b/ios/Classes/BaseListenerWrapper.swift @@ -27,6 +27,7 @@ class FlutterListenerWrapper: NSObject where T: EventStreamHandler { func register(_ codec: MethodCodec? = nil, completion: ((T?) -> Void)? = nil) { guard eventStreamHandler == nil else { + print("FlutterListenerWrapper: already registered, skipping") return } @@ -37,18 +38,25 @@ class FlutterListenerWrapper: NSObject where T: EventStreamHandler { messenger = binding.messenger() #endif + print("FlutterListenerWrapper: creating eventStreamHandler for \(T.self)") eventStreamHandler = T() + + let channelName = "qonversion_flutter_\(eventChannelPostfix)" + print("FlutterListenerWrapper: creating eventChannel with name: \(channelName)") + 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) } + print("FlutterListenerWrapper: setting stream handler") eventChannel?.setStreamHandler(eventStreamHandler) + print("FlutterListenerWrapper: calling completion") completion?(eventStreamHandler) } diff --git a/ios/Classes/NoCodesPlugin.swift b/ios/Classes/NoCodesPlugin.swift new file mode 100644 index 0000000..37d07d4 --- /dev/null +++ b/ios/Classes/NoCodesPlugin.swift @@ -0,0 +1,158 @@ +// +// 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 { + 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) { + print("NoCodesPlugin: register called") + + // Register separate event channels for each event type + let screenShownListener = FlutterListenerWrapper(registrar, postfix: "nocodes_screen_shown") + screenShownListener.register() { eventStreamHandler in + print("NoCodesPlugin: screenShownEventStreamHandler received - \(eventStreamHandler != nil ? "not nil" : "nil")") + self.screenShownEventStreamHandler = eventStreamHandler + } + + let finishedListener = FlutterListenerWrapper(registrar, postfix: "nocodes_finished") + finishedListener.register() { eventStreamHandler in + print("NoCodesPlugin: finishedEventStreamHandler received - \(eventStreamHandler != nil ? "not nil" : "nil")") + self.finishedEventStreamHandler = eventStreamHandler + } + + let actionStartedListener = FlutterListenerWrapper(registrar, postfix: "nocodes_action_started") + actionStartedListener.register() { eventStreamHandler in + print("NoCodesPlugin: actionStartedEventStreamHandler received - \(eventStreamHandler != nil ? "not nil" : "nil")") + self.actionStartedEventStreamHandler = eventStreamHandler + } + + let actionFailedListener = FlutterListenerWrapper(registrar, postfix: "nocodes_action_failed") + actionFailedListener.register() { eventStreamHandler in + print("NoCodesPlugin: actionFailedEventStreamHandler received - \(eventStreamHandler != nil ? "not nil" : "nil")") + self.actionFailedEventStreamHandler = eventStreamHandler + } + + let actionFinishedListener = FlutterListenerWrapper(registrar, postfix: "nocodes_action_finished") + actionFinishedListener.register() { eventStreamHandler in + print("NoCodesPlugin: actionFinishedEventStreamHandler received - \(eventStreamHandler != nil ? "not nil" : "nil")") + self.actionFinishedEventStreamHandler = eventStreamHandler + } + + let screenFailedToLoadListener = FlutterListenerWrapper(registrar, postfix: "nocodes_screen_failed_to_load") + screenFailedToLoadListener.register() { eventStreamHandler in + print("NoCodesPlugin: screenFailedToLoadEventStreamHandler received - \(eventStreamHandler != nil ? "not nil" : "nil")") + self.screenFailedToLoadEventStreamHandler = eventStreamHandler + } + + print("NoCodesPlugin: register completed") + } + + 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) + } + + public func getAvailableEvents(_ result: @escaping FlutterResult) { + let events = noCodesSandwich?.getAvailableEvents() ?? [] + result(events) + } +} + +extension NoCodesPlugin: NoCodesEventListener { + public func noCodesDidTrigger(event: String, payload: [String: Any]?) { + print("NoCodesPlugin: noCodesDidTrigger called with event: \(event)") + + 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 "nocodes_screen_shown": + print("NoCodesPlugin: sending screenShownEventStreamHandler event") + self.screenShownEventStreamHandler?.eventSink?(jsonString) + + case "nocodes_finished": + print("NoCodesPlugin: sending finishedEventStreamHandler event") + self.finishedEventStreamHandler?.eventSink?(jsonString) + + case "nocodes_action_started": + print("NoCodesPlugin: sending actionStartedEventStreamHandler event") + self.actionStartedEventStreamHandler?.eventSink?(jsonString) + + case "nocodes_action_failed": + print("NoCodesPlugin: sending actionFailedEventStreamHandler event") + self.actionFailedEventStreamHandler?.eventSink?(jsonString) + + case "nocodes_action_finished": + print("NoCodesPlugin: sending actionFinishedEventStreamHandler event") + self.actionFinishedEventStreamHandler?.eventSink?(jsonString) + + case "nocodes_screen_failed_to_load": + print("NoCodesPlugin: sending screenFailedToLoadEventStreamHandler event") + self.screenFailedToLoadEventStreamHandler?.eventSink?(jsonString) + + default: + print("NoCodesPlugin: unknown event type: \(event)") + } + } + } +} diff --git a/ios/Classes/SwiftQonversionPlugin.swift b/ios/Classes/SwiftQonversionPlugin.swift index 64717aa..990090d 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,20 @@ public class SwiftQonversionPlugin: NSObject, FlutterPlugin { case "detachUserFromRemoteConfiguration": return detachUserFromRemoteConfiguration(args, result) - case "automationsSetNotificationsToken": - automationsPlugin?.setNotificationsToken(args["notificationsToken"] as? String, result) + case "initializeNoCodes": + noCodesPlugin?.initialize(args, result) return - case "automationsHandleNotification": - automationsPlugin?.handleNotification(args, result) + case "setScreenPresentationConfig": + noCodesPlugin?.setScreenPresentationConfig(args, result) return - case "automationsGetNotificationCustomPayload": - automationsPlugin?.getNotificationCustomPayload(args, result) + case "showNoCodesScreen": + noCodesPlugin?.showScreen(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 "getAvailableNoCodesEvents": + noCodesPlugin?.getAvailableEvents(result) return default: diff --git a/ios/qonversion_flutter.podspec b/ios/qonversion_flutter.podspec index 50e2f68..f056689 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.0" + s.platform = :ios, '13.0' + s.dependency "QonversionSandwich", "6.0.6" # 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 13990d9..f075cd5 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 1b02c2a..0000000 --- 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 5a70948..0000000 --- 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 ae5ecd2..0000000 --- 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 be982bd..0000000 --- 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 df8861c..0000000 --- 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 3d30902..0000000 --- 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 035defa..0000000 --- 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 f5ca743..0000000 --- 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 9a05f94..0000000 --- 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 cff624e..0000000 --- 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/internal/automations_internal.dart b/lib/src/internal/automations_internal.dart deleted file mode 100644 index 3146060..0000000 --- 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 9669ceb..6bd1f5c 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'; @@ -72,12 +68,6 @@ 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 mSetScreenPresentationConfig = 'setScreenPresentationConfig'; // Numeric constants static const skuDetailsPriceRatio = 1000000; diff --git a/lib/src/nocodes/nocodes.dart b/lib/src/nocodes/nocodes.dart new file mode 100644 index 0000000..a47d0c2 --- /dev/null +++ b/lib/src/nocodes/nocodes.dart @@ -0,0 +1,110 @@ +import 'dart:async'; +import 'nocodes_events.dart'; +import 'nocodes_config.dart'; +import 'nocodes_internal.dart'; +import 'presentation_config.dart'; + +/// Main NoCodes API class +/// +/// **Platform Support:** +/// - ✅ iOS: Full support +/// - ✅ Android: Full support +/// - ❌ macOS: Not supported (returns empty streams and no-op methods) +/// +/// NoCodes is a feature for A/B testing and user experience optimization. +/// It allows you to show different screens to users based on experiments. +abstract class NoCodes { + static NoCodes? _backingInstance; + + /// Use this variable to get a current initialized instance of the NoCodes 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 NoCodes 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 NoCodes SDK. Call to initialize NoCodes SDK with required config. + /// The function is the best way to set additional configs you need to use NoCodes 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 NoCodes SDK. + static NoCodes initialize(NoCodesConfig config) { + NoCodes instance = NoCodesInternal(config); + _backingInstance = instance; + return instance; + } + + /// Initialize NoCodes 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 NoCodes screen with context key + /// + /// **Platform Support:** iOS and Android. No-op on macOS. + Future showScreen(String contextKey); + + /// Close NoCodes screen + /// + /// **Platform Support:** iOS and Android. No-op on macOS. + Future close(); + + /// Get available events + /// + /// **Platform Support:** iOS and Android. Returns empty list on macOS. + Future> getAvailableEvents(); +} \ 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 0000000..7b00a2c --- /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 0000000..eeec612 --- /dev/null +++ b/lib/src/nocodes/nocodes_events.dart @@ -0,0 +1,154 @@ +/// Base class for all NoCodes events +abstract class NoCodesEvent { + const NoCodesEvent(); +} + +/// Event when NoCodes 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 0000000..6625e48 --- /dev/null +++ b/lib/src/nocodes/nocodes_internal.dart @@ -0,0 +1,168 @@ +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'; + +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) { + print('NoCodes is not supported on macOS'); + return; + } + + final args = { + 'projectKey': config.projectKey, + }; + _channel.invokeMethod('initializeNoCodes', 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) { + print('NoCodes is not supported on macOS'); + return; + } + + final args = { + 'config': config.toMap(), + if (contextKey != null) 'contextKey': contextKey, + }; + await _channel.invokeMethod('setScreenPresentationConfig', args); + } + + @override + Future showScreen(String contextKey) async { + if (Platform.isMacOS) { + print('NoCodes is not supported on macOS'); + return; + } + + await _channel.invokeMethod('showNoCodesScreen', {'contextKey': contextKey}); + } + + @override + Future close() async { + if (Platform.isMacOS) { + print('NoCodes is not supported on macOS'); + return; + } + + await _channel.invokeMethod('closeNoCodes'); + } + + @override + Future> getAvailableEvents() async { + if (Platform.isMacOS) { + return []; + } + + final result = await _channel.invokeMethod('getAvailableEvents'); + return List.from(result ?? []); + } +} \ 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 0000000..4cdb9ae --- /dev/null +++ b/lib/src/nocodes/presentation_config.dart @@ -0,0 +1,66 @@ +/// Presentation style for NoCodes screens +enum NoCodesPresentationStyle { + push, + fullScreen, + popover, +} + +/// Configuration for NoCodes screen presentation +class NoCodesPresentationConfig { + final bool animated; + final NoCodesPresentationStyle presentationStyle; + + const NoCodesPresentationConfig({ + this.animated = true, + this.presentationStyle = NoCodesPresentationStyle.fullScreen, + }); + + factory NoCodesPresentationConfig.fromMap(Map map) { + final presentationStyleString = map['presentationStyle'] as String?; + NoCodesPresentationStyle presentationStyle; + + switch (presentationStyleString) { + case 'Push': + presentationStyle = NoCodesPresentationStyle.push; + break; + case 'FullScreen': + presentationStyle = NoCodesPresentationStyle.fullScreen; + break; + case 'Popover': + presentationStyle = NoCodesPresentationStyle.popover; + break; + default: + presentationStyle = NoCodesPresentationStyle.fullScreen; + } + + return NoCodesPresentationConfig( + animated: map['animated'] as bool? ?? true, + presentationStyle: presentationStyle, + ); + } + + Map toMap() { + String presentationStyleString; + switch (presentationStyle) { + case NoCodesPresentationStyle.push: + presentationStyleString = 'Push'; + break; + case NoCodesPresentationStyle.fullScreen: + presentationStyleString = 'FullScreen'; + break; + case NoCodesPresentationStyle.popover: + presentationStyleString = 'Popover'; + break; + } + + return { + 'animated': animated, + 'presentationStyle': 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 876bc78..2565e76 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.0" + s.dependency "QonversionSandwich", "6.0.6" s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } s.swift_version = '5.0' From 1dfffae1ce45a790e6c388b5f7864a6ef5242dc5 Mon Sep 17 00:00:00 2001 From: Surik Sarkisyan Date: Thu, 3 Jul 2025 13:28:33 +0300 Subject: [PATCH 03/10] Apply suggestions from code review Co-authored-by: Kamo Spertsyan --- .../sdk/qonversion_flutter_sdk/NoCodesPlugin.kt | 6 +++--- ios/Classes/BaseEventStreamHandler.swift | 3 --- ios/Classes/BaseListenerWrapper.swift | 5 ----- ios/Classes/NoCodesPlugin.swift | 15 --------------- 4 files changed, 3 insertions(+), 26 deletions(-) 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 index f797e05..23d9e91 100644 --- 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 @@ -68,7 +68,7 @@ class NoCodesPlugin(private val messenger: BinaryMessenger, private val context: noCodesSandwich?.setDelegate(this) result.success(null) } else { - result.error("NoNecessaryDataError", "Could not find necessary arguments", "Make sure you pass correct call arguments") + result.noNecessaryDataError() } } @@ -77,7 +77,7 @@ class NoCodesPlugin(private val messenger: BinaryMessenger, private val context: noCodesSandwich?.setScreenPresentationConfig(config, contextKey) result.success(null) } else { - result.error("NoNecessaryDataError", "Could not find necessary arguments", "Make sure you pass correct call arguments") + result.noNecessaryDataError() } } @@ -86,7 +86,7 @@ class NoCodesPlugin(private val messenger: BinaryMessenger, private val context: noCodesSandwich?.showScreen(contextKey) result.success(null) } else { - result.error("NoNecessaryDataError", "Could not find necessary arguments", "Make sure you pass correct call arguments") + result.noNecessaryDataError() } } diff --git a/ios/Classes/BaseEventStreamHandler.swift b/ios/Classes/BaseEventStreamHandler.swift index 232efcd..dd7d002 100644 --- a/ios/Classes/BaseEventStreamHandler.swift +++ b/ios/Classes/BaseEventStreamHandler.swift @@ -17,14 +17,11 @@ class BaseEventStreamHandler: NSObject, EventStreamHandler { var eventSink: FlutterEventSink? public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { - print("BaseEventStreamHandler: onListen called for \(type(of: self))") eventSink = events - print("BaseEventStreamHandler: eventSink set - \(eventSink != nil ? "not nil" : "nil")") return nil } public func onCancel(withArguments arguments: Any?) -> FlutterError? { - print("BaseEventStreamHandler: onCancel called for \(type(of: self))") eventSink = nil return nil } diff --git a/ios/Classes/BaseListenerWrapper.swift b/ios/Classes/BaseListenerWrapper.swift index 6928a45..91d042f 100644 --- a/ios/Classes/BaseListenerWrapper.swift +++ b/ios/Classes/BaseListenerWrapper.swift @@ -27,7 +27,6 @@ class FlutterListenerWrapper: NSObject where T: EventStreamHandler { func register(_ codec: MethodCodec? = nil, completion: ((T?) -> Void)? = nil) { guard eventStreamHandler == nil else { - print("FlutterListenerWrapper: already registered, skipping") return } @@ -38,11 +37,9 @@ class FlutterListenerWrapper: NSObject where T: EventStreamHandler { messenger = binding.messenger() #endif - print("FlutterListenerWrapper: creating eventStreamHandler for \(T.self)") eventStreamHandler = T() let channelName = "qonversion_flutter_\(eventChannelPostfix)" - print("FlutterListenerWrapper: creating eventChannel with name: \(channelName)") if let codec = codec { eventChannel = FlutterEventChannel(name: channelName, @@ -53,10 +50,8 @@ class FlutterListenerWrapper: NSObject where T: EventStreamHandler { binaryMessenger: messenger) } - print("FlutterListenerWrapper: setting stream handler") eventChannel?.setStreamHandler(eventStreamHandler) - print("FlutterListenerWrapper: calling completion") completion?(eventStreamHandler) } diff --git a/ios/Classes/NoCodesPlugin.swift b/ios/Classes/NoCodesPlugin.swift index 37d07d4..d3353b3 100644 --- a/ios/Classes/NoCodesPlugin.swift +++ b/ios/Classes/NoCodesPlugin.swift @@ -20,46 +20,38 @@ public class NoCodesPlugin: NSObject { private var noCodesSandwich: NoCodesSandwich? public func register(_ registrar: FlutterPluginRegistrar) { - print("NoCodesPlugin: register called") // Register separate event channels for each event type let screenShownListener = FlutterListenerWrapper(registrar, postfix: "nocodes_screen_shown") screenShownListener.register() { eventStreamHandler in - print("NoCodesPlugin: screenShownEventStreamHandler received - \(eventStreamHandler != nil ? "not nil" : "nil")") self.screenShownEventStreamHandler = eventStreamHandler } let finishedListener = FlutterListenerWrapper(registrar, postfix: "nocodes_finished") finishedListener.register() { eventStreamHandler in - print("NoCodesPlugin: finishedEventStreamHandler received - \(eventStreamHandler != nil ? "not nil" : "nil")") self.finishedEventStreamHandler = eventStreamHandler } let actionStartedListener = FlutterListenerWrapper(registrar, postfix: "nocodes_action_started") actionStartedListener.register() { eventStreamHandler in - print("NoCodesPlugin: actionStartedEventStreamHandler received - \(eventStreamHandler != nil ? "not nil" : "nil")") self.actionStartedEventStreamHandler = eventStreamHandler } let actionFailedListener = FlutterListenerWrapper(registrar, postfix: "nocodes_action_failed") actionFailedListener.register() { eventStreamHandler in - print("NoCodesPlugin: actionFailedEventStreamHandler received - \(eventStreamHandler != nil ? "not nil" : "nil")") self.actionFailedEventStreamHandler = eventStreamHandler } let actionFinishedListener = FlutterListenerWrapper(registrar, postfix: "nocodes_action_finished") actionFinishedListener.register() { eventStreamHandler in - print("NoCodesPlugin: actionFinishedEventStreamHandler received - \(eventStreamHandler != nil ? "not nil" : "nil")") self.actionFinishedEventStreamHandler = eventStreamHandler } let screenFailedToLoadListener = FlutterListenerWrapper(registrar, postfix: "nocodes_screen_failed_to_load") screenFailedToLoadListener.register() { eventStreamHandler in - print("NoCodesPlugin: screenFailedToLoadEventStreamHandler received - \(eventStreamHandler != nil ? "not nil" : "nil")") self.screenFailedToLoadEventStreamHandler = eventStreamHandler } - print("NoCodesPlugin: register completed") } public func initialize(_ args: [String: Any]?, _ result: @escaping FlutterResult) { @@ -111,7 +103,6 @@ public class NoCodesPlugin: NSObject { extension NoCodesPlugin: NoCodesEventListener { public func noCodesDidTrigger(event: String, payload: [String: Any]?) { - print("NoCodesPlugin: noCodesDidTrigger called with event: \(event)") let eventData: [String: Any] = [ "payload": payload ?? [:] @@ -127,27 +118,21 @@ extension NoCodesPlugin: NoCodesEventListener { DispatchQueue.main.async { switch event { case "nocodes_screen_shown": - print("NoCodesPlugin: sending screenShownEventStreamHandler event") self.screenShownEventStreamHandler?.eventSink?(jsonString) case "nocodes_finished": - print("NoCodesPlugin: sending finishedEventStreamHandler event") self.finishedEventStreamHandler?.eventSink?(jsonString) case "nocodes_action_started": - print("NoCodesPlugin: sending actionStartedEventStreamHandler event") self.actionStartedEventStreamHandler?.eventSink?(jsonString) case "nocodes_action_failed": - print("NoCodesPlugin: sending actionFailedEventStreamHandler event") self.actionFailedEventStreamHandler?.eventSink?(jsonString) case "nocodes_action_finished": - print("NoCodesPlugin: sending actionFinishedEventStreamHandler event") self.actionFinishedEventStreamHandler?.eventSink?(jsonString) case "nocodes_screen_failed_to_load": - print("NoCodesPlugin: sending screenFailedToLoadEventStreamHandler event") self.screenFailedToLoadEventStreamHandler?.eventSink?(jsonString) default: From a86b5c000ccadcdb5b0c4079ca832036db2106ec Mon Sep 17 00:00:00 2001 From: Surik Date: Thu, 3 Jul 2025 13:31:09 +0300 Subject: [PATCH 04/10] Renamed NoCodes -> No-Codes in the comments --- lib/src/nocodes/nocodes.dart | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/lib/src/nocodes/nocodes.dart b/lib/src/nocodes/nocodes.dart index a47d0c2..79b6bb4 100644 --- a/lib/src/nocodes/nocodes.dart +++ b/lib/src/nocodes/nocodes.dart @@ -4,23 +4,20 @@ import 'nocodes_config.dart'; import 'nocodes_internal.dart'; import 'presentation_config.dart'; -/// Main NoCodes API class +/// Main No-Codes API class /// /// **Platform Support:** /// - ✅ iOS: Full support /// - ✅ Android: Full support /// - ❌ macOS: Not supported (returns empty streams and no-op methods) -/// -/// NoCodes is a feature for A/B testing and user experience optimization. -/// It allows you to show different screens to users based on experiments. abstract class NoCodes { static NoCodes? _backingInstance; - /// Use this variable to get a current initialized instance of the NoCodes SDK. + /// 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 NoCodes SDK. + /// Returns current initialized instance of the No-Codes SDK. /// Throws exception if the instance has not been initialized static NoCodes getSharedInstance() { NoCodes? instance = _backingInstance; @@ -33,21 +30,21 @@ abstract class NoCodes { return instance; } - /// An entry point to use NoCodes SDK. Call to initialize NoCodes SDK with required config. - /// The function is the best way to set additional configs you need to use NoCodes SDK. + /// 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 NoCodes SDK. + /// Returns initialized instance of the No-Codes SDK. static NoCodes initialize(NoCodesConfig config) { NoCodes instance = NoCodesInternal(config); _backingInstance = instance; return instance; } - /// Initialize NoCodes with project key (for backward compatibility) + /// 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 { @@ -93,12 +90,12 @@ abstract class NoCodes { String? contextKey, }); - /// Show NoCodes screen with context key + /// Show No-Codes screen with context key /// /// **Platform Support:** iOS and Android. No-op on macOS. Future showScreen(String contextKey); - /// Close NoCodes screen + /// Close No-Codes screen /// /// **Platform Support:** iOS and Android. No-op on macOS. Future close(); From 6f6d6aab43fec5d400ae781e03c71b9bcb91dc1b Mon Sep 17 00:00:00 2001 From: Surik Date: Fri, 4 Jul 2025 17:53:07 +0300 Subject: [PATCH 05/10] Added review fixes and bug fixes --- .../QonversionPlugin.kt | 2 +- example/lib/nocodes_view.dart | 11 +++++- ios/Classes/NoCodesPlugin.swift | 36 ++++++++++--------- ios/Classes/SwiftQonversionPlugin.swift | 4 --- lib/src/internal/constants.dart | 5 +++ lib/src/nocodes/nocodes.dart | 4 --- lib/src/nocodes/nocodes_events.dart | 4 +-- lib/src/nocodes/nocodes_internal.dart | 29 +++++---------- lib/src/nocodes/presentation_config.dart | 26 ++++++++------ 9 files changed, 61 insertions(+), 60 deletions(-) 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 bdb3755..041d628 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 @@ -111,6 +111,7 @@ class QonversionPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { "remoteConfigList" -> { return remoteConfigList(result) } + "closeNoCodes" -> noCodesPlugin?.closeNoCodes(result) } // Methods with args @@ -135,7 +136,6 @@ class QonversionPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { "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) - "closeNoCodes" -> noCodesPlugin?.closeNoCodes(result) else -> result.notImplemented() } } diff --git a/example/lib/nocodes_view.dart b/example/lib/nocodes_view.dart index 89c0410..2a48567 100644 --- a/example/lib/nocodes_view.dart +++ b/example/lib/nocodes_view.dart @@ -60,7 +60,7 @@ class _NoCodesViewState extends State { _screenFailedToLoadStream = NoCodes.getSharedInstance().screenFailedToLoadStream.listen((event) { _addEvent('Screen Failed to Load: ${event.payload}'); - NoCodes.getSharedInstance().close(); + // NoCodes.getSharedInstance().close(); }); } @@ -75,6 +75,15 @@ class _NoCodesViewState extends State { Future _showScreen() async { try { + // Set presentation style config before showing screen + final config = NoCodesPresentationConfig( + animated: true, + presentationStyle: NoCodesPresentationStyle.push, + ); + + await NoCodes.getSharedInstance().setScreenPresentationConfig(config, contextKey: 'kamo_test'); + _addEvent('Presentation config set'); + await NoCodes.getSharedInstance().showScreen('kamo_test'); } catch (e) { print('Error showing screen: $e'); diff --git a/ios/Classes/NoCodesPlugin.swift b/ios/Classes/NoCodesPlugin.swift index d3353b3..296e2ce 100644 --- a/ios/Classes/NoCodesPlugin.swift +++ b/ios/Classes/NoCodesPlugin.swift @@ -11,6 +11,13 @@ 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? @@ -22,32 +29,32 @@ public class NoCodesPlugin: NSObject { public func register(_ registrar: FlutterPluginRegistrar) { // Register separate event channels for each event type - let screenShownListener = FlutterListenerWrapper(registrar, postfix: "nocodes_screen_shown") + let screenShownListener = FlutterListenerWrapper(registrar, postfix: eventScreenShown) screenShownListener.register() { eventStreamHandler in self.screenShownEventStreamHandler = eventStreamHandler } - let finishedListener = FlutterListenerWrapper(registrar, postfix: "nocodes_finished") + let finishedListener = FlutterListenerWrapper(registrar, postfix: eventFinished) finishedListener.register() { eventStreamHandler in self.finishedEventStreamHandler = eventStreamHandler } - let actionStartedListener = FlutterListenerWrapper(registrar, postfix: "nocodes_action_started") + let actionStartedListener = FlutterListenerWrapper(registrar, postfix: eventActionStarted) actionStartedListener.register() { eventStreamHandler in self.actionStartedEventStreamHandler = eventStreamHandler } - let actionFailedListener = FlutterListenerWrapper(registrar, postfix: "nocodes_action_failed") + let actionFailedListener = FlutterListenerWrapper(registrar, postfix: eventActionFailed) actionFailedListener.register() { eventStreamHandler in self.actionFailedEventStreamHandler = eventStreamHandler } - let actionFinishedListener = FlutterListenerWrapper(registrar, postfix: "nocodes_action_finished") + let actionFinishedListener = FlutterListenerWrapper(registrar, postfix: eventActionFinished) actionFinishedListener.register() { eventStreamHandler in self.actionFinishedEventStreamHandler = eventStreamHandler } - let screenFailedToLoadListener = FlutterListenerWrapper(registrar, postfix: "nocodes_screen_failed_to_load") + let screenFailedToLoadListener = FlutterListenerWrapper(registrar, postfix: eventScreenFailedToLoad) screenFailedToLoadListener.register() { eventStreamHandler in self.screenFailedToLoadEventStreamHandler = eventStreamHandler } @@ -94,11 +101,6 @@ public class NoCodesPlugin: NSObject { noCodesSandwich?.close() result(nil) } - - public func getAvailableEvents(_ result: @escaping FlutterResult) { - let events = noCodesSandwich?.getAvailableEvents() ?? [] - result(events) - } } extension NoCodesPlugin: NoCodesEventListener { @@ -117,22 +119,22 @@ extension NoCodesPlugin: NoCodesEventListener { DispatchQueue.main.async { switch event { - case "nocodes_screen_shown": + case eventScreenShown: self.screenShownEventStreamHandler?.eventSink?(jsonString) - case "nocodes_finished": + case eventFinished: self.finishedEventStreamHandler?.eventSink?(jsonString) - case "nocodes_action_started": + case eventActionStarted: self.actionStartedEventStreamHandler?.eventSink?(jsonString) - case "nocodes_action_failed": + case eventActionFailed: self.actionFailedEventStreamHandler?.eventSink?(jsonString) - case "nocodes_action_finished": + case eventActionFinished: self.actionFinishedEventStreamHandler?.eventSink?(jsonString) - case "nocodes_screen_failed_to_load": + case eventScreenFailedToLoad: self.screenFailedToLoadEventStreamHandler?.eventSink?(jsonString) default: diff --git a/ios/Classes/SwiftQonversionPlugin.swift b/ios/Classes/SwiftQonversionPlugin.swift index 990090d..36b3268 100644 --- a/ios/Classes/SwiftQonversionPlugin.swift +++ b/ios/Classes/SwiftQonversionPlugin.swift @@ -165,10 +165,6 @@ public class SwiftQonversionPlugin: NSObject, FlutterPlugin { case "showNoCodesScreen": noCodesPlugin?.showScreen(args, result) return - - case "getAvailableNoCodesEvents": - noCodesPlugin?.getAvailableEvents(result) - return default: return result(FlutterMethodNotImplemented) diff --git a/lib/src/internal/constants.dart b/lib/src/internal/constants.dart index 6bd1f5c..088a56d 100644 --- a/lib/src/internal/constants.dart +++ b/lib/src/internal/constants.dart @@ -32,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'; @@ -68,6 +69,10 @@ class Constants { static const mDetachUserFromRemoteConfiguration = 'detachUserFromRemoteConfiguration'; static const mCollectAppleSearchAdsAttribution = 'collectAppleSearchAdsAttribution'; static const mPresentCodeRedemptionSheet = 'presentCodeRedemptionSheet'; + 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/nocodes/nocodes.dart b/lib/src/nocodes/nocodes.dart index 79b6bb4..d763d64 100644 --- a/lib/src/nocodes/nocodes.dart +++ b/lib/src/nocodes/nocodes.dart @@ -100,8 +100,4 @@ abstract class NoCodes { /// **Platform Support:** iOS and Android. No-op on macOS. Future close(); - /// Get available events - /// - /// **Platform Support:** iOS and Android. Returns empty list on macOS. - Future> getAvailableEvents(); } \ No newline at end of file diff --git a/lib/src/nocodes/nocodes_events.dart b/lib/src/nocodes/nocodes_events.dart index eeec612..4c46e32 100644 --- a/lib/src/nocodes/nocodes_events.dart +++ b/lib/src/nocodes/nocodes_events.dart @@ -1,9 +1,9 @@ -/// Base class for all NoCodes events +/// Base class for all No-Codes events abstract class NoCodesEvent { const NoCodesEvent(); } -/// Event when NoCodes screen is shown +/// Event when No-Codes screen is shown class NoCodesScreenShownEvent extends NoCodesEvent { final Map? payload; diff --git a/lib/src/nocodes/nocodes_internal.dart b/lib/src/nocodes/nocodes_internal.dart index 6625e48..72c409a 100644 --- a/lib/src/nocodes/nocodes_internal.dart +++ b/lib/src/nocodes/nocodes_internal.dart @@ -6,6 +6,7 @@ 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'); @@ -25,14 +26,13 @@ class NoCodesInternal implements NoCodes { void _initialize(NoCodesConfig config) { // NoCodes is not supported on macOS if (Platform.isMacOS) { - print('NoCodes is not supported on macOS'); return; } final args = { - 'projectKey': config.projectKey, + Constants.kProjectKey: config.projectKey, }; - _channel.invokeMethod('initializeNoCodes', args); + _channel.invokeMethod(Constants.mInitializeNoCodes, args); } @override @@ -125,44 +125,31 @@ class NoCodesInternal implements NoCodes { String? contextKey, }) async { if (Platform.isMacOS) { - print('NoCodes is not supported on macOS'); return; } final args = { - 'config': config.toMap(), - if (contextKey != null) 'contextKey': contextKey, + Constants.kConfig: config.toMap(), + if (contextKey != null) Constants.kContextKey: contextKey, }; - await _channel.invokeMethod('setScreenPresentationConfig', args); + await _channel.invokeMethod(Constants.mSetScreenPresentationConfig, args); } @override Future showScreen(String contextKey) async { if (Platform.isMacOS) { - print('NoCodes is not supported on macOS'); return; } - await _channel.invokeMethod('showNoCodesScreen', {'contextKey': contextKey}); + await _channel.invokeMethod(Constants.mShowNoCodesScreen, {Constants.kContextKey: contextKey}); } @override Future close() async { if (Platform.isMacOS) { - print('NoCodes is not supported on macOS'); return; } - await _channel.invokeMethod('closeNoCodes'); - } - - @override - Future> getAvailableEvents() async { - if (Platform.isMacOS) { - return []; - } - - final result = await _channel.invokeMethod('getAvailableEvents'); - return List.from(result ?? []); + 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 index 4cdb9ae..731981a 100644 --- a/lib/src/nocodes/presentation_config.dart +++ b/lib/src/nocodes/presentation_config.dart @@ -1,3 +1,9 @@ +static const kPush = 'Push'; +static const kFullScreen = 'FullScreen'; +static const kPopover = 'Popover'; +static const kAnimated = 'animated'; +static const kPresentationStyle = 'presentationStyle'; + /// Presentation style for NoCodes screens enum NoCodesPresentationStyle { push, @@ -16,17 +22,17 @@ class NoCodesPresentationConfig { }); factory NoCodesPresentationConfig.fromMap(Map map) { - final presentationStyleString = map['presentationStyle'] as String?; + final presentationStyleString = map[kPresentationStyle] as String?; NoCodesPresentationStyle presentationStyle; switch (presentationStyleString) { - case 'Push': + case kPush: presentationStyle = NoCodesPresentationStyle.push; break; - case 'FullScreen': + case kFullScreen: presentationStyle = NoCodesPresentationStyle.fullScreen; break; - case 'Popover': + case kPopover: presentationStyle = NoCodesPresentationStyle.popover; break; default: @@ -34,7 +40,7 @@ class NoCodesPresentationConfig { } return NoCodesPresentationConfig( - animated: map['animated'] as bool? ?? true, + animated: map[kAnimated] as bool? ?? true, presentationStyle: presentationStyle, ); } @@ -43,19 +49,19 @@ class NoCodesPresentationConfig { String presentationStyleString; switch (presentationStyle) { case NoCodesPresentationStyle.push: - presentationStyleString = 'Push'; + presentationStyleString = kPush; break; case NoCodesPresentationStyle.fullScreen: - presentationStyleString = 'FullScreen'; + presentationStyleString = kFullScreen; break; case NoCodesPresentationStyle.popover: - presentationStyleString = 'Popover'; + presentationStyleString = kPopover; break; } return { - 'animated': animated, - 'presentationStyle': presentationStyleString, + kAnimated: animated, + kPresentationStyle: presentationStyleString, }; } From c23b6cfeff491753ac920d068d0f9b2c9bc4c7b7 Mon Sep 17 00:00:00 2001 From: Surik Date: Mon, 7 Jul 2025 13:39:12 +0300 Subject: [PATCH 06/10] Review fixes --- android/build.gradle | 2 +- example/lib/nocodes_view.dart | 8 ++++---- ios/Classes/NoCodesPlugin.swift | 12 ++++++------ ios/qonversion_flutter.podspec | 2 +- lib/src/nocodes/presentation_config.dart | 12 ++++++------ macos/qonversion_flutter.podspec | 2 +- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 91e088a..af8fc67 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -53,6 +53,6 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation "io.qonversion:sandwich:6.0.6" + implementation "io.qonversion:sandwich:6.0.8" implementation 'com.google.code.gson:gson:2.9.0' } diff --git a/example/lib/nocodes_view.dart b/example/lib/nocodes_view.dart index 2a48567..cf873a1 100644 --- a/example/lib/nocodes_view.dart +++ b/example/lib/nocodes_view.dart @@ -60,7 +60,7 @@ class _NoCodesViewState extends State { _screenFailedToLoadStream = NoCodes.getSharedInstance().screenFailedToLoadStream.listen((event) { _addEvent('Screen Failed to Load: ${event.payload}'); - // NoCodes.getSharedInstance().close(); + NoCodes.getSharedInstance().close(); }); } @@ -78,13 +78,13 @@ class _NoCodesViewState extends State { // Set presentation style config before showing screen final config = NoCodesPresentationConfig( animated: true, - presentationStyle: NoCodesPresentationStyle.push, + presentationStyle: NoCodesPresentationStyle.fullScreen, ); - await NoCodes.getSharedInstance().setScreenPresentationConfig(config, contextKey: 'kamo_test'); + await NoCodes.getSharedInstance().setScreenPresentationConfig(config, contextKey: 'your_context_key'); _addEvent('Presentation config set'); - await NoCodes.getSharedInstance().showScreen('kamo_test'); + await NoCodes.getSharedInstance().showScreen('your_context_key'); } catch (e) { print('Error showing screen: $e'); _addEvent('Error showing screen: $e'); diff --git a/ios/Classes/NoCodesPlugin.swift b/ios/Classes/NoCodesPlugin.swift index 296e2ce..c1cb83d 100644 --- a/ios/Classes/NoCodesPlugin.swift +++ b/ios/Classes/NoCodesPlugin.swift @@ -119,22 +119,22 @@ extension NoCodesPlugin: NoCodesEventListener { DispatchQueue.main.async { switch event { - case eventScreenShown: + case self.eventScreenShown: self.screenShownEventStreamHandler?.eventSink?(jsonString) - case eventFinished: + case self.eventFinished: self.finishedEventStreamHandler?.eventSink?(jsonString) - case eventActionStarted: + case self.eventActionStarted: self.actionStartedEventStreamHandler?.eventSink?(jsonString) - case eventActionFailed: + case self.eventActionFailed: self.actionFailedEventStreamHandler?.eventSink?(jsonString) - case eventActionFinished: + case self.eventActionFinished: self.actionFinishedEventStreamHandler?.eventSink?(jsonString) - case eventScreenFailedToLoad: + case self.eventScreenFailedToLoad: self.screenFailedToLoadEventStreamHandler?.eventSink?(jsonString) default: diff --git a/ios/qonversion_flutter.podspec b/ios/qonversion_flutter.podspec index f056689..0cd0bc4 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, '13.0' - s.dependency "QonversionSandwich", "6.0.6" + 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/src/nocodes/presentation_config.dart b/lib/src/nocodes/presentation_config.dart index 731981a..639c425 100644 --- a/lib/src/nocodes/presentation_config.dart +++ b/lib/src/nocodes/presentation_config.dart @@ -1,9 +1,3 @@ -static const kPush = 'Push'; -static const kFullScreen = 'FullScreen'; -static const kPopover = 'Popover'; -static const kAnimated = 'animated'; -static const kPresentationStyle = 'presentationStyle'; - /// Presentation style for NoCodes screens enum NoCodesPresentationStyle { push, @@ -13,6 +7,12 @@ enum NoCodesPresentationStyle { /// 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; diff --git a/macos/qonversion_flutter.podspec b/macos/qonversion_flutter.podspec index 2565e76..c03367d 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 "QonversionSandwich", "6.0.6" + s.dependency "QonversionSandwich", "6.0.8" s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } s.swift_version = '5.0' From 8a759de494007c2b4f77b07cd6e8b76d49fef90f Mon Sep 17 00:00:00 2001 From: Surik Sarkisyan Date: Mon, 7 Jul 2025 14:15:02 +0300 Subject: [PATCH 07/10] Update checks.yml --- .github/workflows/checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index ed67d9a..9b26938 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 From ab55e1dfd50c6b02739b4ad2e6e2bafe6b6aea6d Mon Sep 17 00:00:00 2001 From: suriksarkisyan Date: Mon, 7 Jul 2025 11:28:53 +0000 Subject: [PATCH 08/10] [create-pull-request] automated change --- CHANGELOG.md | 3 +++ lib/src/internal/qonversion_internal.dart | 2 +- pubspec.yaml | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29e2c5b..8515cbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 10.0.0 +* // Update changelog here + ## 9.3.1 * Android and iOS stability improvements. diff --git a/lib/src/internal/qonversion_internal.dart b/lib/src/internal/qonversion_internal.dart index 5e6879e..fc0b779 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/pubspec.yaml b/pubspec.yaml index 12508d9..422905e 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' From 924e858f8c8afb0a8c88f1211bdf17d6f053c968 Mon Sep 17 00:00:00 2001 From: Surik Date: Mon, 7 Jul 2025 14:33:12 +0300 Subject: [PATCH 09/10] Updated const --- example/lib/nocodes_view.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/example/lib/nocodes_view.dart b/example/lib/nocodes_view.dart index cf873a1..791bf09 100644 --- a/example/lib/nocodes_view.dart +++ b/example/lib/nocodes_view.dart @@ -2,6 +2,8 @@ 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(); @@ -81,10 +83,10 @@ class _NoCodesViewState extends State { presentationStyle: NoCodesPresentationStyle.fullScreen, ); - await NoCodes.getSharedInstance().setScreenPresentationConfig(config, contextKey: 'your_context_key'); + await NoCodes.getSharedInstance().setScreenPresentationConfig(config, contextKey: CONTEXT_KEY); _addEvent('Presentation config set'); - await NoCodes.getSharedInstance().showScreen('your_context_key'); + await NoCodes.getSharedInstance().showScreen(CONTEXT_KEY); } catch (e) { print('Error showing screen: $e'); _addEvent('Error showing screen: $e'); From db0ef47f564da2979987aa049e249d49f31e8a4b Mon Sep 17 00:00:00 2001 From: Surik Sarkisyan Date: Mon, 7 Jul 2025 14:34:25 +0300 Subject: [PATCH 10/10] Update CHANGELOG.md --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8515cbd..30e6789 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ ## 10.0.0 -* // Update changelog here +### 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.