Skip to content

feat: add amplify_push package#6985

Open
ekjotmultani wants to merge 3 commits into
mainfrom
feat/amplify-push-v3
Open

feat: add amplify_push package#6985
ekjotmultani wants to merge 3 commits into
mainfrom
feat/amplify-push-v3

Conversation

@ekjotmultani

@ekjotmultani ekjotmultani commented May 19, 2026

Copy link
Copy Markdown
Member

Summary

New Flutter plugin package amplify_push for standalone on-device push notification handling, decoupled from Pinpoint.

This PR contains the package implementation only. To keep reviews scoped, the rest is split into stacked PRs based on this branch:

What is this?

A backend-agnostic push notifications client that handles:

  • Device token lifecycle (FCM/APNs)
  • Foreground notification routing
  • Background notification processing (Android isolate, iOS callback)
  • Tap/open notification handling + launch notification detection
  • Permission request/status APIs
  • Badge count (iOS)

No Amplify.configure() required. No AWS service dependency. Works standalone or with an optional PushServiceProvider for backend wiring (Connect client integration coming in Phase 2).

Design

  • Standalone: AmplifyPushClient.create() static factory — no Amplify plugin lifecycle
  • Result types: Uses Result<T> from amplify_foundation_dart
  • Sealed exceptions: AmplifyPushException hierarchy extending foundation AmplifyException
  • Optional provider: PushServiceProvider interface with onTokenReceived and onPushEvent callbacks
  • Native code: Ported from amplify_push_notifications (same platform channel bridge, backend-agnostic)
  • Android lifecycle: depends on flutter_plugin_android_lifecycle (the plugin resolves the activity lifecycle via FlutterLifecycleAdapter)

Usage

// Standalone — no backend
final result = await AmplifyPushClient.create(
  onBackgroundMessage: myHandler,
);

// With a provider (e.g., Connect)
final result = await AmplifyPushClient.create(
  provider: connectClient.asPushProvider(),
  onBackgroundMessage: myHandler,
);

Verification


Known follow-ups / not-yet-verified

  • Android background-isolate path: needs on-device testing to confirm the background processor signals completion correctly when no Amplify re-init is performed
  • iOS AmplifyUtilsNotifications: pod resolves and builds (verified via the example app), runtime behavior on device not yet tested
  • No Connect provider wired yet (Phase 2 — depends on Connect client shipping)
  • Native code package namespaces: still use amplify_push_notifications internally (cosmetic, channel names must match)

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@ekjotmultani ekjotmultani changed the title feat: add amplify_push package — standalone on-device push notifications feat: add amplify_push package May 19, 2026
@codecov-commenter

codecov-commenter commented May 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 43.23%. Comparing base (5d896ca) to head (623f8af).
⚠️ Report is 51 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6985      +/-   ##
==========================================
+ Coverage   43.22%   43.23%   +0.01%     
==========================================
  Files          99       99              
  Lines        7769     7769              
  Branches     3400     3401       +1     
==========================================
+ Hits         3358     3359       +1     
+ Misses       4411     4410       -1     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ekjotmultani ekjotmultani changed the title feat: add amplify_push package feat: add amplify_push package — standalone on-device push notifications Jun 17, 2026
@ekjotmultani ekjotmultani force-pushed the feat/amplify-push-v3 branch 3 times, most recently from b28b8ff to d479ea0 Compare June 29, 2026 19:48
@ekjotmultani ekjotmultani force-pushed the feat/amplify-push-v3 branch from d479ea0 to 9e9a51c Compare June 29, 2026 20:15
@ekjotmultani ekjotmultani force-pushed the feat/amplify-push-v3 branch 2 times, most recently from 5dbc37a to 763af62 Compare June 30, 2026 17:13
Standalone, backend-agnostic on-device push handling (token lifecycle, display,
deeplink routing, permissions, background processing, badge) built on Amplify
Foundation. Depends on flutter_plugin_android_lifecycle for the activity lifecycle.
@ekjotmultani ekjotmultani force-pushed the feat/amplify-push-v3 branch from 763af62 to fa2f10f Compare June 30, 2026 17:18
@ekjotmultani ekjotmultani marked this pull request as ready for review June 30, 2026 19:24
@ekjotmultani ekjotmultani requested a review from a team as a code owner June 30, 2026 19:24
@ekjotmultani ekjotmultani changed the title feat: add amplify_push package — standalone on-device push notifications feat: add amplify_push package Jun 30, 2026
- Fix close() to cancel all provider stream subscriptions
- Expose token stream as broadcast (supports multiple listeners)
- Add create() idempotency guard (PushAlreadyConfiguredException)
- Deduplicate initial token dispatch to provider
- Fix APNs deeplink parser to use backend-neutral data key
- Add ==, hashCode, toString to all value types
- Replace Kotlin !! with null-safe guards for Firebase race safety
- Use fixed JOB_ID constant for background service
- Align podspec version with pubspec (0.1.0)
- Restore warning log on missing callback dispatcher
- Remove unused async dependency
@ekjotmultani ekjotmultani force-pushed the feat/amplify-push-v3 branch from d6e3ec7 to 48cb67a Compare June 30, 2026 22:35

@cadivus cadivus left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One general note: some of the doc comments (the class template and the create() docs especially) run pretty long. Trimming them to a line or two would make them easier to scan — not blocking.

///
/// Cancels all provider subscriptions, clears background callbacks, and
/// disconnects the provider. The client cannot be reused after closing.
Future<void> close() async {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The broadcast controller behind onTokenReceived never gets closed here. onDone: tokenController.close only fires if the source stream completes — cancelling the subscription won't trigger it. Worth holding a ref to the controller and calling await _tokenController.close() in here.

///
/// Returns `null` if the app was launched by other means.
/// Consumed on first read (subsequent reads return `null`).
PushNotificationMessage? get launchNotification {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This getter nullifies the field as a side effect of reading it. It's documented, but a getter that mutates state is easy to trip over. Something like consumeLaunchNotification() would read more honestly.

@cadivus cadivus left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Went over the native side too. Generated files (Pigeon .m/.h/.java/.g.dart) aside, a couple of Android things inline.

One minor iOS nit: in AmplifyPushNotificationsPlugin.swift the badge getter/setter use applicationIconBadgeNumber (lines ~95 and ~99), which is deprecated as of iOS 17 in favor of UNUserNotificationCenter.setBadgeCount(_:). Not urgent, just expect deprecation warnings.


override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
mainBinaryMessenger = null
_flutterEngineCache.clear()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

FlutterEngineCache.getInstance() is a process-wide singleton, so clear() drops every cached engine in the app — including this plugin's background engine and anything other plugins or an add-to-app host put there. Safer to remove just our own key: _flutterEngineCache.remove(FLUTTER_ENGINE_ID).


if (res is PermissionRequestResult.Granted) {
result.success(true)
} else if (res is PermissionRequestResult.NotGranted){

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This handles Granted and NotGranted but has no terminal else. If requestPermission() ever returns another result (or null), result.success(...) never fires and the Dart future hangs forever. Worth a fallback result.success(false) to be safe.

- close() now closes the buffered token stream in addition to cancelling the
  provider stream subscriptions
- rename launchNotification getter to consumeLaunchNotification() since reading
  it mutates state
- add onError handlers to the provider token, foreground, and opened stream
  listeners so native error events are logged rather than unhandled
- Android: on engine detach, remove only this plugin's engine cache keys
  instead of clearing the process-wide cache
- Android: handle permission results with an exhaustive when plus a terminal
  else, and surface exceptions via result.error so the Dart future always
  completes
- iOS: migrate badge setting to UNUserNotificationCenter.setBadgeCount with an
  availability guard and a fallback to applicationIconBadgeNumber for iOS < 16
- trim the client doc comments
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.

3 participants