diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 29b6fdf..4baa14e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -19,6 +19,8 @@ jobs: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 + with: + channel: stable - name: Check flutter version run: flutter --version diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..802dfdd --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,50 @@ +name: Test and Lint + +on: + push: + branches: [main] + pull_request: + branches: + - "*" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: Build, Lint and Test the SDK + runs-on: macos-26 + steps: + - name: Repository checkout + uses: actions/checkout@v5 + - name: Setup Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: "26.1.1" + - name: Install Cocoapods + run: gem install cocoapods + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + - name: Configure JDK + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: "17" + - name: Install dependencies + run: flutter pub get + - name: Build + run: | + cd example + flutter build ios --release --no-codesign + flutter build macos + flutter build apk + - name: Lint + run: make checklint + - name: Test Android + run: make test-android + - name: Test macOS + run: make test-macos + # todo: add iOS testing (with code signing enabled) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5911718..c9731a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## 3.0.0 + +- https://github.com/TelemetryDeck/FlutterSDK/releases/tag/3.0.0 + +- This update introduces the latest features from the native SDKs including: + + - it's now possible to set `floatValue` + - added helper methods: acquiredUser, leadStarted, leadConverted, onboardingCompleted, coreFeatureUsed, paywallShown, purchaseCompleted, referralSent, userRatingSubmitted and errorOccurred + +- [Android] Gradle Kotlin 2.2.20 and Gradle 8.14 + ## 2.5.0 - https://github.com/TelemetryDeck/FlutterSDK/releases/tag/2.5.0 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..dabfb90 --- /dev/null +++ b/Makefile @@ -0,0 +1,44 @@ +.PHONY: help get test analyze checklint lint example-get example-test clean test-all test-android test-ios test-macos test-native + +help: + @perl -nle'print $& if m{^[a-zA-Z_-]+:.*?## .*$$}' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + + +analyze: ## Run static analysis + @flutter analyze + +checklint: ## Checks the project for linting errors, used by the CI + @dart format lib test --set-exit-if-changed + +clean: ## Clean build artifacts + @flutter clean + +example-get: ## Install example app dependencies + @cd example && flutter pub get + +get: ## Install dependencies + @flutter pub get + +lint: ## Applies all auto-correctable lint issues and reformats all source files + @dart format lib test + +test: ## Run unit tests + @flutter test + +test-all: ## Run all tests (Dart + native bridges) + @$(MAKE) test + @$(MAKE) test-native + +test-android: ## Run Android native bridge tests + @cd example/android && ./gradlew telemetrydecksdk:testDebugUnitTest + +test-ios: ## Run iOS native bridge tests + @xcodebuild test -workspace example/ios/Runner.xcworkspace -scheme Runner -sdk iphonesimulator -destination 'platform=iOS Simulator,OS=26.1,name=iPhone 17' -quiet + +test-macos: ## Run macOS native bridge tests + @xcodebuild test -workspace example/macos/Runner.xcworkspace -scheme Runner -destination 'platform=macOS' -quiet + +test-native: ## Run all native bridge tests (iOS, macOS, Android) + @$(MAKE) test-ios + @$(MAKE) test-macos + @$(MAKE) test-android diff --git a/RELEASE.md b/RELEASE.md index 4427b6b..5e40c1c 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -19,7 +19,7 @@ The Flutter SDK depends on the latest major version of the native SDKs. This is On Android, the dependency is configured in `android/build.gradle`: ``` -implementation 'com.telemetrydeck:kotlin-sdk:2.2.0' +implementation 'com.telemetrydeck:kotlin-sdk:6.3.0' ``` On iOS, the dependency is configured in `ios/telemetrydecksdk.podspec` using the podspect Dependency format `s.dependency 'TelemetryClient', '~> 2.0'`. diff --git a/android/build.gradle b/android/build.gradle index 4ba53b7..6dd7f4e 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,14 +2,14 @@ group 'com.telemetrydeck.telemetrydecksdk' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.9.24' + ext.kotlin_version = '2.2.20' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.1.0' + classpath 'com.android.tools.build:gradle:8.11.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -29,15 +29,15 @@ android { namespace 'com.telemetrydeck.telemetrydecksdk' } - compileSdkVersion 33 + compileSdk = 36 compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = JavaVersion.VERSION_17 } sourceSets { @@ -49,21 +49,19 @@ android { minSdkVersion 21 } - dependencies { - implementation 'com.telemetrydeck:kotlin-sdk:6.1.0' - testImplementation 'org.jetbrains.kotlin:kotlin-test' - testImplementation 'org.mockito:mockito-core:5.0.0' - } - testOptions { - unitTests.all { - useJUnitPlatform() - - testLogging { - events "passed", "skipped", "failed", "standardOut", "standardError" - outputs.upToDateWhen {false} - showStandardStreams = true - } + unitTests { + includeAndroidResources = true + returnDefaultValues = true } } } + +dependencies { + implementation 'com.telemetrydeck:kotlin-sdk:6.3.0' + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.jetbrains.kotlin:kotlin-test' + testImplementation 'org.jetbrains.kotlin:kotlin-reflect:2.2.20' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3' + testImplementation 'androidx.arch.core:core-testing:2.2.0' +} diff --git a/android/src/main/kotlin/com/telemetrydeck/telemetrydecksdk/TelemetrydecksdkPlugin.kt b/android/src/main/kotlin/com/telemetrydeck/telemetrydecksdk/TelemetrydecksdkPlugin.kt index 2f4411e..19030c9 100644 --- a/android/src/main/kotlin/com/telemetrydeck/telemetrydecksdk/TelemetrydecksdkPlugin.kt +++ b/android/src/main/kotlin/com/telemetrydeck/telemetrydecksdk/TelemetrydecksdkPlugin.kt @@ -2,7 +2,10 @@ package com.telemetrydeck.telemetrydecksdk import android.app.Application import android.content.Context +import com.telemetrydeck.sdk.PurchaseEvent +import com.telemetrydeck.sdk.PurchaseType import com.telemetrydeck.sdk.TelemetryDeck +import com.telemetrydeck.sdk.params.ErrorCategory import com.telemetrydeck.sdk.providers.DefaultParameterProvider import com.telemetrydeck.sdk.providers.DefaultPrefixProvider import io.flutter.embedding.engine.plugins.FlutterPlugin @@ -74,6 +77,46 @@ class TelemetrydecksdkPlugin : FlutterPlugin, MethodCallHandler { nativeNavigateDestination(call, result) } + "acquiredUser" -> { + nativeAcquiredUser(call, result) + } + + "leadStarted" -> { + nativeLeadStarted(call, result) + } + + "leadConverted" -> { + nativeLeadConverted(call, result) + } + + "onboardingCompleted" -> { + nativeOnboardingCompleted(call, result) + } + + "coreFeatureUsed" -> { + nativeCoreFeatureUsed(call, result) + } + + "paywallShown" -> { + nativePaywallShown(call, result) + } + + "purchaseCompleted" -> { + nativePurchaseCompleted(call, result) + } + + "referralSent" -> { + nativeReferralSent(call, result) + } + + "userRatingSubmitted" -> { + nativeUserRatingSubmitted(call, result) + } + + "errorOccurred" -> { + nativeErrorOccurred(call, result) + } + else -> { result.notImplemented() } @@ -152,9 +195,10 @@ class TelemetrydecksdkPlugin : FlutterPlugin, MethodCallHandler { if (signalType != null) { val clientUser = call.argument("clientUser") val additionalPayload = call.argument>("additionalPayload") + val floatValue = call.argument("floatValue") coroutineScope.launch { - TelemetryDeck.signal(signalType, clientUser, additionalPayload.orEmpty()) + TelemetryDeck.signal(signalType, additionalPayload.orEmpty(), floatValue, clientUser) withContext(Dispatchers.Main) { result.success(null) @@ -205,6 +249,229 @@ class TelemetrydecksdkPlugin : FlutterPlugin, MethodCallHandler { } } + private fun nativeAcquiredUser(call: MethodCall, result: Result) { + val channel = call.argument("channel") + if (channel == null) { + result.error("INVALID_ARGUMENT", "channel is required", null) + return + } + + val params = call.argument>("params") + val customUserID = call.argument("customUserID") + + coroutineScope.launch { + TelemetryDeck.acquiredUser(channel, params.orEmpty(), customUserID) + withContext(Dispatchers.Main) { + result.success(null) + } + } + } + + private fun nativeLeadStarted(call: MethodCall, result: Result) { + val leadId = call.argument("leadId") + if (leadId == null) { + result.error("INVALID_ARGUMENT", "leadId is required", null) + return + } + + val params = call.argument>("params") + val customUserID = call.argument("customUserID") + + coroutineScope.launch { + TelemetryDeck.leadStarted(leadId, params.orEmpty(), customUserID) + withContext(Dispatchers.Main) { + result.success(null) + } + } + } + + private fun nativeLeadConverted(call: MethodCall, result: Result) { + val leadId = call.argument("leadId") + if (leadId == null) { + result.error("INVALID_ARGUMENT", "leadId is required", null) + return + } + + val params = call.argument>("params") + val customUserID = call.argument("customUserID") + + coroutineScope.launch { + TelemetryDeck.leadConverted(leadId, params.orEmpty(), customUserID) + withContext(Dispatchers.Main) { + result.success(null) + } + } + } + + private fun nativeOnboardingCompleted(call: MethodCall, result: Result) { + val params = call.argument>("params") + val customUserID = call.argument("customUserID") + + coroutineScope.launch { + TelemetryDeck.onboardingCompleted(params.orEmpty(), customUserID) + withContext(Dispatchers.Main) { + result.success(null) + } + } + } + + private fun nativeCoreFeatureUsed(call: MethodCall, result: Result) { + val featureName = call.argument("featureName") + if (featureName == null) { + result.error("INVALID_ARGUMENT", "featureName is required", null) + return + } + + val params = call.argument>("params") + val customUserID = call.argument("customUserID") + + coroutineScope.launch { + TelemetryDeck.coreFeatureUsed(featureName, params.orEmpty(), customUserID) + withContext(Dispatchers.Main) { + result.success(null) + } + } + } + + private fun nativePaywallShown(call: MethodCall, result: Result) { + val reason = call.argument("reason") + if (reason == null) { + result.error("INVALID_ARGUMENT", "reason is required", null) + return + } + + val params = call.argument>("params") + val customUserID = call.argument("customUserID") + + coroutineScope.launch { + TelemetryDeck.paywallShown(reason, params.orEmpty(), customUserID) + withContext(Dispatchers.Main) { + result.success(null) + } + } + } + + private fun nativePurchaseCompleted(call: MethodCall, result: Result) { + val eventString = call.argument("event") + val countryCode = call.argument("countryCode") + val productID = call.argument("productID") + val purchaseTypeString = call.argument("purchaseType") + val priceAmountMicros = call.argument("priceAmountMicros") + val currencyCode = call.argument("currencyCode") + + if (eventString == null || countryCode == null || productID == null || + purchaseTypeString == null || priceAmountMicros == null || currencyCode == null) { + result.error("INVALID_ARGUMENT", "event, countryCode, productID, purchaseType, priceAmountMicros, and currencyCode are required", null) + return + } + + val event = when (eventString) { + "purchaseCompleted" -> PurchaseEvent.PAID_PURCHASE + "freeTrialStarted" -> PurchaseEvent.STARTED_FREE_TRIAL + "convertedFromFreeTrial" -> PurchaseEvent.CONVERTED_FROM_TRIAL + else -> { + result.error("INVALID_ARGUMENT", "Invalid purchase event: $eventString", null) + return + } + } + + val purchaseType = when (purchaseTypeString) { + "subscription" -> PurchaseType.SUBSCRIPTION + "oneTimePurchase" -> PurchaseType.ONE_TIME_PURCHASE + else -> { + result.error("INVALID_ARGUMENT", "Invalid purchase type: $purchaseTypeString", null) + return + } + } + + val offerID = call.argument("offerID") + val params = call.argument>("params") + val customUserID = call.argument("customUserID") + + coroutineScope.launch { + TelemetryDeck.purchaseCompleted( + event, + countryCode, + productID, + purchaseType, + priceAmountMicros, + currencyCode, + offerID, + params.orEmpty(), + customUserID + ) + withContext(Dispatchers.Main) { + result.success(null) + } + } + } + + private fun nativeReferralSent(call: MethodCall, result: Result) { + val receiversCount = call.argument("receiversCount") + if (receiversCount == null) { + result.error("INVALID_ARGUMENT", "receiversCount is required", null) + return + } + + val kind = call.argument("kind") + val params = call.argument>("params") + val customUserID = call.argument("customUserID") + + coroutineScope.launch { + TelemetryDeck.referralSent(receiversCount, kind, params.orEmpty(), customUserID) + withContext(Dispatchers.Main) { + result.success(null) + } + } + } + + private fun nativeUserRatingSubmitted(call: MethodCall, result: Result) { + val rating = call.argument("rating") + if (rating == null) { + result.error("INVALID_ARGUMENT", "rating is required", null) + return + } + + val comment = call.argument("comment") + val params = call.argument>("params") + val customUserID = call.argument("customUserID") + + coroutineScope.launch { + TelemetryDeck.userRatingSubmitted(rating, comment, params.orEmpty(), customUserID) + withContext(Dispatchers.Main) { + result.success(null) + } + } + } + + private fun nativeErrorOccurred(call: MethodCall, result: Result) { + val id = call.argument("id") + if (id == null) { + result.error("INVALID_ARGUMENT", "id is required", null) + return + } + + val categoryString = call.argument("category") + val category = when (categoryString) { + "thrownException" -> ErrorCategory.ThrownException + "userInput" -> ErrorCategory.UserInput + "appState" -> ErrorCategory.AppState + else -> null + } + + val message = call.argument("message") + val parameters = call.argument>("parameters") + val floatValue = call.argument("floatValue") + val customUserID = call.argument("customUserID") + + coroutineScope.launch { + TelemetryDeck.errorOccurred(id, category, message, parameters.orEmpty(), floatValue, customUserID) + withContext(Dispatchers.Main) { + result.success(null) + } + } + } + private fun nativeInitialize(call: MethodCall, result: Result) { val arguments = call.arguments as? Map<*, *> // Cast to a Map if (arguments != null) { diff --git a/android/src/test/kotlin/com/telemetrydeck/telemetrydecksdk/TelemetrydecksdkPluginTest.kt b/android/src/test/kotlin/com/telemetrydeck/telemetrydecksdk/TelemetrydecksdkPluginTest.kt new file mode 100644 index 0000000..90ff6ba --- /dev/null +++ b/android/src/test/kotlin/com/telemetrydeck/telemetrydecksdk/TelemetrydecksdkPluginTest.kt @@ -0,0 +1,282 @@ +package com.telemetrydeck.telemetrydecksdk + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.telemetrydeck.sdk.Signal +import com.telemetrydeck.sdk.TelemetryDeck +import com.telemetrydeck.sdk.TelemetryManagerConfiguration +import org.junit.Rule +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class TelemetrydecksdkPluginTest { + + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @Test + fun testSignalBasicProperties() { + val appID = "32CB6574-6732-4238-879F-582FEBEB6536" + val config = TelemetryManagerConfiguration(appID) + val manager = TelemetryDeck.Builder().configuration(config).build(null) + + manager.signal("TestSignal", mapOf("key1" to "value1")) + + val signals = manager.cache?.empty() + assertNotNull(signals) + assertTrue(signals.isNotEmpty()) + + val signal = signals.first() + assertEquals("TestSignal", signal.type) + } + + @Test + fun testSignalWithFloatValue() { + val appID = "32CB6574-6732-4238-879F-582FEBEB6536" + val config = TelemetryManagerConfiguration(appID) + val manager = TelemetryDeck.Builder().configuration(config).build(null) + + manager.signal("MetricSignal", emptyMap(), 42.5) + + val signal = manager.cache?.empty()?.first() + assertNotNull(signal) + assertEquals("MetricSignal", signal.type) + assertEquals(42.5, signal.floatValue) + } + + @Test + fun testSignalWithCustomUser() { + val appID = "32CB6574-6732-4238-879F-582FEBEB6536" + val config = TelemetryManagerConfiguration(appID) + val manager = TelemetryDeck.Builder().configuration(config).build(null) + + manager.signal("UserSignal", emptyMap(), null, "test-user-123") + + val signal = manager.cache?.empty()?.first() + assertNotNull(signal) + assertEquals("UserSignal", signal.type) + assertNotNull(signal.clientUser) + } + + @Test + fun testMultipleSignalsInCache() { + val appID = "32CB6574-6732-4238-879F-582FEBEB6536" + val config = TelemetryManagerConfiguration(appID) + val manager = TelemetryDeck.Builder().configuration(config).build(null) + + manager.signal("Signal1", emptyMap()) + manager.signal("Signal2", emptyMap()) + manager.signal("Signal3", emptyMap()) + + val signals = manager.cache?.empty() + assertNotNull(signals) + assertEquals(3, signals.size) + + val types = signals.map { it.type } + assertTrue(types.contains("Signal1")) + assertTrue(types.contains("Signal2")) + assertTrue(types.contains("Signal3")) + } + + @Test + fun testCacheClearedAfterEmpty() { + val appID = "32CB6574-6732-4238-879F-582FEBEB6536" + val config = TelemetryManagerConfiguration(appID) + val manager = TelemetryDeck.Builder().configuration(config).build(null) + + manager.signal("TestSignal1", mapOf("key1" to "value1")) + + val firstRead = manager.cache?.empty() + assertNotNull(firstRead) + assertTrue(firstRead.isNotEmpty()) + + val secondRead = manager.cache?.empty() + assertNotNull(secondRead) + assertTrue(secondRead.isEmpty()) + } + + @Test + fun testSignalPayloadFormat() { + val appID = "32CB6574-6732-4238-879F-582FEBEB6536" + val config = TelemetryManagerConfiguration(appID) + val manager = TelemetryDeck.Builder().configuration(config).build(null) + + manager.signal("TestSignal", mapOf("customParam" to "customValue")) + + val signal = manager.cache?.empty()?.first() + assertNotNull(signal) + + val payloadItem = signal.payload.firstOrNull { it.startsWith("customParam:") } + assertEquals("customParam:customValue", payloadItem) + } + + @Test + fun testNavigationSignalStructure() { + val appID = "32CB6574-6732-4238-879F-582FEBEB6536" + val config = TelemetryManagerConfiguration(appID) + val manager = TelemetryDeck.Builder().configuration(config).build(null) + + manager.navigate("/home", "/profile", "nav-user-789") + + val signal = manager.cache?.empty()?.first() + assertNotNull(signal) + assertEquals("TelemetryDeck.Navigation.pathChanged", signal.type) + + val sourcePath = signal.payload.firstOrNull { it.startsWith("TelemetryDeck.Navigation.sourcePath:") } + assertEquals("TelemetryDeck.Navigation.sourcePath:/home", sourcePath) + + val destinationPath = signal.payload.firstOrNull { it.startsWith("TelemetryDeck.Navigation.destinationPath:") } + assertEquals("TelemetryDeck.Navigation.destinationPath:/profile", destinationPath) + } + + @Test + fun testSignalWithComplexPayload() { + val appID = "32CB6574-6732-4238-879F-582FEBEB6536" + val config = TelemetryManagerConfiguration(appID) + val manager = TelemetryDeck.Builder().configuration(config).build(null) + + val complexPayload = mapOf( + "feature" to "export", + "format" to "pdf", + "pages" to "10", + "quality" to "high" + ) + + manager.signal("Export.Completed", complexPayload, 123.45, "user-456") + + val signal = manager.cache?.empty()?.first() + assertNotNull(signal) + assertEquals("Export.Completed", signal.type) + assertEquals(123.45, signal.floatValue) + + assertEquals("feature:export", signal.payload.firstOrNull { it.startsWith("feature:") }) + assertEquals("format:pdf", signal.payload.firstOrNull { it.startsWith("format:") }) + assertEquals("pages:10", signal.payload.firstOrNull { it.startsWith("pages:") }) + assertEquals("quality:high", signal.payload.firstOrNull { it.startsWith("quality:") }) + } + + @Test + fun testTestModeEnabled() { + val appID = "32CB6574-6732-4238-879F-582FEBEB6536" + val manager = TelemetryDeck.Builder() + .appID(appID) + .testMode(true) + .build(null) + + manager.signal("TestSignal", emptyMap()) + + val signal = manager.cache?.empty()?.first() + assertNotNull(signal) + assertEquals("true", signal.isTestMode) + } + + @Test + fun testTestModeDisabled() { + val appID = "32CB6574-6732-4238-879F-582FEBEB6536" + val manager = TelemetryDeck.Builder() + .appID(appID) + .testMode(false) + .build(null) + + manager.signal("TestSignal", emptyMap()) + + val signal = manager.cache?.empty()?.first() + assertNotNull(signal) + assertEquals("false", signal.isTestMode) + } + + @Test + fun testCustomSaltApplied() { + val appID = "32CB6574-6732-4238-879F-582FEBEB6536" + val config = TelemetryManagerConfiguration(appID) + config.salt = "my salt" + val manager = TelemetryDeck.Builder().configuration(config).build(null) + + manager.signal("TestSignal", "clientUser", emptyMap()) + + val signal = manager.cache?.empty()?.first() + assertNotNull(signal) + assertEquals("9a68a3790deb1db66f80855b8e7c5a97df8002ef90d3039f9e16c94cfbd11d99", signal.clientUser) + } + + @Test + fun testAcquiredUserSignal() { + val appID = "32CB6574-6732-4238-879F-582FEBEB6536" + val manager = TelemetryDeck.Builder().appID(appID).build(null) + + manager.acquiredUser("channel 1") + + val signal = manager.cache?.empty()?.first() + assertNotNull(signal) + assertEquals("TelemetryDeck.Acquisition.userAcquired", signal.type) + assertEquals( + "TelemetryDeck.Acquisition.channel:channel 1", + signal.payload.firstOrNull { it.startsWith("TelemetryDeck.Acquisition.channel:") } + ) + } + + @Test + fun testLeadStartedSignal() { + val appID = "32CB6574-6732-4238-879F-582FEBEB6536" + val manager = TelemetryDeck.Builder().appID(appID).build(null) + + manager.leadStarted("lead 1") + + val signal = manager.cache?.empty()?.first() + assertNotNull(signal) + assertEquals("TelemetryDeck.Acquisition.leadStarted", signal.type) + assertEquals( + "TelemetryDeck.Acquisition.leadID:lead 1", + signal.payload.firstOrNull { it.startsWith("TelemetryDeck.Acquisition.leadID:") } + ) + } + + @Test + fun testLeadConvertedSignal() { + val appID = "32CB6574-6732-4238-879F-582FEBEB6536" + val manager = TelemetryDeck.Builder().appID(appID).build(null) + + manager.leadConverted("lead 1") + + val signal = manager.cache?.empty()?.first() + assertNotNull(signal) + assertEquals("TelemetryDeck.Acquisition.leadConverted", signal.type) + assertEquals( + "TelemetryDeck.Acquisition.leadID:lead 1", + signal.payload.firstOrNull { it.startsWith("TelemetryDeck.Acquisition.leadID:") } + ) + } + + @Test + fun testCoreFeatureUsedSignal() { + val appID = "32CB6574-6732-4238-879F-582FEBEB6536" + val manager = TelemetryDeck.Builder().appID(appID).build(null) + + manager.coreFeatureUsed("feature 1") + + val signal = manager.cache?.empty()?.first() + assertNotNull(signal) + assertEquals("TelemetryDeck.Activation.coreFeatureUsed", signal.type) + assertEquals( + "TelemetryDeck.Activation.featureName:feature 1", + signal.payload.firstOrNull { it.startsWith("TelemetryDeck.Activation.featureName:") } + ) + } + + @Test + fun testPaywallShownSignal() { + val appID = "32CB6574-6732-4238-879F-582FEBEB6536" + val manager = TelemetryDeck.Builder().appID(appID).build(null) + + manager.paywallShown("trial_ended") + + val signal = manager.cache?.empty()?.first() + assertNotNull(signal) + assertEquals("TelemetryDeck.Revenue.paywallShown", signal.type) + assertEquals( + "TelemetryDeck.Revenue.paywallShowReason:trial_ended", + signal.payload.firstOrNull { it.startsWith("TelemetryDeck.Revenue.paywallShowReason:") } + ) + } +} diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle deleted file mode 100644 index 7412694..0000000 --- a/example/android/app/build.gradle +++ /dev/null @@ -1,67 +0,0 @@ -plugins { - id "com.android.application" - id "kotlin-android" - id "dev.flutter.flutter-gradle-plugin" -} - -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' -} - -def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' -} - -android { - namespace "com.telemetrydeck.telemetrydecksdk_example" - compileSdkVersion flutter.compileSdkVersion - ndkVersion flutter.ndkVersion - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = '1.8' - } - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - } - - defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "com.telemetrydeck.telemetrydecksdk_example" - // You can update the following values to match your application needs. - // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. - minSdkVersion 21 - targetSdkVersion flutter.targetSdkVersion - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - } - - buildTypes { - release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug - } - } -} - -flutter { - source '../..' -} - -dependencies {} diff --git a/example/android/app/build.gradle.kts b/example/android/app/build.gradle.kts new file mode 100644 index 0000000..9c230ad --- /dev/null +++ b/example/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.example.telemetrydecksdk_example" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.telemetrydecksdk_example" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/example/android/build.gradle b/example/android/build.gradle deleted file mode 100644 index cff2187..0000000 --- a/example/android/build.gradle +++ /dev/null @@ -1,30 +0,0 @@ -buildscript { - ext.kotlin_version = '2.0.21' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - -allprojects { - repositories { - google() - mavenCentral() - } -} - -rootProject.buildDir = '../build' -subprojects { - project.buildDir = "${rootProject.buildDir}/${project.name}" -} -subprojects { - project.evaluationDependsOn(':app') -} - -tasks.register("clean", Delete) { - delete rootProject.buildDir -} diff --git a/example/android/build.gradle.kts b/example/android/build.gradle.kts new file mode 100644 index 0000000..dbee657 --- /dev/null +++ b/example/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 09523c0..e4ef43f 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip -networkTimeout=10000 -validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip diff --git a/example/android/settings.gradle b/example/android/settings.gradle deleted file mode 100644 index 80cb46b..0000000 --- a/example/android/settings.gradle +++ /dev/null @@ -1,29 +0,0 @@ -pluginManagement { - def flutterSdkPath = { - def properties = new Properties() - file("local.properties").withInputStream { properties.load(it) } - def flutterSdkPath = properties.getProperty("flutter.sdk") - assert flutterSdkPath != null, "flutter.sdk not set in local.properties" - return flutterSdkPath - } - settings.ext.flutterSdkPath = flutterSdkPath() - - includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") - - repositories { - google() - mavenCentral() - gradlePluginPortal() - } - - plugins { - id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false - } -} - -plugins { - id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.6.1" apply false -} - -include ":app" diff --git a/example/android/settings.gradle.kts b/example/android/settings.gradle.kts new file mode 100644 index 0000000..ca7fe06 --- /dev/null +++ b/example/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.11.1" apply false + id("org.jetbrains.kotlin.android") version "2.2.20" apply false +} + +include(":app") diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 50bb188..2520608 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -23,4 +23,4 @@ MinimumOSVersion 12.0 - \ No newline at end of file + diff --git a/example/ios/Podfile b/example/ios/Podfile index 164df53..3e44f9c 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/Podfile.lock b/example/ios/Podfile.lock index 7836e30..dee0723 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -2,10 +2,10 @@ PODS: - Flutter (1.0.0) - integration_test (0.0.1): - Flutter - - TelemetryDeck (2.9.3) + - TelemetryDeck (2.11.0) - telemetrydecksdk (1.0.0): - Flutter - - TelemetryDeck (~> 2.9.3) + - TelemetryDeck (~> 2.11.0) DEPENDENCIES: - Flutter (from `Flutter`) @@ -25,11 +25,11 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/telemetrydecksdk/ios" SPEC CHECKSUMS: - Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e - TelemetryDeck: 5331ca64c53ab1ab6162c558c8b46b412c744483 - telemetrydecksdk: 99b635696b35527877d918a9844a798a24e1828b + TelemetryDeck: 50c6ddb1031ee91e7828d5f023af1419f9aadcad + telemetrydecksdk: 9947cdb29545b0ce3356b6b19e524bdc2303df7c -PODFILE CHECKSUM: 7be2f5f74864d463a8ad433546ed1de7e0f29aef +PODFILE CHECKSUM: a57f30d18f102dd3ce366b1d62a55ecbef2158e5 COCOAPODS: 1.16.2 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 313fdf5..23bae70 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -452,7 +452,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -580,7 +580,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -629,7 +629,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 8e3ca5d..15cada4 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -59,6 +59,7 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> diff --git a/example/ios/RunnerTests/RunnerTests.swift b/example/ios/RunnerTests/RunnerTests.swift index 6e9632d..de2ee82 100644 --- a/example/ios/RunnerTests/RunnerTests.swift +++ b/example/ios/RunnerTests/RunnerTests.swift @@ -1,9 +1,782 @@ import Flutter import UIKit import XCTest +import TelemetryDeck @testable import telemetrydecksdk -// This demonstrates a simple unit test of the Swift portion of this plugin's implementation. -// -// See https://developer.apple.com/documentation/xctest for more information about using XCTest. +class RunnerTests: XCTestCase { + var plugin: TelemetrydecksdkPlugin! + + override func setUp() { + super.setUp() + plugin = TelemetrydecksdkPlugin() + } + + override func tearDown() { + plugin = nil + if TelemetryManager.isInitialized { + TelemetryDeck.terminate() + } + super.tearDown() + } + + func testStart_withRequiredAppID() { + let call = FlutterMethodCall(methodName: "start", arguments: [ + "appID": "32CB6574-6732-4238-879F-582FEBEB6536" + ]) + + let expectation = expectation(description: "start completes") + plugin.handle(call) { result in + XCTAssertNil(result) + XCTAssertTrue(TelemetryManager.isInitialized) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testStart_withMissingAppID_returnsError() { + let call = FlutterMethodCall(methodName: "start", arguments: [:]) + + let expectation = expectation(description: "start returns error") + plugin.handle(call) { result in + guard let error = result as? FlutterError else { + XCTFail("Expected FlutterError") + return + } + XCTAssertEqual(error.code, "INVALID_ARGUMENT") + XCTAssertEqual(error.message, "Expected value appID is not provided.") + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testStart_withOptionalParameters() { + let call = FlutterMethodCall(methodName: "start", arguments: [ + "appID": "32CB6574-6732-4238-879F-582FEBEB6536", + "apiBaseURL": "https://nom.telemetrydeck.com", + "salt": "customSalt", + "namespace": "testNamespace", + "defaultUser": "testUser", + "debug": true, + "testMode": true, + "defaultSignalPrefix": "Test.", + "defaultParameterPrefix": "Custom.", + "defaultParameters": ["key": "value"] + ]) + + let expectation = expectation(description: "start completes with options") + plugin.handle(call) { result in + XCTAssertNil(result) + XCTAssertTrue(TelemetryManager.isInitialized) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testStop() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "stop", arguments: nil) + + let expectation = expectation(description: "stop completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testSend_withRequiredSignalType() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "send", arguments: [ + "signalType": "Test.Event" + ]) + + let expectation = expectation(description: "send completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testSend_withAllParameters() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "send", arguments: [ + "signalType": "Test.Event", + "clientUser": "testUser123", + "additionalPayload": ["key1": "value1", "key2": "value2"], + "floatValue": 42.5 + ]) + + let expectation = expectation(description: "send completes with params") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testSend_withMissingSignalType_returnsError() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "send", arguments: [:]) + + let expectation = expectation(description: "send returns error") + plugin.handle(call) { result in + guard let error = result as? FlutterError else { + XCTFail("Expected FlutterError") + return + } + XCTAssertEqual(error.code, "INVALID_ARGUMENT") + XCTAssertEqual(error.message, "Missing required argument signalType") + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testGenerateNewSession() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "generateNewSession", arguments: nil) + + let expectation = expectation(description: "generateNewSession completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testUpdateDefaultUser() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "updateDefaultUser", arguments: "newUserID") + + let expectation = expectation(description: "updateDefaultUser completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testNavigate_withSourceAndDestination() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "navigate", arguments: [ + "sourcePath": "/home", + "destinationPath": "/profile" + ]) + + let expectation = expectation(description: "navigate completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testNavigate_withMissingParameters_returnsError() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "navigate", arguments: [ + "sourcePath": "/home" + ]) + + let expectation = expectation(description: "navigate returns error") + plugin.handle(call) { result in + guard let error = result as? FlutterError else { + XCTFail("Expected FlutterError") + return + } + XCTAssertEqual(error.code, "INVALID_ARGUMENT") + XCTAssertEqual(error.message, "sourcePath and destinationPath are required") + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testNavigateToDestination() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "navigateToDestination", arguments: [ + "destinationPath": "/settings" + ]) + + let expectation = expectation(description: "navigateToDestination completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testNavigateToDestination_withMissingDestination_returnsError() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "navigateToDestination", arguments: [:]) + + let expectation = expectation(description: "navigateToDestination returns error") + plugin.handle(call) { result in + guard let error = result as? FlutterError else { + XCTFail("Expected FlutterError") + return + } + XCTAssertEqual(error.code, "INVALID_ARGUMENT") + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testAcquiredUser_withChannel() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "acquiredUser", arguments: [ + "channel": "organic" + ]) + + let expectation = expectation(description: "acquiredUser completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testAcquiredUser_withAllParameters() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "acquiredUser", arguments: [ + "channel": "facebook", + "params": ["campaign": "summer2024"], + "customUserID": "user123" + ]) + + let expectation = expectation(description: "acquiredUser completes with params") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testAcquiredUser_withMissingChannel_returnsError() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "acquiredUser", arguments: [:]) + + let expectation = expectation(description: "acquiredUser returns error") + plugin.handle(call) { result in + guard let error = result as? FlutterError else { + XCTFail("Expected FlutterError") + return + } + XCTAssertEqual(error.code, "INVALID_ARGUMENT") + XCTAssertEqual(error.message, "channel is required") + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testLeadStarted() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "leadStarted", arguments: [ + "leadId": "lead_12345", + "params": ["source": "website"], + "customUserID": "user456" + ]) + + let expectation = expectation(description: "leadStarted completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testLeadStarted_withMissingLeadId_returnsError() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "leadStarted", arguments: [:]) + + let expectation = expectation(description: "leadStarted returns error") + plugin.handle(call) { result in + guard let error = result as? FlutterError else { + XCTFail("Expected FlutterError") + return + } + XCTAssertEqual(error.code, "INVALID_ARGUMENT") + XCTAssertEqual(error.message, "leadId is required") + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testLeadConverted() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "leadConverted", arguments: [ + "leadId": "lead_12345", + "params": ["value": "100"], + "customUserID": "user456" + ]) + + let expectation = expectation(description: "leadConverted completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testLeadConverted_withMissingLeadId_returnsError() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "leadConverted", arguments: [:]) + + let expectation = expectation(description: "leadConverted returns error") + plugin.handle(call) { result in + guard let error = result as? FlutterError else { + XCTFail("Expected FlutterError") + return + } + XCTAssertEqual(error.code, "INVALID_ARGUMENT") + XCTAssertEqual(error.message, "leadId is required") + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testOnboardingCompleted() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "onboardingCompleted", arguments: [ + "params": ["steps": "5"], + "customUserID": "user789" + ]) + + let expectation = expectation(description: "onboardingCompleted completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testCoreFeatureUsed() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "coreFeatureUsed", arguments: [ + "featureName": "darkMode", + "params": ["enabled": "true"], + "customUserID": "user123" + ]) + + let expectation = expectation(description: "coreFeatureUsed completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testCoreFeatureUsed_withMissingFeatureName_returnsError() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "coreFeatureUsed", arguments: [:]) + + let expectation = expectation(description: "coreFeatureUsed returns error") + plugin.handle(call) { result in + guard let error = result as? FlutterError else { + XCTFail("Expected FlutterError") + return + } + XCTAssertEqual(error.code, "INVALID_ARGUMENT") + XCTAssertEqual(error.message, "featureName is required") + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testPaywallShown() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "paywallShown", arguments: [ + "reason": "featureAccess", + "params": ["feature": "premium"], + "customUserID": "user456" + ]) + + let expectation = expectation(description: "paywallShown completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testPaywallShown_withMissingReason_returnsError() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "paywallShown", arguments: [:]) + + let expectation = expectation(description: "paywallShown returns error") + plugin.handle(call) { result in + guard let error = result as? FlutterError else { + XCTFail("Expected FlutterError") + return + } + XCTAssertEqual(error.code, "INVALID_ARGUMENT") + XCTAssertEqual(error.message, "reason is required") + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testPurchaseCompleted_withDefaultEvent() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "purchaseCompleted", arguments: [ + "event": "completed", + "countryCode": "US", + "productID": "com.app.premium", + "purchaseType": "subscription", + "priceAmountMicros": 4990000, + "currencyCode": "USD" + ]) + + let expectation = expectation(description: "purchaseCompleted completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testPurchaseCompleted_withFreeTrialStarted() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "purchaseCompleted", arguments: [ + "event": "freeTrialStarted", + "countryCode": "DE", + "productID": "com.app.pro", + "purchaseType": "subscription", + "priceAmountMicros": 9990000, + "currencyCode": "EUR", + "offerID": "trial7days" + ]) + + let expectation = expectation(description: "purchaseCompleted with trial completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testPurchaseCompleted_withConvertedFromFreeTrial() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "purchaseCompleted", arguments: [ + "event": "convertedFromFreeTrial", + "countryCode": "UK", + "productID": "com.app.yearly", + "purchaseType": "subscription", + "priceAmountMicros": 49990000, + "currencyCode": "GBP" + ]) + + let expectation = expectation(description: "purchaseCompleted with conversion completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testPurchaseCompleted_withMissingParameters_returnsError() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "purchaseCompleted", arguments: [ + "event": "completed", + "countryCode": "US" + ]) + + let expectation = expectation(description: "purchaseCompleted returns error") + plugin.handle(call) { result in + guard let error = result as? FlutterError else { + XCTFail("Expected FlutterError") + return + } + XCTAssertEqual(error.code, "INVALID_ARGUMENT") + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testReferralSent_withDefaults() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "referralSent", arguments: [:]) + + let expectation = expectation(description: "referralSent completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testReferralSent_withAllParameters() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "referralSent", arguments: [ + "receiversCount": 3, + "kind": "email", + "params": ["campaign": "refer_friend"], + "customUserID": "user789" + ]) + + let expectation = expectation(description: "referralSent with params completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testUserRatingSubmitted() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "userRatingSubmitted", arguments: [ + "rating": 5, + "comment": "Great app!", + "params": ["platform": "iOS"], + "customUserID": "user123" + ]) + + let expectation = expectation(description: "userRatingSubmitted completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testUserRatingSubmitted_withMissingRating_returnsError() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "userRatingSubmitted", arguments: [:]) + + let expectation = expectation(description: "userRatingSubmitted returns error") + plugin.handle(call) { result in + guard let error = result as? FlutterError else { + XCTFail("Expected FlutterError") + return + } + XCTAssertEqual(error.code, "INVALID_ARGUMENT") + XCTAssertEqual(error.message, "rating is required") + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testErrorOccurred_withMinimalParameters() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "errorOccurred", arguments: [ + "id": "error_001" + ]) + + let expectation = expectation(description: "errorOccurred completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testErrorOccurred_withThrownExceptionCategory() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "errorOccurred", arguments: [ + "id": "error_002", + "category": "thrownException", + "message": "Null pointer exception", + "parameters": ["file": "main.dart"], + "floatValue": 1.0, + "customUserID": "user456" + ]) + + let expectation = expectation(description: "errorOccurred with category completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testErrorOccurred_withUserInputCategory() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "errorOccurred", arguments: [ + "id": "error_003", + "category": "userInput" + ]) + + let expectation = expectation(description: "errorOccurred with userInput completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testErrorOccurred_withAppStateCategory() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "errorOccurred", arguments: [ + "id": "error_004", + "category": "appState" + ]) + + let expectation = expectation(description: "errorOccurred with appState completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testErrorOccurred_withUnknownCategory() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "errorOccurred", arguments: [ + "id": "error_005", + "category": "unknownCategory" + ]) + + let expectation = expectation(description: "errorOccurred with unknown category completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testErrorOccurred_withMissingId_returnsError() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "errorOccurred", arguments: [:]) + + let expectation = expectation(description: "errorOccurred returns error") + plugin.handle(call) { result in + guard let error = result as? FlutterError else { + XCTFail("Expected FlutterError") + return + } + XCTAssertEqual(error.code, "INVALID_ARGUMENT") + XCTAssertEqual(error.message, "id is required") + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testStartDurationSignal() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "startDurationSignal", arguments: [ + "signalType": "Task.Duration", + "parameters": ["taskType": "download"] + ]) + + let expectation = expectation(description: "startDurationSignal completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testStartDurationSignal_withMissingSignalType_returnsError() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "startDurationSignal", arguments: [:]) + + let expectation = expectation(description: "startDurationSignal returns error") + plugin.handle(call) { result in + guard let error = result as? FlutterError else { + XCTFail("Expected FlutterError") + return + } + XCTAssertEqual(error.code, "INVALID_ARGUMENT") + XCTAssertEqual(error.message, "Missing required argument signalType") + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testStopAndSendDurationSignal() { + initializeSDK() + + let startCall = FlutterMethodCall(methodName: "startDurationSignal", arguments: [ + "signalType": "Task.Duration", + "parameters": ["taskType": "upload"] + ]) + plugin.handle(startCall) { _ in } + + let stopCall = FlutterMethodCall(methodName: "stopAndSendDurationSignal", arguments: [ + "signalType": "Task.Duration", + "parameters": ["taskType": "upload"] + ]) + + let expectation = expectation(description: "stopAndSendDurationSignal completes") + plugin.handle(stopCall) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testStopAndSendDurationSignal_withMissingSignalType_returnsError() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "stopAndSendDurationSignal", arguments: [:]) + + let expectation = expectation(description: "stopAndSendDurationSignal returns error") + plugin.handle(call) { result in + guard let error = result as? FlutterError else { + XCTFail("Expected FlutterError") + return + } + XCTAssertEqual(error.code, "INVALID_ARGUMENT") + XCTAssertEqual(error.message, "Missing required argument signalType") + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testUnknownMethod_returnsNotImplemented() { + let call = FlutterMethodCall(methodName: "unknownMethod", arguments: nil) + + let expectation = expectation(description: "unknown method returns not implemented") + plugin.handle(call) { result in + XCTAssertTrue((result as? NSObject) === FlutterMethodNotImplemented) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + private func initializeSDK() { + let call = FlutterMethodCall(methodName: "start", arguments: [ + "appID": "32CB6574-6732-4238-879F-582FEBEB6536", + "testMode": true + ]) + plugin.handle(call) { _ in } + } +} diff --git a/example/macos/Podfile b/example/macos/Podfile index c795730..b52666a 100644 --- a/example/macos/Podfile +++ b/example/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.14' +platform :osx, '10.15' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/example/macos/Podfile.lock b/example/macos/Podfile.lock index 7f7d477..99db6e1 100644 --- a/example/macos/Podfile.lock +++ b/example/macos/Podfile.lock @@ -1,9 +1,9 @@ PODS: - FlutterMacOS (1.0.0) - - TelemetryDeck (2.9.3) + - TelemetryDeck (2.11.0) - telemetrydecksdk (0.0.1): - FlutterMacOS - - TelemetryDeck (~> 2.9.3) + - TelemetryDeck (~> 2.11.0) DEPENDENCIES: - FlutterMacOS (from `Flutter/ephemeral`) @@ -20,10 +20,10 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/telemetrydecksdk/macos SPEC CHECKSUMS: - FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - TelemetryDeck: 5331ca64c53ab1ab6162c558c8b46b412c744483 - telemetrydecksdk: 93d7906aee5406574bc5a5dc4e6027d9bd429c43 + FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + TelemetryDeck: 50c6ddb1031ee91e7828d5f023af1419f9aadcad + telemetrydecksdk: 7c31fa86d7ef3f2dbfd4bba0165233de9c1c84f3 -PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 +PODFILE CHECKSUM: 9ebaf0ce3d369aaa26a9ea0e159195ed94724cf3 COCOAPODS: 1.16.2 diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj index ca11094..aa379cb 100644 --- a/example/macos/Runner.xcodeproj/project.pbxproj +++ b/example/macos/Runner.xcodeproj/project.pbxproj @@ -557,7 +557,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -639,7 +639,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -689,7 +689,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 4fee184..bc10ac2 100644 --- a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -60,6 +60,7 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> diff --git a/example/macos/RunnerTests/RunnerTests.swift b/example/macos/RunnerTests/RunnerTests.swift index 5b12224..ea2af40 100644 --- a/example/macos/RunnerTests/RunnerTests.swift +++ b/example/macos/RunnerTests/RunnerTests.swift @@ -1,27 +1,782 @@ import FlutterMacOS import Cocoa import XCTest +import TelemetryDeck @testable import telemetrydecksdk -// This demonstrates a simple unit test of the Swift portion of this plugin's implementation. -// -// See https://developer.apple.com/documentation/xctest for more information about using XCTest. - class RunnerTests: XCTestCase { + var plugin: TelemetrydecksdkPlugin! + + override func setUp() { + super.setUp() + plugin = TelemetrydecksdkPlugin() + } + + override func tearDown() { + plugin = nil + if TelemetryManager.isInitialized { + TelemetryDeck.terminate() + } + super.tearDown() + } + + func testStart_withRequiredAppID() { + let call = FlutterMethodCall(methodName: "start", arguments: [ + "appID": "32CB6574-6732-4238-879F-582FEBEB6536" + ]) + + let expectation = expectation(description: "start completes") + plugin.handle(call) { result in + XCTAssertNil(result) + XCTAssertTrue(TelemetryManager.isInitialized) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testStart_withMissingAppID_returnsError() { + let call = FlutterMethodCall(methodName: "start", arguments: [:]) + + let expectation = expectation(description: "start returns error") + plugin.handle(call) { result in + guard let error = result as? FlutterError else { + XCTFail("Expected FlutterError") + return + } + XCTAssertEqual(error.code, "INVALID_ARGUMENT") + XCTAssertEqual(error.message, "Expected value appID is not provided.") + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testStart_withOptionalParameters() { + let call = FlutterMethodCall(methodName: "start", arguments: [ + "appID": "32CB6574-6732-4238-879F-582FEBEB6536", + "apiBaseURL": "https://nom.telemetrydeck.com", + "salt": "customSalt", + "namespace": "testNamespace", + "defaultUser": "testUser", + "debug": true, + "testMode": true, + "defaultSignalPrefix": "Test.", + "defaultParameterPrefix": "Custom.", + "defaultParameters": ["key": "value"] + ]) + + let expectation = expectation(description: "start completes with options") + plugin.handle(call) { result in + XCTAssertNil(result) + XCTAssertTrue(TelemetryManager.isInitialized) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testStop() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "stop", arguments: nil) + + let expectation = expectation(description: "stop completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testSend_withRequiredSignalType() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "send", arguments: [ + "signalType": "Test.Event" + ]) + + let expectation = expectation(description: "send completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testSend_withAllParameters() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "send", arguments: [ + "signalType": "Test.Event", + "clientUser": "testUser123", + "additionalPayload": ["key1": "value1", "key2": "value2"], + "floatValue": 42.5 + ]) + + let expectation = expectation(description: "send completes with params") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testSend_withMissingSignalType_returnsError() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "send", arguments: [:]) + + let expectation = expectation(description: "send returns error") + plugin.handle(call) { result in + guard let error = result as? FlutterError else { + XCTFail("Expected FlutterError") + return + } + XCTAssertEqual(error.code, "INVALID_ARGUMENT") + XCTAssertEqual(error.message, "Missing required argument signalType") + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testGenerateNewSession() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "generateNewSession", arguments: nil) + + let expectation = expectation(description: "generateNewSession completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testUpdateDefaultUser() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "updateDefaultUser", arguments: "newUserID") + + let expectation = expectation(description: "updateDefaultUser completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testNavigate_withSourceAndDestination() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "navigate", arguments: [ + "sourcePath": "/home", + "destinationPath": "/profile" + ]) + + let expectation = expectation(description: "navigate completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testNavigate_withMissingParameters_returnsError() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "navigate", arguments: [ + "sourcePath": "/home" + ]) - func testGetPlatformVersion() { - let plugin = TelemetrydecksdkPlugin() + let expectation = expectation(description: "navigate returns error") + plugin.handle(call) { result in + guard let error = result as? FlutterError else { + XCTFail("Expected FlutterError") + return + } + XCTAssertEqual(error.code, "INVALID_ARGUMENT") + XCTAssertEqual(error.message, "sourcePath and destinationPath are required") + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testNavigateToDestination() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "navigateToDestination", arguments: [ + "destinationPath": "/settings" + ]) + + let expectation = expectation(description: "navigateToDestination completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testNavigateToDestination_withMissingDestination_returnsError() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "navigateToDestination", arguments: [:]) + + let expectation = expectation(description: "navigateToDestination returns error") + plugin.handle(call) { result in + guard let error = result as? FlutterError else { + XCTFail("Expected FlutterError") + return + } + XCTAssertEqual(error.code, "INVALID_ARGUMENT") + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testAcquiredUser_withChannel() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "acquiredUser", arguments: [ + "channel": "organic" + ]) + + let expectation = expectation(description: "acquiredUser completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testAcquiredUser_withAllParameters() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "acquiredUser", arguments: [ + "channel": "facebook", + "params": ["campaign": "summer2024"], + "customUserID": "user123" + ]) + + let expectation = expectation(description: "acquiredUser completes with params") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testAcquiredUser_withMissingChannel_returnsError() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "acquiredUser", arguments: [:]) + + let expectation = expectation(description: "acquiredUser returns error") + plugin.handle(call) { result in + guard let error = result as? FlutterError else { + XCTFail("Expected FlutterError") + return + } + XCTAssertEqual(error.code, "INVALID_ARGUMENT") + XCTAssertEqual(error.message, "channel is required") + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testLeadStarted() { + initializeSDK() - let call = FlutterMethodCall(methodName: "getPlatformVersion", arguments: []) + let call = FlutterMethodCall(methodName: "leadStarted", arguments: [ + "leadId": "lead_12345", + "params": ["source": "website"], + "customUserID": "user456" + ]) - let resultExpectation = expectation(description: "result block must be called.") - plugin.handle(call) { result in - XCTAssertEqual(result as! String, - "macOS " + ProcessInfo.processInfo.operatingSystemVersionString) - resultExpectation.fulfill() + let expectation = expectation(description: "leadStarted completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) } - waitForExpectations(timeout: 1) - } + func testLeadStarted_withMissingLeadId_returnsError() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "leadStarted", arguments: [:]) + + let expectation = expectation(description: "leadStarted returns error") + plugin.handle(call) { result in + guard let error = result as? FlutterError else { + XCTFail("Expected FlutterError") + return + } + XCTAssertEqual(error.code, "INVALID_ARGUMENT") + XCTAssertEqual(error.message, "leadId is required") + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testLeadConverted() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "leadConverted", arguments: [ + "leadId": "lead_12345", + "params": ["value": "100"], + "customUserID": "user456" + ]) + + let expectation = expectation(description: "leadConverted completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testLeadConverted_withMissingLeadId_returnsError() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "leadConverted", arguments: [:]) + + let expectation = expectation(description: "leadConverted returns error") + plugin.handle(call) { result in + guard let error = result as? FlutterError else { + XCTFail("Expected FlutterError") + return + } + XCTAssertEqual(error.code, "INVALID_ARGUMENT") + XCTAssertEqual(error.message, "leadId is required") + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testOnboardingCompleted() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "onboardingCompleted", arguments: [ + "params": ["steps": "5"], + "customUserID": "user789" + ]) + + let expectation = expectation(description: "onboardingCompleted completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testCoreFeatureUsed() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "coreFeatureUsed", arguments: [ + "featureName": "darkMode", + "params": ["enabled": "true"], + "customUserID": "user123" + ]) + + let expectation = expectation(description: "coreFeatureUsed completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testCoreFeatureUsed_withMissingFeatureName_returnsError() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "coreFeatureUsed", arguments: [:]) + + let expectation = expectation(description: "coreFeatureUsed returns error") + plugin.handle(call) { result in + guard let error = result as? FlutterError else { + XCTFail("Expected FlutterError") + return + } + XCTAssertEqual(error.code, "INVALID_ARGUMENT") + XCTAssertEqual(error.message, "featureName is required") + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testPaywallShown() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "paywallShown", arguments: [ + "reason": "featureAccess", + "params": ["feature": "premium"], + "customUserID": "user456" + ]) + + let expectation = expectation(description: "paywallShown completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testPaywallShown_withMissingReason_returnsError() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "paywallShown", arguments: [:]) + + let expectation = expectation(description: "paywallShown returns error") + plugin.handle(call) { result in + guard let error = result as? FlutterError else { + XCTFail("Expected FlutterError") + return + } + XCTAssertEqual(error.code, "INVALID_ARGUMENT") + XCTAssertEqual(error.message, "reason is required") + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testPurchaseCompleted_withDefaultEvent() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "purchaseCompleted", arguments: [ + "event": "completed", + "countryCode": "US", + "productID": "com.app.premium", + "purchaseType": "subscription", + "priceAmountMicros": 4990000, + "currencyCode": "USD" + ]) + + let expectation = expectation(description: "purchaseCompleted completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testPurchaseCompleted_withFreeTrialStarted() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "purchaseCompleted", arguments: [ + "event": "freeTrialStarted", + "countryCode": "DE", + "productID": "com.app.pro", + "purchaseType": "subscription", + "priceAmountMicros": 9990000, + "currencyCode": "EUR", + "offerID": "trial7days" + ]) + + let expectation = expectation(description: "purchaseCompleted with trial completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testPurchaseCompleted_withConvertedFromFreeTrial() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "purchaseCompleted", arguments: [ + "event": "convertedFromFreeTrial", + "countryCode": "UK", + "productID": "com.app.yearly", + "purchaseType": "subscription", + "priceAmountMicros": 49990000, + "currencyCode": "GBP" + ]) + + let expectation = expectation(description: "purchaseCompleted with conversion completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testPurchaseCompleted_withMissingParameters_returnsError() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "purchaseCompleted", arguments: [ + "event": "completed", + "countryCode": "US" + ]) + + let expectation = expectation(description: "purchaseCompleted returns error") + plugin.handle(call) { result in + guard let error = result as? FlutterError else { + XCTFail("Expected FlutterError") + return + } + XCTAssertEqual(error.code, "INVALID_ARGUMENT") + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testReferralSent_withDefaults() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "referralSent", arguments: [:]) + + let expectation = expectation(description: "referralSent completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testReferralSent_withAllParameters() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "referralSent", arguments: [ + "receiversCount": 3, + "kind": "email", + "params": ["campaign": "refer_friend"], + "customUserID": "user789" + ]) + + let expectation = expectation(description: "referralSent with params completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testUserRatingSubmitted() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "userRatingSubmitted", arguments: [ + "rating": 5, + "comment": "Great app!", + "params": ["platform": "macOS"], + "customUserID": "user123" + ]) + + let expectation = expectation(description: "userRatingSubmitted completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testUserRatingSubmitted_withMissingRating_returnsError() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "userRatingSubmitted", arguments: [:]) + + let expectation = expectation(description: "userRatingSubmitted returns error") + plugin.handle(call) { result in + guard let error = result as? FlutterError else { + XCTFail("Expected FlutterError") + return + } + XCTAssertEqual(error.code, "INVALID_ARGUMENT") + XCTAssertEqual(error.message, "rating is required") + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testErrorOccurred_withMinimalParameters() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "errorOccurred", arguments: [ + "id": "error_001" + ]) + + let expectation = expectation(description: "errorOccurred completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testErrorOccurred_withThrownExceptionCategory() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "errorOccurred", arguments: [ + "id": "error_002", + "category": "thrownException", + "message": "Null pointer exception", + "parameters": ["file": "main.dart"], + "floatValue": 1.0, + "customUserID": "user456" + ]) + + let expectation = expectation(description: "errorOccurred with category completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testErrorOccurred_withUserInputCategory() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "errorOccurred", arguments: [ + "id": "error_003", + "category": "userInput" + ]) + + let expectation = expectation(description: "errorOccurred with userInput completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testErrorOccurred_withAppStateCategory() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "errorOccurred", arguments: [ + "id": "error_004", + "category": "appState" + ]) + + let expectation = expectation(description: "errorOccurred with appState completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testErrorOccurred_withUnknownCategory() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "errorOccurred", arguments: [ + "id": "error_005", + "category": "unknownCategory" + ]) + + let expectation = expectation(description: "errorOccurred with unknown category completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testErrorOccurred_withMissingId_returnsError() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "errorOccurred", arguments: [:]) + + let expectation = expectation(description: "errorOccurred returns error") + plugin.handle(call) { result in + guard let error = result as? FlutterError else { + XCTFail("Expected FlutterError") + return + } + XCTAssertEqual(error.code, "INVALID_ARGUMENT") + XCTAssertEqual(error.message, "id is required") + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testStartDurationSignal() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "startDurationSignal", arguments: [ + "signalType": "Task.Duration", + "parameters": ["taskType": "download"] + ]) + + let expectation = expectation(description: "startDurationSignal completes") + plugin.handle(call) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testStartDurationSignal_withMissingSignalType_returnsError() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "startDurationSignal", arguments: [:]) + + let expectation = expectation(description: "startDurationSignal returns error") + plugin.handle(call) { result in + guard let error = result as? FlutterError else { + XCTFail("Expected FlutterError") + return + } + XCTAssertEqual(error.code, "INVALID_ARGUMENT") + XCTAssertEqual(error.message, "Missing required argument signalType") + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testStopAndSendDurationSignal() { + initializeSDK() + + let startCall = FlutterMethodCall(methodName: "startDurationSignal", arguments: [ + "signalType": "Task.Duration", + "parameters": ["taskType": "upload"] + ]) + plugin.handle(startCall) { _ in } + + let stopCall = FlutterMethodCall(methodName: "stopAndSendDurationSignal", arguments: [ + "signalType": "Task.Duration", + "parameters": ["taskType": "upload"] + ]) + + let expectation = expectation(description: "stopAndSendDurationSignal completes") + plugin.handle(stopCall) { result in + XCTAssertNil(result) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testStopAndSendDurationSignal_withMissingSignalType_returnsError() { + initializeSDK() + + let call = FlutterMethodCall(methodName: "stopAndSendDurationSignal", arguments: [:]) + + let expectation = expectation(description: "stopAndSendDurationSignal returns error") + plugin.handle(call) { result in + guard let error = result as? FlutterError else { + XCTFail("Expected FlutterError") + return + } + XCTAssertEqual(error.code, "INVALID_ARGUMENT") + XCTAssertEqual(error.message, "Missing required argument signalType") + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testUnknownMethod_returnsNotImplemented() { + let call = FlutterMethodCall(methodName: "unknownMethod", arguments: nil) + + let expectation = expectation(description: "unknown method returns not implemented") + plugin.handle(call) { result in + XCTAssertTrue((result as? NSObject) === FlutterMethodNotImplemented) + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + private func initializeSDK() { + let call = FlutterMethodCall(methodName: "start", arguments: [ + "appID": "32CB6574-6732-4238-879F-582FEBEB6536", + "testMode": true + ]) + plugin.handle(call) { _ in } + } } diff --git a/example/pubspec.lock b/example/pubspec.lock index e4b8010..18778df 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -5,42 +5,42 @@ 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" characters: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.19.0" + version: "1.19.1" cupertino_icons: dependency: "direct main" description: @@ -53,18 +53,18 @@ 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: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" flutter: dependency: "direct main" description: flutter @@ -79,10 +79,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "6.0.0" flutter_test: dependency: "direct dev" description: flutter @@ -102,42 +102,42 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.7" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: name: lints - sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "6.0.0" matcher: 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: @@ -150,26 +150,26 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.17.0" path: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" platform: dependency: transitive description: name: platform - sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -182,10 +182,10 @@ packages: dependency: transitive description: name: process - sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" + sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744 url: "https://pub.dev" source: hosted - version: "5.0.2" + version: "5.0.5" sky_engine: dependency: transitive description: flutter @@ -195,34 +195,34 @@ 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: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.12.0" + 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" string_scanner: dependency: transitive description: name: string_scanner - sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.1" sync_http: dependency: transitive description: @@ -237,47 +237,47 @@ packages: path: ".." relative: true source: path - version: "2.3.0" + version: "2.5.0" 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: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.7" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: name: vm_service - sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" url: "https://pub.dev" source: hosted - version: "14.3.0" + version: "15.0.2" webdriver: dependency: transitive description: name: webdriver - sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8" + sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" url: "https://pub.dev" source: hosted - version: "3.0.4" + version: "3.1.0" sdks: - dart: ">=3.4.0 <4.0.0" + dart: ">=3.8.0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 363ca1b..d27acc3 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -2,10 +2,10 @@ name: telemetrydecksdk_example description: "Demonstrates how to use the telemetrydecksdk plugin." # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev +publish_to: "none" # Remove this line if you wish to publish to pub.dev environment: - sdk: '>=3.2.3 <4.0.0' + sdk: ">=3.2.3 <4.0.0" # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions @@ -40,14 +40,13 @@ dev_dependencies: # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. - flutter_lints: ^2.0.0 + flutter_lints: ^6.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter packages. flutter: - # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. diff --git a/ios/Classes/TelemetrydecksdkPlugin.swift b/ios/Classes/TelemetrydecksdkPlugin.swift index f6c754d..045a88d 100644 --- a/ios/Classes/TelemetrydecksdkPlugin.swift +++ b/ios/Classes/TelemetrydecksdkPlugin.swift @@ -30,6 +30,26 @@ public class TelemetrydecksdkPlugin: NSObject, FlutterPlugin { nativeNavigate(call, result: result) case "navigateToDestination": nativeNavigateDestination(call, result: result) + case "acquiredUser": + nativeAcquiredUser(call, result: result) + case "leadStarted": + nativeLeadStarted(call, result: result) + case "leadConverted": + nativeLeadConverted(call, result: result) + case "onboardingCompleted": + nativeOnboardingCompleted(call, result: result) + case "coreFeatureUsed": + nativeCoreFeatureUsed(call, result: result) + case "paywallShown": + nativePaywallShown(call, result: result) + case "purchaseCompleted": + nativePurchaseCompleted(call, result: result) + case "referralSent": + nativeReferralSent(call, result: result) + case "userRatingSubmitted": + nativeUserRatingSubmitted(call, result: result) + case "errorOccurred": + nativeErrorOccurred(call, result: result) default: result(FlutterMethodNotImplemented) } @@ -87,15 +107,16 @@ public class TelemetrydecksdkPlugin: NSObject, FlutterPlugin { result(FlutterError(code: "INVALID_ARGUMENT", message: "Missing required argument signalType", details: nil)) return } - + let clientUser = arguments["clientUser"] as? String let additionalPayload = arguments["additionalPayload"] as? [String : String] ?? [:] - + let floatValue = arguments["floatValue"] as? Double + // do not attempt to send signals if the client is stopped if TelemetryManager.isInitialized { - TelemetryDeck.signal(signalType, parameters: additionalPayload, customUserID: clientUser) + TelemetryDeck.signal(signalType, parameters: additionalPayload, floatValue: floatValue, customUserID: clientUser) } - + result(nil) } @@ -191,7 +212,222 @@ public class TelemetrydecksdkPlugin: NSObject, FlutterPlugin { } TelemetryDeck.initialize(config: configuration) - + result(nil) } + + private func nativeAcquiredUser(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let arguments = call.arguments as? [String: Any], + let channel = arguments["channel"] as? String else { + result(FlutterError(code: "INVALID_ARGUMENT", message: "channel is required", details: nil)) + return + } + let params = arguments["params"] as? [String: String] ?? [:] + let customUserID = arguments["customUserID"] as? String + DispatchQueue.main.async { + TelemetryDeck.acquiredUser(channel: channel, parameters: params, customUserID: customUserID) + result(nil) + } + } + + private func nativeLeadStarted(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let arguments = call.arguments as? [String: Any], + let leadId = arguments["leadId"] as? String else { + result(FlutterError(code: "INVALID_ARGUMENT", message: "leadId is required", details: nil)) + return + } + let params = arguments["params"] as? [String: String] ?? [:] + let customUserID = arguments["customUserID"] as? String + DispatchQueue.main.async { + TelemetryDeck.leadStarted(leadID: leadId, parameters: params, customUserID: customUserID) + result(nil) + } + } + + private func nativeLeadConverted(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let arguments = call.arguments as? [String: Any], + let leadId = arguments["leadId"] as? String else { + result(FlutterError(code: "INVALID_ARGUMENT", message: "leadId is required", details: nil)) + return + } + let params = arguments["params"] as? [String: String] ?? [:] + let customUserID = arguments["customUserID"] as? String + DispatchQueue.main.async { + TelemetryDeck.leadConverted(leadID: leadId, parameters: params, customUserID: customUserID) + result(nil) + } + } + + private func nativeOnboardingCompleted(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let arguments = call.arguments as? [String: Any] else { + result(FlutterError(code: "INVALID_ARGUMENT", message: "Arguments are not a map", details: nil)) + return + } + let params = arguments["params"] as? [String: String] ?? [:] + let customUserID = arguments["customUserID"] as? String + DispatchQueue.main.async { + TelemetryDeck.onboardingCompleted(parameters: params, customUserID: customUserID) + result(nil) + } + } + + private func nativeCoreFeatureUsed(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let arguments = call.arguments as? [String: Any], + let featureName = arguments["featureName"] as? String else { + result(FlutterError(code: "INVALID_ARGUMENT", message: "featureName is required", details: nil)) + return + } + let params = arguments["params"] as? [String: String] ?? [:] + let customUserID = arguments["customUserID"] as? String + DispatchQueue.main.async { + TelemetryDeck.coreFeatureUsed(featureName: featureName, parameters: params, customUserID: customUserID) + result(nil) + } + } + + private func nativePaywallShown(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let arguments = call.arguments as? [String: Any], + let reason = arguments["reason"] as? String else { + result(FlutterError(code: "INVALID_ARGUMENT", message: "reason is required", details: nil)) + return + } + let params = arguments["params"] as? [String: String] ?? [:] + let customUserID = arguments["customUserID"] as? String + DispatchQueue.main.async { + TelemetryDeck.paywallShown(reason: reason, parameters: params, customUserID: customUserID) + result(nil) + } + } + + private func nativePurchaseCompleted(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let arguments = call.arguments as? [String: Any], + let eventString = arguments["event"] as? String, + let countryCode = arguments["countryCode"] as? String, + let productID = arguments["productID"] as? String, + let purchaseTypeString = arguments["purchaseType"] as? String, + let priceAmountMicros = arguments["priceAmountMicros"] as? Int, + let currencyCode = arguments["currencyCode"] as? String else { + result(FlutterError(code: "INVALID_ARGUMENT", message: "event, countryCode, productID, purchaseType, priceAmountMicros, and currencyCode are required", details: nil)) + return + } + + let offerID = arguments["offerID"] as? String + let params = arguments["params"] as? [String: String] ?? [:] + let customUserID = arguments["customUserID"] as? String + + let signalName: String + switch eventString { + case "freeTrialStarted": + signalName = "TelemetryDeck.Purchase.freeTrialStarted" + case "convertedFromFreeTrial": + signalName = "TelemetryDeck.Purchase.convertedFromFreeTrial" + default: + signalName = "TelemetryDeck.Purchase.completed" + } + + let type: String + switch purchaseTypeString { + case "subscription": + type = "subscription" + default: + type = "one-time-purchase" + } + + var purchaseParams: [String: String] = [ + "TelemetryDeck.Purchase.type": type, + "TelemetryDeck.Purchase.countryCode": countryCode, + "TelemetryDeck.Purchase.productID": productID, + "TelemetryDeck.Purchase.currencyCode": currencyCode + ] + + if let offerID = offerID { + purchaseParams["TelemetryDeck.Purchase.offerID"] = offerID + } + + let mergedParams = purchaseParams.merging(params) { $1 } + + let priceInUSD = Double(priceAmountMicros) / 1_000_000.0 + + DispatchQueue.main.async { + TelemetryDeck.signal( + signalName, + parameters: mergedParams, + floatValue: priceInUSD, + customUserID: customUserID + ) + result(nil) + } + } + + private func nativeReferralSent(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let arguments = call.arguments as? [String: Any] else { + result(FlutterError(code: "INVALID_ARGUMENT", message: "Arguments are not a map", details: nil)) + return + } + let receiversCount = arguments["receiversCount"] as? Int ?? 1 + let kind = arguments["kind"] as? String + let params = arguments["params"] as? [String: String] ?? [:] + let customUserID = arguments["customUserID"] as? String + DispatchQueue.main.async { + TelemetryDeck.referralSent(receiversCount: receiversCount, kind: kind, parameters: params, customUserID: customUserID) + result(nil) + } + } + + private func nativeUserRatingSubmitted(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let arguments = call.arguments as? [String: Any], + let rating = arguments["rating"] as? Int else { + result(FlutterError(code: "INVALID_ARGUMENT", message: "rating is required", details: nil)) + return + } + let comment = arguments["comment"] as? String + let params = arguments["params"] as? [String: String] ?? [:] + let customUserID = arguments["customUserID"] as? String + DispatchQueue.main.async { + TelemetryDeck.userRatingSubmitted(rating: rating, comment: comment, parameters: params, customUserID: customUserID) + result(nil) + } + } + + private func nativeErrorOccurred(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let arguments = call.arguments as? [String: Any], + let id = arguments["id"] as? String else { + result(FlutterError(code: "INVALID_ARGUMENT", message: "id is required", details: nil)) + return + } + + let categoryString = arguments["category"] as? String + let category: ErrorCategory? + if let categoryString = categoryString { + switch categoryString { + case "thrownException": + category = .thrownException + case "userInput": + category = .userInput + case "appState": + category = .appState + default: + category = nil + } + } else { + category = nil + } + + let message = arguments["message"] as? String + let parameters = arguments["parameters"] as? [String: String] ?? [:] + let floatValue = arguments["floatValue"] as? Double + let customUserID = arguments["customUserID"] as? String + + DispatchQueue.main.async { + TelemetryDeck.errorOccurred( + id: id, + category: category, + message: message, + parameters: parameters, + floatValue: floatValue, + customUserID: customUserID + ) + result(nil) + } + } } diff --git a/ios/telemetrydecksdk.podspec b/ios/telemetrydecksdk.podspec index 2038b31..b86b60e 100644 --- a/ios/telemetrydecksdk.podspec +++ b/ios/telemetrydecksdk.podspec @@ -15,7 +15,7 @@ Flutter SDK for TelemetryDeck, a privacy-conscious analytics service for apps an s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.dependency 'Flutter' - s.dependency 'TelemetryDeck', '~> 2.9.3' + s.dependency 'TelemetryDeck', '~> 2.11.0' s.platform = :ios, '12.0' # Flutter.framework does not contain a i386 slice. diff --git a/lib/src/error_category.dart b/lib/src/error_category.dart new file mode 100644 index 0000000..18f5a26 --- /dev/null +++ b/lib/src/error_category.dart @@ -0,0 +1,7 @@ +enum ErrorCategory { + thrownException, + userInput, + appState, + integration, + unknown, +} diff --git a/lib/src/purchase_event.dart b/lib/src/purchase_event.dart new file mode 100644 index 0000000..2751db8 --- /dev/null +++ b/lib/src/purchase_event.dart @@ -0,0 +1,5 @@ +enum PurchaseEvent { + purchaseCompleted, + freeTrialStarted, + convertedFromFreeTrial, +} diff --git a/lib/src/purchase_type.dart b/lib/src/purchase_type.dart new file mode 100644 index 0000000..8b11a8d --- /dev/null +++ b/lib/src/purchase_type.dart @@ -0,0 +1,4 @@ +enum PurchaseType { + subscription, + oneTimePurchase, +} diff --git a/lib/src/telemetrydecksdk.dart b/lib/src/telemetrydecksdk.dart index 85df633..e06060a 100644 --- a/lib/src/telemetrydecksdk.dart +++ b/lib/src/telemetrydecksdk.dart @@ -1,5 +1,8 @@ import 'dart:async'; +import 'package:telemetrydecksdk/src/error_category.dart'; +import 'package:telemetrydecksdk/src/purchase_event.dart'; +import 'package:telemetrydecksdk/src/purchase_type.dart'; import 'package:telemetrydecksdk/src/telemetry_manager_configuration.dart'; import 'package:telemetrydecksdk/src/telemetry_provider.dart'; @@ -31,6 +34,7 @@ abstract class Telemetrydecksdk { String signalType, { String? clientUser, Map? additionalPayload, + double? floatValue, }) async { final payload = additionalPayload ?? {}; @@ -42,6 +46,7 @@ abstract class Telemetrydecksdk { signalType, clientUser: clientUser, additionalPayload: stringifiedPayload, + floatValue: floatValue, ); } @@ -96,4 +101,96 @@ abstract class Telemetrydecksdk { await TelemetrydecksdkPlatform.instance .navigateToDestination(destinationPath, clientUser: clientUser); } + + static Future acquiredUser(String channel, + {Map? params, String? customUserID}) async { + await TelemetrydecksdkPlatform.instance + .acquiredUser(channel, params: params, customUserID: customUserID); + } + + static Future leadStarted(String leadId, + {Map? params, String? customUserID}) async { + await TelemetrydecksdkPlatform.instance + .leadStarted(leadId, params: params, customUserID: customUserID); + } + + static Future leadConverted(String leadId, + {Map? params, String? customUserID}) async { + await TelemetrydecksdkPlatform.instance + .leadConverted(leadId, params: params, customUserID: customUserID); + } + + static Future onboardingCompleted( + {Map? params, String? customUserID}) async { + await TelemetrydecksdkPlatform.instance + .onboardingCompleted(params: params, customUserID: customUserID); + } + + static Future coreFeatureUsed(String featureName, + {Map? params, String? customUserID}) async { + await TelemetrydecksdkPlatform.instance.coreFeatureUsed(featureName, + params: params, customUserID: customUserID); + } + + static Future paywallShown(String reason, + {Map? params, String? customUserID}) async { + await TelemetrydecksdkPlatform.instance + .paywallShown(reason, params: params, customUserID: customUserID); + } + + static Future purchaseCompleted( + PurchaseEvent event, + String countryCode, + String productID, + PurchaseType purchaseType, + int priceAmountMicros, + String currencyCode, + {String? offerID, + Map? params, + String? customUserID}) async { + await TelemetrydecksdkPlatform.instance.purchaseCompleted( + event.name, + countryCode, + productID, + purchaseType.name, + priceAmountMicros, + currencyCode, + offerID: offerID, + params: params, + customUserID: customUserID); + } + + static Future referralSent( + {int receiversCount = 1, + String? kind, + Map? params, + String? customUserID}) async { + await TelemetrydecksdkPlatform.instance.referralSent( + receiversCount: receiversCount, + kind: kind, + params: params, + customUserID: customUserID); + } + + static Future userRatingSubmitted(int rating, + {String? comment, + Map? params, + String? customUserID}) async { + await TelemetrydecksdkPlatform.instance.userRatingSubmitted(rating, + comment: comment, params: params, customUserID: customUserID); + } + + static Future errorOccurred(String id, + {ErrorCategory? category, + String? message, + Map? parameters, + double? floatValue, + String? customUserID}) async { + await TelemetrydecksdkPlatform.instance.errorOccurred(id, + category: category?.name, + message: message, + parameters: parameters, + floatValue: floatValue, + customUserID: customUserID); + } } diff --git a/lib/src/telemetrydecksdk_method_channel.dart b/lib/src/telemetrydecksdk_method_channel.dart index c4f3ffb..5c3a6ee 100644 --- a/lib/src/telemetrydecksdk_method_channel.dart +++ b/lib/src/telemetrydecksdk_method_channel.dart @@ -25,6 +25,7 @@ class MethodChannelTelemetrydecksdk extends TelemetrydecksdkPlatform { String signalType, { String? clientUser, Map? additionalPayload, + double? floatValue, }) async { await methodChannel.invokeMethod( 'send', @@ -32,6 +33,7 @@ class MethodChannelTelemetrydecksdk extends TelemetrydecksdkPlatform { 'signalType': signalType, 'clientUser': clientUser, 'additionalPayload': additionalPayload, + 'floatValue': floatValue, }, ); } @@ -92,4 +94,131 @@ class MethodChannelTelemetrydecksdk extends TelemetrydecksdkPlatform { 'clientUser': clientUser, }); } + + @override + Future acquiredUser(String channel, + {Map? params, String? customUserID}) async { + await methodChannel.invokeMethod('acquiredUser', { + 'channel': channel, + 'params': params, + 'customUserID': customUserID, + }); + } + + @override + Future leadStarted(String leadId, + {Map? params, String? customUserID}) async { + await methodChannel.invokeMethod('leadStarted', { + 'leadId': leadId, + 'params': params, + 'customUserID': customUserID, + }); + } + + @override + Future leadConverted(String leadId, + {Map? params, String? customUserID}) async { + await methodChannel.invokeMethod('leadConverted', { + 'leadId': leadId, + 'params': params, + 'customUserID': customUserID, + }); + } + + @override + Future onboardingCompleted( + {Map? params, String? customUserID}) async { + await methodChannel.invokeMethod('onboardingCompleted', { + 'params': params, + 'customUserID': customUserID, + }); + } + + @override + Future coreFeatureUsed(String featureName, + {Map? params, String? customUserID}) async { + await methodChannel.invokeMethod('coreFeatureUsed', { + 'featureName': featureName, + 'params': params, + 'customUserID': customUserID, + }); + } + + @override + Future paywallShown(String reason, + {Map? params, String? customUserID}) async { + await methodChannel.invokeMethod('paywallShown', { + 'reason': reason, + 'params': params, + 'customUserID': customUserID, + }); + } + + @override + Future purchaseCompleted( + String event, + String countryCode, + String productID, + String purchaseType, + int priceAmountMicros, + String currencyCode, + {String? offerID, + Map? params, + String? customUserID}) async { + await methodChannel.invokeMethod('purchaseCompleted', { + 'event': event, + 'countryCode': countryCode, + 'productID': productID, + 'purchaseType': purchaseType, + 'priceAmountMicros': priceAmountMicros, + 'currencyCode': currencyCode, + 'offerID': offerID, + 'params': params, + 'customUserID': customUserID, + }); + } + + @override + Future referralSent( + {int receiversCount = 1, + String? kind, + Map? params, + String? customUserID}) async { + await methodChannel.invokeMethod('referralSent', { + 'receiversCount': receiversCount, + 'kind': kind, + 'params': params, + 'customUserID': customUserID, + }); + } + + @override + Future userRatingSubmitted(int rating, + {String? comment, + Map? params, + String? customUserID}) async { + await methodChannel.invokeMethod('userRatingSubmitted', { + 'rating': rating, + 'comment': comment, + 'params': params, + 'customUserID': customUserID, + }); + } + + @override + Future errorOccurred(String id, + {String? category, + String? message, + Map? parameters, + double? floatValue, + String? customUserID}) async { + await methodChannel.invokeMethod('errorOccurred', { + 'id': id, + 'category': category, + 'message': message, + 'parameters': parameters, + 'floatValue': floatValue, + 'customUserID': customUserID, + }); + } } diff --git a/lib/src/telemetrydecksdk_platform_interface.dart b/lib/src/telemetrydecksdk_platform_interface.dart index f9b713b..33c936c 100644 --- a/lib/src/telemetrydecksdk_platform_interface.dart +++ b/lib/src/telemetrydecksdk_platform_interface.dart @@ -36,6 +36,7 @@ abstract class TelemetrydecksdkPlatform extends PlatformInterface { String signalType, { String? clientUser, Map? additionalPayload, + double? floatValue, }) async { throw UnimplementedError('send() has not been implemented.'); } @@ -73,4 +74,71 @@ abstract class TelemetrydecksdkPlatform extends PlatformInterface { throw UnimplementedError( 'navigateToDestination() has not been implemented.'); } + + Future acquiredUser(String channel, + {Map? params, String? customUserID}) async { + throw UnimplementedError('acquiredUser() has not been implemented.'); + } + + Future leadStarted(String leadId, + {Map? params, String? customUserID}) async { + throw UnimplementedError('leadStarted() has not been implemented.'); + } + + Future leadConverted(String leadId, + {Map? params, String? customUserID}) async { + throw UnimplementedError('leadConverted() has not been implemented.'); + } + + Future onboardingCompleted( + {Map? params, String? customUserID}) async { + throw UnimplementedError('onboardingCompleted() has not been implemented.'); + } + + Future coreFeatureUsed(String featureName, + {Map? params, String? customUserID}) async { + throw UnimplementedError('coreFeatureUsed() has not been implemented.'); + } + + Future paywallShown(String reason, + {Map? params, String? customUserID}) async { + throw UnimplementedError('paywallShown() has not been implemented.'); + } + + Future purchaseCompleted( + String event, + String countryCode, + String productID, + String purchaseType, + int priceAmountMicros, + String currencyCode, + {String? offerID, + Map? params, + String? customUserID}) async { + throw UnimplementedError('purchaseCompleted() has not been implemented.'); + } + + Future referralSent( + {int receiversCount = 1, + String? kind, + Map? params, + String? customUserID}) async { + throw UnimplementedError('referralSent() has not been implemented.'); + } + + Future userRatingSubmitted(int rating, + {String? comment, + Map? params, + String? customUserID}) async { + throw UnimplementedError('userRatingSubmitted() has not been implemented.'); + } + + Future errorOccurred(String id, + {String? category, + String? message, + Map? parameters, + double? floatValue, + String? customUserID}) async { + throw UnimplementedError('errorOccurred() has not been implemented.'); + } } diff --git a/lib/telemetrydecksdk.dart b/lib/telemetrydecksdk.dart index 5ad432c..b62ba82 100644 --- a/lib/telemetrydecksdk.dart +++ b/lib/telemetrydecksdk.dart @@ -1,4 +1,7 @@ library; +export 'src/error_category.dart'; +export 'src/purchase_event.dart'; +export 'src/purchase_type.dart'; export 'src/telemetry_manager_configuration.dart'; export 'src/telemetrydecksdk.dart'; diff --git a/macos/Classes/TelemetrydecksdkPlugin.swift b/macos/Classes/TelemetrydecksdkPlugin.swift index 1e49e22..d12c49f 100644 --- a/macos/Classes/TelemetrydecksdkPlugin.swift +++ b/macos/Classes/TelemetrydecksdkPlugin.swift @@ -31,6 +31,26 @@ public class TelemetrydecksdkPlugin: NSObject, FlutterPlugin { nativeNavigate(call, result: result) case "navigateToDestination": nativeNavigateDestination(call, result: result) + case "acquiredUser": + nativeAcquiredUser(call, result: result) + case "leadStarted": + nativeLeadStarted(call, result: result) + case "leadConverted": + nativeLeadConverted(call, result: result) + case "onboardingCompleted": + nativeOnboardingCompleted(call, result: result) + case "coreFeatureUsed": + nativeCoreFeatureUsed(call, result: result) + case "paywallShown": + nativePaywallShown(call, result: result) + case "purchaseCompleted": + nativePurchaseCompleted(call, result: result) + case "referralSent": + nativeReferralSent(call, result: result) + case "userRatingSubmitted": + nativeUserRatingSubmitted(call, result: result) + case "errorOccurred": + nativeErrorOccurred(call, result: result) default: result(FlutterMethodNotImplemented) } @@ -54,6 +74,221 @@ public class TelemetrydecksdkPlugin: NSObject, FlutterPlugin { result(nil) } } + + private func nativeAcquiredUser(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let arguments = call.arguments as? [String: Any], + let channel = arguments["channel"] as? String else { + result(FlutterError(code: "INVALID_ARGUMENT", message: "channel is required", details: nil)) + return + } + let params = arguments["params"] as? [String: String] ?? [:] + let customUserID = arguments["customUserID"] as? String + DispatchQueue.main.async { + TelemetryDeck.acquiredUser(channel: channel, parameters: params, customUserID: customUserID) + result(nil) + } + } + + private func nativeLeadStarted(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let arguments = call.arguments as? [String: Any], + let leadId = arguments["leadId"] as? String else { + result(FlutterError(code: "INVALID_ARGUMENT", message: "leadId is required", details: nil)) + return + } + let params = arguments["params"] as? [String: String] ?? [:] + let customUserID = arguments["customUserID"] as? String + DispatchQueue.main.async { + TelemetryDeck.leadStarted(leadID: leadId, parameters: params, customUserID: customUserID) + result(nil) + } + } + + private func nativeLeadConverted(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let arguments = call.arguments as? [String: Any], + let leadId = arguments["leadId"] as? String else { + result(FlutterError(code: "INVALID_ARGUMENT", message: "leadId is required", details: nil)) + return + } + let params = arguments["params"] as? [String: String] ?? [:] + let customUserID = arguments["customUserID"] as? String + DispatchQueue.main.async { + TelemetryDeck.leadConverted(leadID: leadId, parameters: params, customUserID: customUserID) + result(nil) + } + } + + private func nativeOnboardingCompleted(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let arguments = call.arguments as? [String: Any] else { + result(FlutterError(code: "INVALID_ARGUMENT", message: "Arguments are not a map", details: nil)) + return + } + let params = arguments["params"] as? [String: String] ?? [:] + let customUserID = arguments["customUserID"] as? String + DispatchQueue.main.async { + TelemetryDeck.onboardingCompleted(parameters: params, customUserID: customUserID) + result(nil) + } + } + + private func nativeCoreFeatureUsed(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let arguments = call.arguments as? [String: Any], + let featureName = arguments["featureName"] as? String else { + result(FlutterError(code: "INVALID_ARGUMENT", message: "featureName is required", details: nil)) + return + } + let params = arguments["params"] as? [String: String] ?? [:] + let customUserID = arguments["customUserID"] as? String + DispatchQueue.main.async { + TelemetryDeck.coreFeatureUsed(featureName: featureName, parameters: params, customUserID: customUserID) + result(nil) + } + } + + private func nativePaywallShown(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let arguments = call.arguments as? [String: Any], + let reason = arguments["reason"] as? String else { + result(FlutterError(code: "INVALID_ARGUMENT", message: "reason is required", details: nil)) + return + } + let params = arguments["params"] as? [String: String] ?? [:] + let customUserID = arguments["customUserID"] as? String + DispatchQueue.main.async { + TelemetryDeck.paywallShown(reason: reason, parameters: params, customUserID: customUserID) + result(nil) + } + } + + private func nativePurchaseCompleted(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let arguments = call.arguments as? [String: Any], + let eventString = arguments["event"] as? String, + let countryCode = arguments["countryCode"] as? String, + let productID = arguments["productID"] as? String, + let purchaseTypeString = arguments["purchaseType"] as? String, + let priceAmountMicros = arguments["priceAmountMicros"] as? Int, + let currencyCode = arguments["currencyCode"] as? String else { + result(FlutterError(code: "INVALID_ARGUMENT", message: "event, countryCode, productID, purchaseType, priceAmountMicros, and currencyCode are required", details: nil)) + return + } + + let offerID = arguments["offerID"] as? String + let params = arguments["params"] as? [String: String] ?? [:] + let customUserID = arguments["customUserID"] as? String + + let signalName: String + switch eventString { + case "freeTrialStarted": + signalName = "TelemetryDeck.Purchase.freeTrialStarted" + case "convertedFromFreeTrial": + signalName = "TelemetryDeck.Purchase.convertedFromFreeTrial" + default: + signalName = "TelemetryDeck.Purchase.completed" + } + + let type: String + switch purchaseTypeString { + case "subscription": + type = "subscription" + default: + type = "one-time-purchase" + } + + var purchaseParams: [String: String] = [ + "TelemetryDeck.Purchase.type": type, + "TelemetryDeck.Purchase.countryCode": countryCode, + "TelemetryDeck.Purchase.productID": productID, + "TelemetryDeck.Purchase.currencyCode": currencyCode + ] + + if let offerID = offerID { + purchaseParams["TelemetryDeck.Purchase.offerID"] = offerID + } + + let mergedParams = purchaseParams.merging(params) { $1 } + + let priceInUSD = Double(priceAmountMicros) / 1_000_000.0 + + DispatchQueue.main.async { + TelemetryDeck.signal( + signalName, + parameters: mergedParams, + floatValue: priceInUSD, + customUserID: customUserID + ) + result(nil) + } + } + + private func nativeReferralSent(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let arguments = call.arguments as? [String: Any] else { + result(FlutterError(code: "INVALID_ARGUMENT", message: "Arguments are not a map", details: nil)) + return + } + let receiversCount = arguments["receiversCount"] as? Int ?? 1 + let kind = arguments["kind"] as? String + let params = arguments["params"] as? [String: String] ?? [:] + let customUserID = arguments["customUserID"] as? String + DispatchQueue.main.async { + TelemetryDeck.referralSent(receiversCount: receiversCount, kind: kind, parameters: params, customUserID: customUserID) + result(nil) + } + } + + private func nativeUserRatingSubmitted(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let arguments = call.arguments as? [String: Any], + let rating = arguments["rating"] as? Int else { + result(FlutterError(code: "INVALID_ARGUMENT", message: "rating is required", details: nil)) + return + } + let comment = arguments["comment"] as? String + let params = arguments["params"] as? [String: String] ?? [:] + let customUserID = arguments["customUserID"] as? String + DispatchQueue.main.async { + TelemetryDeck.userRatingSubmitted(rating: rating, comment: comment, parameters: params, customUserID: customUserID) + result(nil) + } + } + + private func nativeErrorOccurred(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let arguments = call.arguments as? [String: Any], + let id = arguments["id"] as? String else { + result(FlutterError(code: "INVALID_ARGUMENT", message: "id is required", details: nil)) + return + } + + let categoryString = arguments["category"] as? String + let category: ErrorCategory? + if let categoryString = categoryString { + switch categoryString { + case "thrownException": + category = .thrownException + case "userInput": + category = .userInput + case "appState": + category = .appState + default: + category = nil + } + } else { + category = nil + } + + let message = arguments["message"] as? String + let parameters = arguments["parameters"] as? [String: String] ?? [:] + let floatValue = arguments["floatValue"] as? Double + let customUserID = arguments["customUserID"] as? String + + DispatchQueue.main.async { + TelemetryDeck.errorOccurred( + id: id, + category: category, + message: message, + parameters: parameters, + floatValue: floatValue, + customUserID: customUserID + ) + result(nil) + } + } } /** @@ -89,15 +324,16 @@ private func nativeQueue(_ call: FlutterMethodCall, result: @escaping FlutterRes result(FlutterError(code: "INVALID_ARGUMENT", message: "Missing required argument signalType", details: nil)) return } - + let clientUser = arguments["clientUser"] as? String let additionalPayload = arguments["additionalPayload"] as? [String : String] ?? [:] - + let floatValue = arguments["floatValue"] as? Double + // do not attempt to send signals if the client is stopped if TelemetryManager.isInitialized { - TelemetryDeck.signal(signalType, parameters: additionalPayload, customUserID: clientUser) + TelemetryDeck.signal(signalType, parameters: additionalPayload, floatValue: floatValue, customUserID: clientUser) } - + result(nil) } diff --git a/macos/telemetrydecksdk.podspec b/macos/telemetrydecksdk.podspec index 8450b7d..d1d72b5 100644 --- a/macos/telemetrydecksdk.podspec +++ b/macos/telemetrydecksdk.podspec @@ -16,7 +16,7 @@ Flutter SDK for TelemetryDeck, a privacy-conscious analytics service for apps an s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.dependency 'FlutterMacOS' - s.dependency 'TelemetryDeck', '~> 2.9.3' + s.dependency 'TelemetryDeck', '~> 2.11.0' s.platform = :osx, '10.11' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } diff --git a/pubspec.yaml b/pubspec.yaml index eef7412..d3d0820 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,7 +18,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^5.0.0 + flutter_lints: ^6.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/test/telemetrydecksdk_test.dart b/test/telemetrydecksdk_test.dart index af6f448..e7c4eaa 100644 --- a/test/telemetrydecksdk_test.dart +++ b/test/telemetrydecksdk_test.dart @@ -25,9 +25,20 @@ class MockTelemetrydecksdkPlatform String signalType, { String? clientUser, Map? additionalPayload, + double? floatValue, }) async => (); + @override + Future startDurationSignal(String signalType, + {Map? parameters}) async => + (); + + @override + Future stopAndSendDurationSignal(String signalType, + {Map? parameters}) async => + (); + @override Future navigate(String sourcePath, String destinationPath, {String? clientUser}) async => @@ -37,6 +48,73 @@ class MockTelemetrydecksdkPlatform Future navigateToDestination(String destinationPath, {String? clientUser}) async => (); + + @override + Future acquiredUser(String channel, + {Map? params, String? customUserID}) async => + (); + + @override + Future leadStarted(String leadId, + {Map? params, String? customUserID}) async => + (); + + @override + Future leadConverted(String leadId, + {Map? params, String? customUserID}) async => + (); + + @override + Future onboardingCompleted( + {Map? params, String? customUserID}) async => + (); + + @override + Future coreFeatureUsed(String featureName, + {Map? params, String? customUserID}) async => + (); + + @override + Future paywallShown(String reason, + {Map? params, String? customUserID}) async => + (); + + @override + Future purchaseCompleted( + String event, + String countryCode, + String productID, + String purchaseType, + int priceAmountMicros, + String currencyCode, + {String? offerID, + Map? params, + String? customUserID}) async => + (); + + @override + Future referralSent( + {int receiversCount = 1, + String? kind, + Map? params, + String? customUserID}) async => + (); + + @override + Future userRatingSubmitted(int rating, + {String? comment, + Map? params, + String? customUserID}) async => + (); + + @override + Future errorOccurred(String id, + {String? category, + String? message, + Map? parameters, + double? floatValue, + String? customUserID}) async => + (); } void main() {