diff --git a/CHANGELOG.md b/CHANGELOG.md index e5e398b..3431240 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## Next - chore: change host to new address ([#106](https://github.com/PostHog/posthog-flutter/pull/106)) +- chore: allow manual initialization of the SDK ([#117](https://github.com/PostHog/posthog-flutter/pull/117)) ## 4.5.0 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ca99bbd --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +.PHONY: formatKotlin formatSwift formatDart + +# brew install ktlint +# TODO: add ktlint steps in CI +formatKotlin: + ktlint --format + +# brew install swiftlint +# TODO: add swiftlint steps in CI +formatSwift: + swiftformat ios/Classes --swiftversion 5.3 + swiftlint ios/Classes --fix + +formatDart: + dart format . + dart analyze . diff --git a/README.md b/README.md index c4e71c4..434951c 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ To use this plugin, add `posthog_flutter` as a [dependency in your pubspec.yaml | Method | Android | iOS/macOS | Web | | ------------------------- | ------- | --------- | --- | +| `setup` | X | X | | | `identify` | X | X | X | | `capture` | X | X | X | | `screen` | X | X | X | @@ -29,6 +30,7 @@ To use this plugin, add `posthog_flutter` as a [dependency in your pubspec.yaml | `getFeatureFlag` | X | X | X | | `getFeatureFlagPayload` | X | X | X | | `group` | X | X | X | +| `close` | X | X | | ### Example @@ -80,9 +82,9 @@ Remember that the application lifecycle events won't have any special context se ### Android -#### AndroidManifest.xml +Automatically: -```xml +```xml file=AndroidManifest.xml @@ -97,11 +99,40 @@ Remember that the application lifecycle events won't have any special context se ``` +Or manually, disable the auto init: + +```xml file=AndroidManifest.xml + + + + [...] + + + + +``` + +And setup the SDK manually: + +```dart +Future main() async { + // init WidgetsFlutterBinding if not yet + WidgetsFlutterBinding.ensureInitialized(); + final config = PostHogConfig('YOUR_API_KEY_GOES_HERE'); + config.debug = true; + config.captureApplicationLifecycleEvents = true; + // or EU Host: 'https://eu.i.posthog.com' + config.host = 'https://us.i.posthog.com'; + await Posthog().setup(config); + runApp(MyApp()); +} +``` + ### iOS/macOS -#### Info.plist +Automatically: -```xml +```xml file=Info.plist @@ -121,9 +152,40 @@ Remember that the application lifecycle events won't have any special context se ``` +Or manually, disable the auto init: + +```xml file=Info.plist + + + + + [...] + com.posthog.posthog.AUTO_INIT + + [...] + + +``` + +And setup the SDK manually: + +```dart +Future main() async { + // init WidgetsFlutterBinding if not yet + WidgetsFlutterBinding.ensureInitialized(); + final config = PostHogConfig('YOUR_API_KEY_GOES_HERE'); + config.debug = true; + config.captureApplicationLifecycleEvents = true; + // or EU Host: 'https://eu.i.posthog.com' + config.host = 'https://us.i.posthog.com'; + await Posthog().setup(config); + runApp(MyApp()); +} +``` + ### Web -```html +```html file=index.html diff --git a/android/src/main/kotlin/com/posthog/posthog_flutter/PosthogFlutterPlugin.kt b/android/src/main/kotlin/com/posthog/posthog_flutter/PosthogFlutterPlugin.kt index a921be4..fb97fbf 100644 --- a/android/src/main/kotlin/com/posthog/posthog_flutter/PosthogFlutterPlugin.kt +++ b/android/src/main/kotlin/com/posthog/posthog_flutter/PosthogFlutterPlugin.kt @@ -1,8 +1,12 @@ package com.posthog.posthog_flutter import android.content.Context +import android.content.pm.ApplicationInfo import android.content.pm.PackageManager +import android.os.Build +import android.os.Bundle import android.util.Log +import com.posthog.PersonProfiles import com.posthog.PostHog import com.posthog.PostHogConfig import com.posthog.android.PostHogAndroid @@ -14,27 +18,54 @@ import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result /** PosthogFlutterPlugin */ -class PosthogFlutterPlugin : FlutterPlugin, MethodCallHandler { - /// The MethodChannel that will the communication between Flutter and native Android - /// - /// This local reference serves to register the plugin with the Flutter Engine and unregister it - /// when the Flutter Engine is detached from the Activity +class PosthogFlutterPlugin : + FlutterPlugin, + MethodCallHandler { + // / The MethodChannel that will be the communication between Flutter and native Android + // / + // / This local reference serves to register the plugin with the Flutter Engine and unregister it + // / when the Flutter Engine is detached from the Activity private lateinit var channel: MethodChannel + private lateinit var applicationContext: Context + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { channel = MethodChannel(flutterPluginBinding.binaryMessenger, "posthog_flutter") - initPlugin(flutterPluginBinding.applicationContext) + this.applicationContext = flutterPluginBinding.applicationContext + initPlugin() channel.setMethodCallHandler(this) } - private fun initPlugin(applicationContext: Context) { + // TODO: expose on the android SDK instead + @Throws(PackageManager.NameNotFoundException::class) + private fun getApplicationInfo(context: Context): ApplicationInfo = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + context + .packageManager + .getApplicationInfo( + context.packageName, + PackageManager.ApplicationInfoFlags.of(PackageManager.GET_META_DATA.toLong()), + ) + } else { + context + .packageManager + .getApplicationInfo(context.packageName, PackageManager.GET_META_DATA) + } + + private fun initPlugin() { try { - // TODO: replace deprecated method API 33 - val ai = applicationContext.packageManager.getApplicationInfo(applicationContext.packageName, PackageManager.GET_META_DATA) - val bundle = ai.metaData - val apiKey = bundle.getString("com.posthog.posthog.API_KEY", null) + val ai = getApplicationInfo(applicationContext) + val bundle = ai.metaData ?: Bundle() + val autoInit = bundle.getBoolean("com.posthog.posthog.AUTO_INIT", true) + + if (!autoInit) { + Log.i("PostHog", "com.posthog.posthog.AUTO_INIT is disabled!") + return + } + + val apiKey = bundle.getString("com.posthog.posthog.API_KEY") if (apiKey.isNullOrEmpty()) { Log.e("PostHog", "com.posthog.posthog.API_KEY is missing!") @@ -42,29 +73,29 @@ class PosthogFlutterPlugin : FlutterPlugin, MethodCallHandler { } val host = bundle.getString("com.posthog.posthog.POSTHOG_HOST", PostHogConfig.DEFAULT_HOST) - val trackApplicationLifecycleEvents = bundle.getBoolean("com.posthog.posthog.TRACK_APPLICATION_LIFECYCLE_EVENTS", false) - val enableDebug = bundle.getBoolean("com.posthog.posthog.DEBUG", false) + val captureApplicationLifecycleEvents = bundle.getBoolean("com.posthog.posthog.TRACK_APPLICATION_LIFECYCLE_EVENTS", false) + val debug = bundle.getBoolean("com.posthog.posthog.DEBUG", false) - // Init PostHog - val config = PostHogAndroidConfig(apiKey, host).apply { - captureScreenViews = false - captureDeepLinks = false - captureApplicationLifecycleEvents = trackApplicationLifecycleEvents - debug = enableDebug - sdkName = "posthog-flutter" - sdkVersion = postHogVersion - } - PostHogAndroid.setup(applicationContext, config) + val posthogConfig = mutableMapOf() + posthogConfig["apiKey"] = apiKey + posthogConfig["host"] = host + posthogConfig["captureApplicationLifecycleEvents"] = captureApplicationLifecycleEvents + posthogConfig["debug"] = debug + setupPostHog(posthogConfig) } catch (e: Throwable) { - e.localizedMessage?.let { Log.e("PostHog", "initPlugin error: $it") } + Log.e("PostHog", "initPlugin error: $e") } } - override fun onMethodCall(call: MethodCall, result: Result) { - + override fun onMethodCall( + call: MethodCall, + result: Result, + ) { when (call.method) { - + "setup" -> { + setup(call, result) + } "identify" -> { identify(call, result) } @@ -129,18 +160,95 @@ class PosthogFlutterPlugin : FlutterPlugin, MethodCallHandler { "flush" -> { flush(result) } + "close" -> { + close(result) + } else -> { result.notImplemented() } } + } + + private fun setup( + call: MethodCall, + result: Result, + ) { + try { + val args = call.arguments() as Map? ?: mapOf() + if (args.isEmpty()) { + result.error("PosthogFlutterException", "Arguments is null or empty", null) + return + } + + setupPostHog(args) + + result.success(null) + } catch (e: Throwable) { + result.error("PosthogFlutterException", e.localizedMessage, null) + } + } + + private fun setupPostHog(posthogConfig: Map) { + val apiKey = posthogConfig["apiKey"] as String? + if (apiKey.isNullOrEmpty()) { + Log.e("PostHog", "apiKey is missing!") + return + } + + val host = posthogConfig["host"] as String? ?: PostHogConfig.DEFAULT_HOST + val config = + PostHogAndroidConfig(apiKey, host).apply { + captureScreenViews = false + captureDeepLinks = false + posthogConfig.getIfNotNull("captureApplicationLifecycleEvents") { + captureApplicationLifecycleEvents = it + } + posthogConfig.getIfNotNull("debug") { + debug = it + } + posthogConfig.getIfNotNull("flushAt") { + flushAt = it + } + posthogConfig.getIfNotNull("maxQueueSize") { + maxQueueSize = it + } + posthogConfig.getIfNotNull("maxBatchSize") { + maxBatchSize = it + } + posthogConfig.getIfNotNull("flushInterval") { + flushIntervalSeconds = it + } + posthogConfig.getIfNotNull("sendFeatureFlagEvents") { + sendFeatureFlagEvent = it + } + posthogConfig.getIfNotNull("preloadFeatureFlags") { + preloadFeatureFlags = it + } + posthogConfig.getIfNotNull("optOut") { + optOut = it + } + posthogConfig.getIfNotNull("personProfiles") { + when (it) { + "never" -> personProfiles = PersonProfiles.NEVER + "always" -> personProfiles = PersonProfiles.ALWAYS + "identifiedOnly" -> personProfiles = PersonProfiles.IDENTIFIED_ONLY + } + } + sdkName = "posthog-flutter" + sdkVersion = postHogVersion + } + PostHogAndroid.setup(applicationContext, config) } override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { channel.setMethodCallHandler(null) } - private fun getFeatureFlag(call: MethodCall, result: Result) { + private fun getFeatureFlag( + call: MethodCall, + result: Result, + ) { try { val featureFlagKey: String = call.argument("key")!! val flag = PostHog.getFeatureFlag(featureFlagKey) @@ -150,7 +258,10 @@ class PosthogFlutterPlugin : FlutterPlugin, MethodCallHandler { } } - private fun getFeatureFlagPayload(call: MethodCall, result: Result) { + private fun getFeatureFlagPayload( + call: MethodCall, + result: Result, + ) { try { val featureFlagKey: String = call.argument("key")!! val flag = PostHog.getFeatureFlagPayload(featureFlagKey) @@ -160,7 +271,10 @@ class PosthogFlutterPlugin : FlutterPlugin, MethodCallHandler { } } - private fun identify(call: MethodCall, result: Result) { + private fun identify( + call: MethodCall, + result: Result, + ) { try { val userId: String = call.argument("userId")!! val userProperties: Map? = call.argument("userProperties") @@ -172,7 +286,10 @@ class PosthogFlutterPlugin : FlutterPlugin, MethodCallHandler { } } - private fun capture(call: MethodCall, result: Result) { + private fun capture( + call: MethodCall, + result: Result, + ) { try { val eventName: String = call.argument("eventName")!! val properties: Map? = call.argument("properties") @@ -183,7 +300,10 @@ class PosthogFlutterPlugin : FlutterPlugin, MethodCallHandler { } } - private fun screen(call: MethodCall, result: Result) { + private fun screen( + call: MethodCall, + result: Result, + ) { try { val screenName: String = call.argument("screenName")!! val properties: Map? = call.argument("properties") @@ -194,7 +314,10 @@ class PosthogFlutterPlugin : FlutterPlugin, MethodCallHandler { } } - private fun alias(call: MethodCall, result: Result) { + private fun alias( + call: MethodCall, + result: Result, + ) { try { val alias: String = call.argument("alias")!! PostHog.alias(alias) @@ -231,7 +354,10 @@ class PosthogFlutterPlugin : FlutterPlugin, MethodCallHandler { } } - private fun debug(call: MethodCall, result: Result) { + private fun debug( + call: MethodCall, + result: Result, + ) { try { val debug: Boolean = call.argument("debug")!! PostHog.debug(debug) @@ -250,7 +376,10 @@ class PosthogFlutterPlugin : FlutterPlugin, MethodCallHandler { } } - private fun isFeatureEnabled(call: MethodCall, result: Result) { + private fun isFeatureEnabled( + call: MethodCall, + result: Result, + ) { try { val key: String = call.argument("key")!! val isEnabled = PostHog.isFeatureEnabled(key) @@ -269,7 +398,10 @@ class PosthogFlutterPlugin : FlutterPlugin, MethodCallHandler { } } - private fun group(call: MethodCall, result: Result) { + private fun group( + call: MethodCall, + result: Result, + ) { try { val groupType: String = call.argument("groupType")!! val groupKey: String = call.argument("groupKey")!! @@ -281,7 +413,10 @@ class PosthogFlutterPlugin : FlutterPlugin, MethodCallHandler { } } - private fun register(call: MethodCall, result: Result) { + private fun register( + call: MethodCall, + result: Result, + ) { try { val key: String = call.argument("key")!! val value: Any = call.argument("value")!! @@ -292,7 +427,10 @@ class PosthogFlutterPlugin : FlutterPlugin, MethodCallHandler { } } - private fun unregister(call: MethodCall, result: Result) { + private fun unregister( + call: MethodCall, + result: Result, + ) { try { val key: String = call.argument("key")!! PostHog.unregister(key) @@ -310,4 +448,24 @@ class PosthogFlutterPlugin : FlutterPlugin, MethodCallHandler { result.error("PosthogFlutterException", e.localizedMessage, null) } } + + private fun close(result: Result) { + try { + PostHog.close() + result.success(null) + } catch (e: Throwable) { + result.error("PosthogFlutterException", e.localizedMessage, null) + } + } + + // Call the `completion` closure if cast to map value with `key` and type `T` is successful. + @Suppress("UNCHECKED_CAST") + private fun Map.getIfNotNull( + key: String, + callback: (T) -> Unit, + ) { + (get(key) as? T)?.let { + callback(it) + } + } } diff --git a/android/src/test/kotlin/com/posthog/posthog_flutter/PosthogFlutterPluginTest.kt b/android/src/test/kotlin/com/posthog/posthog_flutter/PosthogFlutterPluginTest.kt index b9a5d12..8d8da6a 100644 --- a/android/src/test/kotlin/com/posthog/posthog_flutter/PosthogFlutterPluginTest.kt +++ b/android/src/test/kotlin/com/posthog/posthog_flutter/PosthogFlutterPluginTest.kt @@ -2,8 +2,8 @@ package com.posthog.posthog_flutter import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel -import kotlin.test.Test import org.mockito.Mockito +import kotlin.test.Test /* * This demonstrates a simple unit test of the Kotlin portion of this plugin's implementation. @@ -14,12 +14,11 @@ import org.mockito.Mockito */ internal class PosthogFlutterPluginTest { - @Test fun onMethodCall_identify_returnsExpectedValue() { val plugin = PosthogFlutterPlugin() - var arguments = mapOf("userId" to "abc"); + var arguments = mapOf("userId" to "abc") val call = MethodCall("identify", arguments) val mockResult: MethodChannel.Result = Mockito.mock(MethodChannel.Result::class.java) @@ -32,7 +31,7 @@ internal class PosthogFlutterPluginTest { fun onMethodCall_alias_returnsExpectedValue() { val plugin = PosthogFlutterPlugin() - var arguments = mapOf("alias" to "abc"); + var arguments = mapOf("alias" to "abc") val call = MethodCall("alias", arguments) val mockResult: MethodChannel.Result = Mockito.mock(MethodChannel.Result::class.java) diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 649438f..bb059f0 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -36,7 +36,7 @@ special context set for you by the time it is initialized. --> + android:value="phc_QFbR1y41s5sxnNTZoyKG2NJo2RlsCIWkUfdpawgb40D" /> @@ -46,5 +46,7 @@ + diff --git a/example/android/app/src/main/kotlin/com/example/posthog_flutter_example/MainActivity.kt b/example/android/app/src/main/kotlin/com/example/posthog_flutter_example/MainActivity.kt index efdc6a1..1529f5a 100644 --- a/example/android/app/src/main/kotlin/com/example/posthog_flutter_example/MainActivity.kt +++ b/example/android/app/src/main/kotlin/com/example/posthog_flutter_example/MainActivity.kt @@ -2,5 +2,4 @@ package com.example.posthog_flutter_example import io.flutter.embedding.android.FlutterActivity -class MainActivity: FlutterActivity() { -} +class MainActivity : FlutterActivity() diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index 97940ec..99dd680 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -44,7 +44,7 @@ com.posthog.posthog.POSTHOG_HOST https://us.i.posthog.com com.posthog.posthog.API_KEY - _6SG-F7I1vCuZ-HdJL3VZQqjBlaSb1_20hDPwqMNnGI + phc_QFbR1y41s5sxnNTZoyKG2NJo2RlsCIWkUfdpawgb40D com.posthog.posthog.CAPTURE_APPLICATION_LIFECYCLE_EVENTS com.posthog.posthog.DEBUG @@ -53,5 +53,7 @@ UIApplicationSupportsIndirectInputEvents + diff --git a/example/lib/main.dart b/example/lib/main.dart index 40e1795..e525d84 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -2,7 +2,16 @@ import 'package:flutter/material.dart'; import 'package:posthog_flutter/posthog_flutter.dart'; -void main() { +Future main() async { + // // init WidgetsFlutterBinding if not yet + // WidgetsFlutterBinding.ensureInitialized(); + // final config = + // PostHogConfig('phc_QFbR1y41s5sxnNTZoyKG2NJo2RlsCIWkUfdpawgb40D'); + // config.debug = true; + // config.captureApplicationLifecycleEvents = true; + // config.host = 'https://us.i.posthog.com'; + // await Posthog().setup(config); + runApp(const MyApp()); } diff --git a/example/macos/Runner/Info.plist b/example/macos/Runner/Info.plist index dc2d6f0..55201d4 100644 --- a/example/macos/Runner/Info.plist +++ b/example/macos/Runner/Info.plist @@ -32,7 +32,7 @@ com.posthog.posthog.POSTHOG_HOST https://us.i.posthog.com com.posthog.posthog.API_KEY - _6SG-F7I1vCuZ-HdJL3VZQqjBlaSb1_20hDPwqMNnGI + phc_QFbR1y41s5sxnNTZoyKG2NJo2RlsCIWkUfdpawgb40D com.posthog.posthog.CAPTURE_APPLICATION_LIFECYCLE_EVENTS com.posthog.posthog.DEBUG diff --git a/ios/Classes/PostHogFlutterVersion.swift b/ios/Classes/PostHogFlutterVersion.swift index 5dcdd41..dbab251 100644 --- a/ios/Classes/PostHogFlutterVersion.swift +++ b/ios/Classes/PostHogFlutterVersion.swift @@ -8,4 +8,4 @@ import Foundation // This property is internal only -internal let postHogFlutterVersion = "4.5.0" +let postHogFlutterVersion = "4.5.0" diff --git a/ios/Classes/PosthogFlutterPlugin.swift b/ios/Classes/PosthogFlutterPlugin.swift index 6fbfdb1..2f88607 100644 --- a/ios/Classes/PosthogFlutterPlugin.swift +++ b/ios/Classes/PosthogFlutterPlugin.swift @@ -1,55 +1,123 @@ import PostHog #if os(iOS) -import Flutter -import UIKit + import Flutter + import UIKit #elseif os(macOS) -import FlutterMacOS -import AppKit + import AppKit + import FlutterMacOS #endif public class PosthogFlutterPlugin: NSObject, FlutterPlugin { public static func register(with registrar: FlutterPluginRegistrar) { -#if os(iOS) - let channel = FlutterMethodChannel(name: "posthog_flutter", binaryMessenger: registrar.messenger()) -#elseif os(macOS) - let channel = FlutterMethodChannel(name: "posthog_flutter", binaryMessenger: registrar.messenger) -#endif + #if os(iOS) + let channel = FlutterMethodChannel(name: "posthog_flutter", binaryMessenger: registrar.messenger()) + #elseif os(macOS) + let channel = FlutterMethodChannel(name: "posthog_flutter", binaryMessenger: registrar.messenger) + #endif let instance = PosthogFlutterPlugin() initPlugin() registrar.addMethodCallDelegate(instance, channel: channel) } public static func initPlugin() { - // Initialise PostHog + let autoInit = Bundle.main.object(forInfoDictionaryKey: "com.posthog.posthog.AUTO_INIT") as? Bool ?? true + if !autoInit { + print("[PostHog] com.posthog.posthog.AUTO_INIT is disabled!") + return + } + let apiKey = Bundle.main.object(forInfoDictionaryKey: "com.posthog.posthog.API_KEY") as? String ?? "" + let host = Bundle.main.object(forInfoDictionaryKey: "com.posthog.posthog.POSTHOG_HOST") as? String ?? PostHogConfig.defaultHost + let captureApplicationLifecycleEvents = Bundle.main.object(forInfoDictionaryKey: "com.posthog.posthog.CAPTURE_APPLICATION_LIFECYCLE_EVENTS") as? Bool ?? false + let debug = Bundle.main.object(forInfoDictionaryKey: "com.posthog.posthog.DEBUG") as? Bool ?? false + + setupPostHog([ + "apiKey": apiKey, + "host": host, + "captureApplicationLifecycleEvents": captureApplicationLifecycleEvents, + "debug": debug + ]) + } + + private static func setupPostHog(_ posthogConfig: [String: Any]) { + let apiKey = posthogConfig["apiKey"] as? String ?? "" if apiKey.isEmpty { - print("[PostHog] com.posthog.posthog.API_KEY is missing!") + print("[PostHog] apiKey is missing!") return } - let host = Bundle.main.object(forInfoDictionaryKey: "com.posthog.posthog.POSTHOG_HOST") as? String ?? PostHogConfig.defaultHost - let postHogCaptureLifecyleEvents = Bundle.main.object(forInfoDictionaryKey: "com.posthog.posthog.CAPTURE_APPLICATION_LIFECYCLE_EVENTS") as? Bool ?? false - let postHogDebug = Bundle.main.object(forInfoDictionaryKey: "com.posthog.posthog.DEBUG") as? Bool ?? false + let host = posthogConfig["host"] as? String ?? PostHogConfig.defaultHost let config = PostHogConfig( apiKey: apiKey, host: host ) - config.captureApplicationLifecycleEvents = postHogCaptureLifecyleEvents - config.debug = postHogDebug config.captureScreenViews = false - + + if let captureApplicationLifecycleEvents = posthogConfig["captureApplicationLifecycleEvents"] as? Bool { + config.captureApplicationLifecycleEvents = captureApplicationLifecycleEvents + } + if let debug = posthogConfig["debug"] as? Bool { + config.debug = debug + } + if let flushAt = posthogConfig["flushAt"] as? Int { + config.flushAt = flushAt + } + if let maxQueueSize = posthogConfig["maxQueueSize"] as? Int { + config.maxQueueSize = maxQueueSize + } + if let maxBatchSize = posthogConfig["maxBatchSize"] as? Int { + config.maxBatchSize = maxBatchSize + } + if let flushInterval = posthogConfig["flushInterval"] as? Int { + config.flushIntervalSeconds = Double(flushInterval) + } + if let sendFeatureFlagEvents = posthogConfig["sendFeatureFlagEvents"] as? Bool { + config.sendFeatureFlagEvent = sendFeatureFlagEvents + } + if let preloadFeatureFlags = posthogConfig["preloadFeatureFlags"] as? Bool { + config.preloadFeatureFlags = preloadFeatureFlags + } + if let optOut = posthogConfig["optOut"] as? Bool { + config.optOut = optOut + } + if let personProfiles = posthogConfig["personProfiles"] as? String { + switch personProfiles { + case "never": + config.personProfiles = .never + case "always": + config.personProfiles = .always + case "identifiedOnly": + config.personProfiles = .identifiedOnly + default: + break + } + } + if let dataMode = posthogConfig["dataMode"] as? String { + switch dataMode { + case "wifi": + config.dataMode = .wifi + case "cellular": + config.dataMode = .cellular + case "any": + config.dataMode = .any + default: + break + } + } + // Update SDK name and version - postHogSdkName = "posthog-flutter" - postHogVersion = postHogFlutterVersion - + postHogSdkName = "posthog-flutter" + postHogVersion = postHogFlutterVersion + PostHogSDK.shared.setup(config) - // } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { switch call.method { + case "setup": + setup(call, result: result) case "getFeatureFlag": getFeatureFlag(call, result: result) case "isFeatureEnabled": @@ -84,18 +152,31 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin { unregister(call, result: result) case "flush": flush(result) + case "close": + close(result) default: result(FlutterMethodNotImplemented) } } + private func setup( + _ call: FlutterMethodCall, + result: @escaping FlutterResult + ) { + if let args = call.arguments as? [String: Any] { + PosthogFlutterPlugin.setupPostHog(args) + result(nil) + } else { + _badArgumentError(result) + } + } + private func getFeatureFlag( _ call: FlutterMethodCall, result: @escaping FlutterResult ) { if let args = call.arguments as? [String: Any], - let featureFlagKey = args["key"] as? String - { + let featureFlagKey = args["key"] as? String { let value = PostHogSDK.shared.getFeatureFlag(featureFlagKey) result(value) } else { @@ -108,8 +189,7 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin { result: @escaping FlutterResult ) { if let args = call.arguments as? [String: Any], - let featureFlagKey = args["key"] as? String - { + let featureFlagKey = args["key"] as? String { let value = PostHogSDK.shared.isFeatureEnabled(featureFlagKey) result(value) } else { @@ -122,8 +202,7 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin { result: @escaping FlutterResult ) { if let args = call.arguments as? [String: Any], - let featureFlagKey = args["key"] as? String - { + let featureFlagKey = args["key"] as? String { let value = PostHogSDK.shared.getFeatureFlagPayload(featureFlagKey) result(value) } else { @@ -136,8 +215,7 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin { result: @escaping FlutterResult ) { if let args = call.arguments as? [String: Any], - let userId = args["userId"] as? String - { + let userId = args["userId"] as? String { let userProperties = args["userProperties"] as? [String: Any] let userPropertiesSetOnce = args["userPropertiesSetOnce"] as? [String: Any] @@ -157,8 +235,7 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin { result: @escaping FlutterResult ) { if let args = call.arguments as? [String: Any], - let eventName = args["eventName"] as? String - { + let eventName = args["eventName"] as? String { let properties = args["properties"] as? [String: Any] PostHogSDK.shared.capture( eventName, @@ -175,8 +252,7 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin { result: @escaping FlutterResult ) { if let args = call.arguments as? [String: Any], - let screenName = args["screenName"] as? String - { + let screenName = args["screenName"] as? String { let properties = args["properties"] as? [String: Any] PostHogSDK.shared.screen( screenName, @@ -193,8 +269,7 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin { result: @escaping FlutterResult ) { if let args = call.arguments as? [String: Any], - let alias = args["alias"] as? String - { + let alias = args["alias"] as? String { PostHogSDK.shared.alias(alias) result(nil) } else { @@ -227,8 +302,7 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin { result: @escaping FlutterResult ) { if let args = call.arguments as? [String: Any], - let debug = args["debug"] as? Bool - { + let debug = args["debug"] as? Bool { PostHogSDK.shared.debug(debug) result(nil) } else { @@ -248,8 +322,7 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin { ) { if let args = call.arguments as? [String: Any], let groupType = args["groupType"] as? String, - let groupKey = args["groupKey"] as? String - { + let groupKey = args["groupKey"] as? String { let groupProperties = args["groupProperties"] as? [String: Any] PostHogSDK.shared.group(type: groupType, key: groupKey, groupProperties: groupProperties) result(nil) @@ -264,8 +337,7 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin { ) { if let args = call.arguments as? [String: Any], let key = args["key"] as? String, - let value = args["value"] - { + let value = args["value"] { PostHogSDK.shared.register([key: value]) result(nil) } else { @@ -278,20 +350,24 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin { result: @escaping FlutterResult ) { if let args = call.arguments as? [String: Any], - let key = args["key"] as? String - { + let key = args["key"] as? String { PostHogSDK.shared.unregister(key) result(nil) } else { _badArgumentError(result) } } - + private func flush(_ result: @escaping FlutterResult) { PostHogSDK.shared.flush() result(nil) } + private func close(_ result: @escaping FlutterResult) { + PostHogSDK.shared.close() + result(nil) + } + // Return bad Arguments error private func _badArgumentError(_ result: @escaping FlutterResult) { result(FlutterError( diff --git a/lib/posthog_flutter.dart b/lib/posthog_flutter.dart index 6250534..1e95495 100644 --- a/lib/posthog_flutter.dart +++ b/lib/posthog_flutter.dart @@ -1,4 +1,5 @@ library posthog_flutter; export 'src/posthog.dart'; +export 'src/posthog_config.dart'; export 'src/posthog_observer.dart'; diff --git a/lib/posthog_flutter_web.dart b/lib/posthog_flutter_web.dart index 93c75dc..83cccbe 100644 --- a/lib/posthog_flutter_web.dart +++ b/lib/posthog_flutter_web.dart @@ -24,7 +24,6 @@ class PosthogFlutterWeb extends PosthogFlutterPlatformInterface { channel.setMethodCallHandler(instance.handleMethodCall); } - Future handleMethodCall(MethodCall call) async { - return handleWebMethodCall(call, context); - } + Future handleMethodCall(MethodCall call) => + handleWebMethodCall(call, context); } diff --git a/lib/src/posthog.dart b/lib/src/posthog.dart index d955fc3..88b34fc 100644 --- a/lib/src/posthog.dart +++ b/lib/src/posthog.dart @@ -1,3 +1,4 @@ +import 'posthog_config.dart'; import 'posthog_flutter_platform_interface.dart'; class Posthog { @@ -12,16 +13,21 @@ class Posthog { String? _currentScreen; + /// Android and iOS only + /// Only used for the manual setup + /// Requires disabling the automatic init on Android and iOS: + /// com.posthog.posthog.AUTO_INIT: false + Future setup(PostHogConfig config) => _posthog.setup(config); + Future identify({ required String userId, Map? userProperties, Map? userPropertiesSetOnce, - }) { - return _posthog.identify( - userId: userId, - userProperties: userProperties, - userPropertiesSetOnce: userPropertiesSetOnce); - } + }) => + _posthog.identify( + userId: userId, + userProperties: userProperties, + userPropertiesSetOnce: userPropertiesSetOnce); Future capture({ required String eventName, @@ -44,82 +50,58 @@ class Posthog { Future screen({ required String screenName, Map? properties, - }) { - _currentScreen = screenName; - - return _posthog.screen( - screenName: screenName, - properties: properties, - ); - } + }) => + _posthog.screen( + screenName: screenName, + properties: properties, + ); Future alias({ required String alias, - }) { - return _posthog.alias( - alias: alias, - ); - } + }) => + _posthog.alias( + alias: alias, + ); - Future getDistinctId() { - return _posthog.getDistinctId(); - } + Future getDistinctId() => _posthog.getDistinctId(); - Future reset() { - return _posthog.reset(); - } + Future reset() => _posthog.reset(); - Future disable() { - return _posthog.disable(); - } + Future disable() => _posthog.disable(); - Future enable() { - return _posthog.enable(); - } + Future enable() => _posthog.enable(); - Future debug(bool enabled) { - return _posthog.debug(enabled); - } + Future debug(bool enabled) => _posthog.debug(enabled); - Future register(String key, Object value) { - return _posthog.register(key, value); - } + Future register(String key, Object value) => + _posthog.register(key, value); - Future unregister(String key) { - return _posthog.unregister(key); - } + Future unregister(String key) => _posthog.unregister(key); - Future isFeatureEnabled(String key) { - return _posthog.isFeatureEnabled(key); - } + Future isFeatureEnabled(String key) => _posthog.isFeatureEnabled(key); - Future reloadFeatureFlags() { - return _posthog.reloadFeatureFlags(); - } + Future reloadFeatureFlags() => _posthog.reloadFeatureFlags(); Future group({ required String groupType, required String groupKey, Map? groupProperties, - }) { - return _posthog.group( - groupType: groupType, - groupKey: groupKey, - groupProperties: groupProperties, - ); - } + }) => + _posthog.group( + groupType: groupType, + groupKey: groupKey, + groupProperties: groupProperties, + ); - Future getFeatureFlag(String key) { - return _posthog.getFeatureFlag(key: key); - } + Future getFeatureFlag(String key) => + _posthog.getFeatureFlag(key: key); - Future getFeatureFlagPayload(String key) { - return _posthog.getFeatureFlagPayload(key: key); - } + Future getFeatureFlagPayload(String key) => + _posthog.getFeatureFlagPayload(key: key); - Future flush() async { - return _posthog.flush(); - } + Future flush() => _posthog.flush(); + + Future close() => _posthog.close(); Posthog._internal(); } diff --git a/lib/src/posthog_config.dart b/lib/src/posthog_config.dart new file mode 100644 index 0000000..59d65c4 --- /dev/null +++ b/lib/src/posthog_config.dart @@ -0,0 +1,44 @@ +enum PostHogPersonProfiles { never, always, identifiedOnly } + +enum PostHogDataMode { wifi, cellular, any } + +class PostHogConfig { + final String apiKey; + var host = 'https://us.i.posthog.com'; + var flushAt = 20; + var maxQueueSize = 1000; + var maxBatchSize = 50; + var flushInterval = const Duration(seconds: 30); + var sendFeatureFlagEvents = true; + var preloadFeatureFlags = true; + var captureApplicationLifecycleEvents = false; + var debug = false; + var optOut = false; + var personProfiles = PostHogPersonProfiles.identifiedOnly; + + /// iOS only + var dataMode = PostHogDataMode.any; + + // TODO: missing getAnonymousId, propertiesSanitizer, sessionReplay, captureDeepLinks + // onFeatureFlags, integrations + + PostHogConfig(this.apiKey); + + Map toMap() { + return { + 'apiKey': apiKey, + 'host': host, + 'flushAt': flushAt, + 'maxQueueSize': maxQueueSize, + 'maxBatchSize': maxBatchSize, + 'flushInterval': flushInterval.inSeconds, + 'sendFeatureFlagEvents': sendFeatureFlagEvents, + 'preloadFeatureFlags': preloadFeatureFlags, + 'captureApplicationLifecycleEvents': captureApplicationLifecycleEvents, + 'debug': debug, + 'optOut': optOut, + 'personProfiles': personProfiles.name, + 'dataMode': dataMode.name, + }; + } +} diff --git a/lib/src/posthog_flutter_io.dart b/lib/src/posthog_flutter_io.dart index ab342cc..13cc29f 100644 --- a/lib/src/posthog_flutter_io.dart +++ b/lib/src/posthog_flutter_io.dart @@ -1,6 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +import 'posthog_config.dart'; import 'posthog_flutter_platform_interface.dart'; /// An implementation of [PosthogFlutterPlatformInterface] that uses method channels. @@ -8,6 +9,15 @@ class PosthogFlutterIO extends PosthogFlutterPlatformInterface { /// The method channel used to interact with the native platform. final _methodChannel = const MethodChannel('posthog_flutter'); + @override + Future setup(PostHogConfig config) async { + try { + await _methodChannel.invokeMethod('setup', config.toMap()); + } on PlatformException catch (exception) { + _printIfDebug('Exeption on setup: $exception'); + } + } + @override Future identify({ required String userId, @@ -211,6 +221,15 @@ class PosthogFlutterIO extends PosthogFlutterPlatformInterface { } } + @override + Future close() async { + try { + return await _methodChannel.invokeMethod('close'); + } on PlatformException catch (exception) { + _printIfDebug('Exeption on close: $exception'); + } + } + void _printIfDebug(String message) { if (kDebugMode) { print(message); diff --git a/lib/src/posthog_flutter_platform_interface.dart b/lib/src/posthog_flutter_platform_interface.dart index 6797cd1..7678282 100644 --- a/lib/src/posthog_flutter_platform_interface.dart +++ b/lib/src/posthog_flutter_platform_interface.dart @@ -1,5 +1,6 @@ import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import 'posthog_config.dart'; import 'posthog_flutter_io.dart'; abstract class PosthogFlutterPlatformInterface extends PlatformInterface { @@ -23,6 +24,10 @@ abstract class PosthogFlutterPlatformInterface extends PlatformInterface { _instance = instance; } + Future setup(PostHogConfig config) { + throw UnimplementedError('setup() has not been implemented.'); + } + Future identify( {required String userId, Map? userProperties, @@ -111,5 +116,9 @@ abstract class PosthogFlutterPlatformInterface extends PlatformInterface { throw UnimplementedError('flush() has not been implemented.'); } - // TODO: missing capture with more parameters, close + Future close() { + throw UnimplementedError('close() has not been implemented.'); + } + + // TODO: missing capture with more parameters } diff --git a/lib/src/posthog_flutter_web_handler.dart b/lib/src/posthog_flutter_web_handler.dart index 70843bd..a4fcd59 100644 --- a/lib/src/posthog_flutter_web_handler.dart +++ b/lib/src/posthog_flutter_web_handler.dart @@ -5,6 +5,10 @@ import 'package:flutter/services.dart'; Future handleWebMethodCall(MethodCall call, JsObject context) async { final analytics = JsObject.fromBrowserObject(context['posthog']); switch (call.method) { + case 'setup': + // not supported on Web + // analytics.callMethod('setup'); + break; case 'identify': final userProperties = call.arguments['userProperties'] ?? {}; final userPropertiesSetOnce = @@ -94,6 +98,10 @@ Future handleWebMethodCall(MethodCall call, JsObject context) async { // not supported on Web // analytics.callMethod('flush'); break; + case 'close': + // not supported on Web + // analytics.callMethod('close'); + break; default: throw PlatformException( code: 'Unimplemented', diff --git a/test/posthog_observer_test.dart b/test/posthog_observer_test.dart index c51dd03..00d2e47 100644 --- a/test/posthog_observer_test.dart +++ b/test/posthog_observer_test.dart @@ -1,5 +1,6 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:posthog_flutter/posthog_flutter.dart'; import 'package:posthog_flutter/src/posthog_flutter_io.dart'; import 'package:posthog_flutter/src/posthog_flutter_platform_interface.dart'; import 'package:posthog_flutter/src/posthog_observer.dart';