Skip to content

Ak/bd 7586#38

Open
akrupczynskirezolve wants to merge 8 commits into
ak/BD-6356from
ak/BD-7586
Open

Ak/bd 7586#38
akrupczynskirezolve wants to merge 8 commits into
ak/BD-6356from
ak/BD-7586

Conversation

@akrupczynskirezolve

@akrupczynskirezolve akrupczynskirezolve commented May 7, 2026

Copy link
Copy Markdown

AI Development Checklist

  • Spec/requirement linked:
  • AI tool used:
  • Tests generated/refined with AI
  • AI review pass completed
  • Repo context files still accurate (update if not)

Jira Context

Ticket: BD-7586 – Push Notifications - Flutter plugin
Status: In Merge Request | Priority: P3 - Major | Type: Story

Summary

Enable Flutter developers to use push notifications (including location-triggered notifications) without requiring native iOS/Android implementation, ensuring parity with our native SDKs.

Acceptance Criteria

  • Developers can integrate push notifications in Flutter without native code
  • Location-based triggers can be configured
  • Notifications work on both iOS and Android
  • SDK provides sensible default values (e.g. notification icon, priority, and system behaviors) to ensure notifications are delivered even when not explicitly configured
  • Documentation is available

@akrupczynskirezolve akrupczynskirezolve left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — BD-7586: Push Notifications - Flutter plugin

ℹ️ .github/copilot-instructions.md and docs/ai/resources/ were not found in the repo; review is performed against general Flutter/Dart best practices.

What this PR does

Adds bluedot_point_sdk_push as an optional dependency, gates the push-notifications feature behind a PUSH_ENABLED flag (set in gradle.properties / Info.plist and bridged to Flutter via a MethodChannel), wires up PushNotificationManager and a new PushNotificationsPage, and substantially refactors GeoTriggeringPage to use a singleton event store + live list view instead of modal alerts.

Overview

The direction is solid — the singleton handler pattern for the geo-triggering channel is a meaningful improvement over per-page setMethodCallHandler calls. The push isolation behind a flag is sensible for a demo app that not-always has Firebase credentials available.

However, there are 4 blocking issues and 5 important issues that need attention before merge (see inline comments). The most critical are:

  • 🔴 Uncancelled stream subscription in PushNotificationsPage (memory/behaviour leak)
  • 🔴 setState without mounted guard in HomePage.initState
  • 🔴 Inconsistent PUSH_ENABLED defaults (true in main.dart, false in constants.dart)
  • 🔴 Split-brain push-enabled detection where the listener registration (compile-time) and the UI button (runtime) are driven by different mechanisms that can disagree

Top-level observations

  • Testing: No tests are included. New logic (singleton store, event parsing, AppConfig, PushNotificationManager) is well-suited for unit tests with a mock MethodChannel. Not blocking for a demo app, but worth flagging.
  • GeoTriggeringPage refactor: The new layout (controls pinned top, scrollable event log below) is a UX improvement. The _GeoTriggeringChannelHandler singleton elegantly solves the previous problem of losing events on page re-creation.
  • Documentation: PR body was empty; Jira context has been added. Consider updating the README.md with how to enable push (setting PUSH_ENABLED=true + providing google-services.json).

🎭 A Flutter dev said: "Push shall be found!"
So flags were born — both true and false, profound.
The singleton stood tall while streams ran free,
Uncancelled, unguarded — a bug in the tree. 🎭

});
}

@override

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Stream subscription leak — the StreamSubscription returned by .listen(...) is never stored and cancelled in dispose(). If the user navigates away and back repeatedly, each mount creates a new subscription that runs indefinitely.

late StreamSubscription<PushNotificationEvent> _sub;

@override
void initState() {
  super.initState();
  _events = List.from(PushNotificationManager.instance.events);
  _sub = PushNotificationManager.instance.stream.listen((event) {
    if (mounted) setState(() => _events.add(event));
  });
}

@override
void dispose() {
  _sub.cancel();
  super.dispose();
}

Comment thread lib/home_page.dart
});
_retrieveProjectId();
AppConfig.isPushEnabled().then((value) {
setState(() {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 setState without mounted guard — if the widget is disposed before the async MethodChannel call resolves (e.g. the user navigates away quickly), this throws a setState() called after dispose() error.

AppConfig.isPushEnabled().then((value) {
  if (mounted) {
    setState(() => _pushEnabled = value);
  }
});

Comment thread lib/main.dart

/// Whether push-notifications is enabled in this build.
/// Set via --dart-define=PUSH_ENABLED=true|false (driven by gradle.properties).
const bool _pushEnabled = bool.fromEnvironment('PUSH_ENABLED', defaultValue: true);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Inconsistent PUSH_ENABLED defaultsdefaultValue: true here means push is on by default when no --dart-define is passed at build time, but constants.dart declares bool.fromEnvironment('PUSH_ENABLED') (which defaults to false). A developer who runs the app without explicitly setting --dart-define=PUSH_ENABLED=true will get _pushEnabled = true in main.dart (push listener registered) but pushEnabled = false in constants.dart. The two sources of truth drift.

Align both to the same default, or remove the compile-time constant in constants.dart and rely solely on the runtime AppConfig.isPushEnabled() path.

Comment thread lib/main.dart
PushNotificationEvent.fromMap('clicked', data),
);
},
);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Split-brain push-enabled detection_pushEnabled in main.dart (compile-time bool.fromEnvironment) drives whether the push listener is wired up, while AppConfig.isPushEnabled() (runtime MethodChannel → native) drives whether the button appears in HomePage. These can disagree:

  • Build without --dart-define=PUSH_ENABLED=true → listener not registered, but AppConfig returns the native value (Android: true from gradle.properties) → button is shown → tapping it may crash or silently do nothing.

Consider making the push listener setup reactive to the same runtime value, or gate everything on a single source of truth.

_controller.add(event);
}

void dispose() {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 dispose() on a singleton is dangerousPushNotificationManager is a singleton, but exposes a dispose() method that closes the StreamController. Any caller who invokes it will permanently break push delivery for the entire app lifetime (the controller cannot be re-opened). Either:

  • Remove the dispose() method if the singleton is meant to live for the app's lifetime, or
  • Add an isClosed guard and re-create the controller on next use.

Comment thread android/gradle.properties
@@ -1,4 +1,5 @@
org.gradle.jvmargs=-Xmx1536M
org.gradle.jvmargs=-Xmx4096M -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError
PUSH_ENABLED=true

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 PUSH_ENABLED=true committed to sourcegradle.properties sets PUSH_ENABLED=true, but android/app/google-services.json is now gitignored. Any contributor who clones the repo and runs flutter build apk (or flutter run) will get a build failure because com.google.gms.google-services is applied but no google-services.json is present.

Consider defaulting to PUSH_ENABLED=false here and documenting in the README how to opt-in, or provide a no-op placeholder google-services.json for CI / open-source builds.

Comment thread pubspec.yaml
# The example app is bundled with the plugin so we use a path dependency on
# the parent directory to use the current plugin's version.
git:
url: https://github.com/Bluedot-Innovation/PointSDK-Flutter-Plugin.git

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Feature-branch SDK refs must not merge to a stable branchpubspec.yaml pins both bluedot_point_sdk and bluedot_point_sdk_push to ref: ak/BD-7586. Likewise pubspec.lock resolves to those branch SHAs. If the SDK branch is rebased or force-pushed, anyone running flutter pub get will get a different (or missing) commit. These refs must be replaced with a stable tag/release SHA before merge.


Future<void> _handleCall(MethodCall call) async {
final args = call.arguments;
final store = GeoTriggerEventStore.instance;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Possible regression: didUpdateZoneInfo no longer fetches zone details — the previous implementation called _getZonesAndFences() on didUpdateZoneInfo, which fetched the full zone list and displayed destination custom data in an alert. The new handler just logs the raw args to the event list. This is a silent behavioural change: the zone detail display is completely removed. If this is intentional (the old approach was overly noisy), please note it in the PR description. For a minimal integration demo, showing the raw event args is probably fine, but the old getZonesAndFences() call was the only place demonstrating that API — its removal should be deliberate.

return result;
} on PlatformException {
return true; // default to enabled on error
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 AppConfig.isPushEnabled() catches only PlatformException — if the MethodChannel is not set up yet (e.g. MissingPluginException in a test harness or a fresh install state), the exception won't be caught and the call will throw unhandled. Consider broadening the catch to Exception or at minimum also catching MissingPluginException.

} catch (_) {
  return true; // default to enabled on error
}

Comment thread ios/Runner/Info.plist
@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 iOS defaults PUSH_ENABLED to false, AppConfig defaults to trueInfo.plist sets <key>PUSH_ENABLED</key><false/>, but AppConfig.isPushEnabled() returns true on any PlatformException. If the Info.plist key is read successfully it returns false (push hidden), but if the channel call fails for any reason it returns true (push shown). The two fallback behaviours are opposite. Make them consistent.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds push-notification support to the Flutter minimal integration app (including a UI event log) and refactors geo-triggering event handling to persist logs across navigations. It also introduces build-time/runtime “push enabled” gating via platform config and Android Gradle flags.

Changes:

  • Add bluedot_point_sdk_push dependency plus a push notifications page + in-app event logging.
  • Wire native (iOS/Android) config into Flutter via a MethodChannel (isPushEnabled) and add Android Gradle toggles for Google Services.
  • Refactor geo-triggering UI to include an on-screen event log backed by a singleton store/channel handler.

Reviewed changes

Copilot reviewed 15 out of 16 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
pubspec.yaml Adds push module dependency and overrides to align git-sourced SDK dependencies.
pubspec.lock Locks new git dependency refs for SDK + push module.
lib/main.dart Registers push notification listener (gated by Dart compile-time flag) and adds route to push notifications page.
lib/home_page.dart Adds conditional navigation button for push notifications based on native config.
lib/push_notifications_page.dart New UI to display received/clicked push notification events.
lib/helpers/push_notification_manager.dart New singleton + stream model for push event collection.
lib/helpers/constants.dart Adds PUSH_ENABLED compile-time constant (documented as build-time flag).
lib/helpers/app_config.dart New MethodChannel wrapper for reading native PUSH_ENABLED flag.
lib/geo_triggering_page.dart Refactors geo-triggering event handling to persist and display event logs.
ios/Runner/Info.plist Adds PUSH_ENABLED flag (default false).
ios/Runner/AppDelegate.swift Exposes isPushEnabled via FlutterMethodChannel.
android/gradle.properties Adds PUSH_ENABLED default and increases Gradle JVM args.
android/build.gradle Adds Google Services Gradle plugin classpath.
android/app/build.gradle Conditionally applies Google Services plugin and exposes PUSH_ENABLED to BuildConfig.
android/app/src/main/kotlin/io/bluedot/flutter_minimal_app/MainActivity.kt Exposes isPushEnabled via MethodChannel backed by BuildConfig.PUSH_ENABLED.
android/.gitignore Ignores google-services.json.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +17 to +23
// Snapshot the current list plus subscribe for live updates.
_events = List.from(PushNotificationManager.instance.events);
PushNotificationManager.instance.stream.listen((event) {
if (mounted) {
setState(() => _events.add(event));
}
});
Comment thread lib/main.dart
Comment on lines 11 to +40
@@ -23,6 +31,28 @@ class _MyAppState extends State<MyApp> {
super.initState();
// Request permissions for location and notification
_requestPermission();

// Register push notification listener when Firebase is enabled.
// In default mode the plugin's DefaultMessagingService automatically
// handles FCM token updates and message delivery — no extra wiring needed.
if (_pushEnabled) {
_setupPushListener();
}
Comment thread lib/home_page.dart
Comment on lines +46 to +50
AppConfig.isPushEnabled().then((value) {
setState(() {
_pushEnabled = value;
});
});
final bool result = await _channel.invokeMethod('isPushEnabled');
return result;
} on PlatformException {
return true; // default to enabled on error
Comment on lines +12 to +16
let controller = window?.rootViewController as! FlutterViewController
let configChannel = FlutterMethodChannel(
name: "io.bluedot.flutter_minimal_app/config",
binaryMessenger: controller.binaryMessenger
)
Comment thread android/gradle.properties
@@ -1,4 +1,5 @@
org.gradle.jvmargs=-Xmx1536M
org.gradle.jvmargs=-Xmx4096M -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError
PUSH_ENABLED=true
Comment thread android/app/build.gradle
Comment on lines +9 to +10

if (pushEnabled) {
Comment thread android/app/build.gradle
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName

// Expose flag to Android (BuildConfig) and Flutter (dart-define)
Comment on lines +7 to +8
/// Set in gradle.properties (or overridden by the PUSH_ENABLED env var).
/// Passed to Dart via --dart-define at build time.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants