From 3a065e4489d20f5e11ae48bae139958594949abf Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Tue, 28 Apr 2026 19:06:38 +0600 Subject: [PATCH 1/6] fix: guard static MethodChannel from multi-engine overwrite (Android) When FirebaseMessaging.onBackgroundMessage (or any mechanism that creates a second FlutterEngine) triggers onAttachedToEngine a second time, the static channel was unconditionally overwritten. This caused notification callbacks to route to the wrong engine, silently dropping them. The fix adds an early return if channel is already set, and nulls the channel in onDetachedFromEngine so re-attachment works after a real detach. Fixes FSSDK-12503 Co-Authored-By: Claude Opus 4.6 --- .../optimizely_flutter_sdk/OptimizelyFlutterSdkPlugin.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterSdkPlugin.java b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterSdkPlugin.java index 1893324..0947d59 100644 --- a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterSdkPlugin.java +++ b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterSdkPlugin.java @@ -212,6 +212,9 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { @Override public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { + if (channel != null) { + return; + } channel = new MethodChannel(binding.getBinaryMessenger(), "optimizely_flutter_sdk"); channel.setMethodCallHandler(this); context = binding.getApplicationContext(); @@ -232,6 +235,7 @@ public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { @Override public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { channel.setMethodCallHandler(null); + channel = null; // Stop and detach the appender if (flutterLogbackAppender != null) { Logger rootLogger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); From cc65dac05b540177a371cca9bbf7350361700f2a Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Tue, 28 Apr 2026 19:07:49 +0600 Subject: [PATCH 2/6] fix: guard logger channel from multi-engine overwrite (Android) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same root cause as the main channel fix — the static logger channel could be overwritten by a second FlutterEngine. The setChannel guard allows explicit null (cleanup) but prevents overwrite of an active channel. Fixes FSSDK-12503 Co-Authored-By: Claude Opus 4.6 --- .../optimizely_flutter_sdk/FlutterLogbackAppender.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/FlutterLogbackAppender.java b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/FlutterLogbackAppender.java index 2252cdd..05a7ddb 100644 --- a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/FlutterLogbackAppender.java +++ b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/FlutterLogbackAppender.java @@ -17,8 +17,10 @@ public class FlutterLogbackAppender extends AppenderBase { public static MethodChannel channel; private static final Handler mainThreadHandler = new Handler(Looper.getMainLooper()); - public static void setChannel(MethodChannel channel) { - FlutterLogbackAppender.channel = channel; + public static void setChannel(MethodChannel newChannel) { + if (newChannel == null || FlutterLogbackAppender.channel == null) { + FlutterLogbackAppender.channel = newChannel; + } } @Override From 485ce5461d744dfd97dde0dabc979629c6bdf17b Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Tue, 28 Apr 2026 19:08:19 +0600 Subject: [PATCH 3/6] fix: guard static MethodChannel from multi-engine overwrite (iOS) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same vulnerability as Android — if register(with:) were called twice (e.g. by a second FlutterEngine), the static channel would be overwritten. The fix adds an early return if channel is already set. Fixes FSSDK-12503 Co-Authored-By: Claude Opus 4.6 --- ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift b/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift index da2affb..82bb58d 100644 --- a/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift +++ b/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift @@ -38,6 +38,9 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin { /// Registers optimizely_flutter_sdk channel to communicate with the flutter sdk to receive requests and send responses public static func register(with registrar: FlutterPluginRegistrar) { + if channel != nil { + return + } channel = FlutterMethodChannel(name: "optimizely_flutter_sdk", binaryMessenger: registrar.messenger()) let instance = SwiftOptimizelyFlutterSdkPlugin() registrar.addMethodCallDelegate(instance, channel: channel) From 69ca3e09146ff1d7752dca6a3aece2c25e1ce784 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Tue, 28 Apr 2026 19:09:03 +0600 Subject: [PATCH 4/6] fix: guard logger channel from multi-engine overwrite (iOS) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same pattern as the Android logger fix — prevents a second engine from overwriting the active logger channel. Fixes FSSDK-12503 Co-Authored-By: Claude Opus 4.6 --- ios/Classes/OptimizelyFlutterLogger.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ios/Classes/OptimizelyFlutterLogger.swift b/ios/Classes/OptimizelyFlutterLogger.swift index 7b9217a..1975126 100644 --- a/ios/Classes/OptimizelyFlutterLogger.swift +++ b/ios/Classes/OptimizelyFlutterLogger.swift @@ -12,8 +12,10 @@ public class OptimizelyFlutterLogger: NSObject, OPTLogger { super.init() } - public static func setChannel(_ channel: FlutterMethodChannel) { - loggerChannel = channel + public static func setChannel(_ channel: FlutterMethodChannel?) { + if channel == nil || loggerChannel == nil { + loggerChannel = channel + } } public func log(level: OptimizelyLogLevel, message: String) { From 65dffed17f8a894b09c244fac75c0251e05507ba Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Tue, 28 Apr 2026 19:30:16 +0600 Subject: [PATCH 5/6] refactor: split setChannel into setChannel/clearChannel for logger channels Replace the compound null-guard logic with explicit setChannel (only sets if no active channel) and clearChannel (always clears). This matches the main plugin's pattern and makes intent immediately obvious. Callers updated: onDetachedFromEngine now calls clearChannel() instead of setChannel(null). Fixes FSSDK-12503 Co-Authored-By: Claude Opus 4.6 --- .../optimizely_flutter_sdk/FlutterLogbackAppender.java | 8 ++++++-- .../OptimizelyFlutterSdkPlugin.java | 4 ++-- ios/Classes/OptimizelyFlutterLogger.swift | 8 ++++++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/FlutterLogbackAppender.java b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/FlutterLogbackAppender.java index 05a7ddb..099329e 100644 --- a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/FlutterLogbackAppender.java +++ b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/FlutterLogbackAppender.java @@ -18,11 +18,15 @@ public class FlutterLogbackAppender extends AppenderBase { private static final Handler mainThreadHandler = new Handler(Looper.getMainLooper()); public static void setChannel(MethodChannel newChannel) { - if (newChannel == null || FlutterLogbackAppender.channel == null) { - FlutterLogbackAppender.channel = newChannel; + if (channel == null) { + channel = newChannel; } } + public static void clearChannel() { + channel = null; + } + @Override protected void append(ILoggingEvent event) { if (channel == null) { diff --git a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterSdkPlugin.java b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterSdkPlugin.java index 0947d59..512b1a1 100644 --- a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterSdkPlugin.java +++ b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterSdkPlugin.java @@ -243,8 +243,8 @@ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { flutterLogbackAppender.stop(); flutterLogbackAppender = null; } - // Clean up the channel - FlutterLogbackAppender.setChannel(null); + // Clean up the logger channel + FlutterLogbackAppender.clearChannel(); } @Override diff --git a/ios/Classes/OptimizelyFlutterLogger.swift b/ios/Classes/OptimizelyFlutterLogger.swift index 1975126..57c7adf 100644 --- a/ios/Classes/OptimizelyFlutterLogger.swift +++ b/ios/Classes/OptimizelyFlutterLogger.swift @@ -12,11 +12,15 @@ public class OptimizelyFlutterLogger: NSObject, OPTLogger { super.init() } - public static func setChannel(_ channel: FlutterMethodChannel?) { - if channel == nil || loggerChannel == nil { + public static func setChannel(_ channel: FlutterMethodChannel) { + if loggerChannel == nil { loggerChannel = channel } } + + public static func clearChannel() { + loggerChannel = nil + } public func log(level: OptimizelyLogLevel, message: String) { // Early return if level check fails From b541ad79cf3a18df7340ed5e9d1c74bcfa6a7e75 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Wed, 29 Apr 2026 21:55:34 +0600 Subject: [PATCH 6/6] fix: add iOS engine detach cleanup and enforce private channel access Add detachFromEngine(for:) to iOS plugin so channel is cleaned up when the engine detaches, matching Android's onDetachedFromEngine behavior. Make FlutterLogbackAppender.channel private to enforce use of setChannel/clearChannel accessors. Co-Authored-By: Claude Opus 4.6 --- .../optimizely_flutter_sdk/FlutterLogbackAppender.java | 2 +- ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/FlutterLogbackAppender.java b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/FlutterLogbackAppender.java index 099329e..28eec89 100644 --- a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/FlutterLogbackAppender.java +++ b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/FlutterLogbackAppender.java @@ -14,7 +14,7 @@ public class FlutterLogbackAppender extends AppenderBase { public static final String CHANNEL_NAME = "optimizely_flutter_sdk_logger"; - public static MethodChannel channel; + private static MethodChannel channel; private static final Handler mainThreadHandler = new Handler(Looper.getMainLooper()); public static void setChannel(MethodChannel newChannel) { diff --git a/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift b/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift index 82bb58d..8d17b66 100644 --- a/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift +++ b/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift @@ -53,7 +53,13 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin { taskQueue: taskQueue) OptimizelyFlutterLogger.setChannel(loggerChannel) } - + + public func detachFromEngine(for registrar: FlutterPluginRegistrar) { + Self.channel?.setMethodCallHandler(nil) + Self.channel = nil + OptimizelyFlutterLogger.clearChannel() + } + /// Part of FlutterPlugin protocol to handle communication with flutter sdk. /// All method handlers receive a main-thread-safe result callback so that /// any handler calling result() from a background thread (e.g. async SDK