Skip to content

feat(push): bridge open web URL actions to Dart#116

Open
morrilltim wants to merge 12 commits into
rel/0.3.0from
feature/open-web-url
Open

feat(push): bridge open web URL actions to Dart#116
morrilltim wants to merge 12 commits into
rel/0.3.0from
feature/open-web-url

Conversation

@morrilltim

@morrilltim morrilltim commented Jul 21, 2026

Copy link
Copy Markdown

Description

Ships the "open web URL" SDK bridge work for the 0.3.0 release: forwards open_url and push action button taps from the native SDKs to Dart via a typed onPushAction EventChannel stream.

Due Diligence

  • I have tested this on a simulator/emulator or a physical device, on iOS and Android (if applicable).
  • I have added sufficient unit/integration tests of my changes.
  • I have adjusted or added new test cases to team test docs, if applicable.
  • I am confident these changes are implemented with feature parity across iOS and Android (if applicable).

Release/Versioning Considerations

  • Patch Contains internal changes or backwards-compatible bug fixes.
  • Minor Contains changes to the public API.
  • Major Contains breaking changes.
  • Contains readme or migration guide changes.
    • Already merged to the feature/open-web-url feature branch per repo convention; this PR promotes that branch to rel/0.3.0.
  • This is planned work for an upcoming release.

Changelog / Code Overview

Consolidates PR #110 (PUSH-639) from the feature/open-web-url branch: KlaviyoPushAction model, onPushAction stream, and iOS/Android EventChannel forwarding for open_url + action button taps.

Note: this branch still pins native SDKs at KlaviyoSwift 5.3.1 / klaviyo-android-sdk 4.4.0. Bumping to the versions landing in the sibling iOS (rel/5.4.0) and Android (rel/4.5.0) open-web-url PRs — required for Android web_url support — is a tracked follow-up, not included here.

Test Plan

Verified end-to-end on Android emulator and physical iPhone against local native SDK builds (see PUSH-639 notes). This PR merges the feature branch as-is into rel/0.3.0.

Related Issues/Tickets

PUSH-639 — part of the "open web URL" project.

Summary by CodeRabbit

  • New Features

    • Added typed push action events for opened URLs and action-button taps.
    • Added support for handling push action details, including button IDs, labels, actions, and URLs.
    • Added support for special URL schemes and platform-specific push action behavior.
  • Documentation

    • Added usage examples and API reference details for push action events.
  • Chores

    • Updated native iOS and Android SDK versions.
    • Improved handling of push actions received before event listeners are ready.

morrilltim and others added 11 commits June 30, 2026 15:22
open_url taps are dispatched externally by the native SDK and never reach
the host app, so only deep_link/open_app action buttons are bridged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mirror the native SDK's klaviyoDeepLinkURL semantics: a url value that is
absent, not a string, or fails URL parsing does not take precedence over
web_url, so the bridge should still emit push_open_web_url.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review feedback: guard that the tapped button carries an action type
(Dart's fromMap requires it) instead of silently emitting an event the
Dart layer drops, log unmatched button identifiers, apply Swift if-let
shorthand, and remove a redundant comment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat(push): bridge open_url action and push action button events to Dart
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds typed push-action events for Klaviyo push notification taps and action buttons. Native iOS and Android plugins forward events through Flutter, Dart decodes them into sealed models, and the example, documentation, tests, and native SDK versions are updated.

Changes

Push action events

Layer / File(s) Summary
Typed action contract
lib/src/models/klaviyo_push_action.dart, lib/klaviyo_flutter_sdk.dart, test/klaviyo_push_action_test.dart
Defines KlaviyoPushAction, OpenWebUrl, and ActionButtonTapped with validation, equality, string formatting, and exhaustive parsing tests.
Native notification event bridge
ios/klaviyo_flutter_sdk/Sources/.../KlaviyoFlutterSdkPlugin.swift, android/src/main/kotlin/.../KlaviyoFlutterSdkPlugin.kt, ios/klaviyo_flutter_sdk.podspec, android/build.gradle, .gitignore
Forwards iOS notification interactions and Android action-button taps, caches iOS events until listening begins, and upgrades native SDK versions.
Dart stream decoding
lib/src/services/klaviyo_native_wrapper.dart, lib/src/klaviyo_sdk.dart, test/push_action_stream_test.dart
Adds raw native event buffering, filters supported event types, decodes typed actions, drops malformed events safely, and tests ordering and stream continuity.
Example and public documentation
example/lib/main.dart, example/lib/tabs/profile_tab.dart, README.md, CHANGELOG.md
Registers an example listener and documents onPushAction, event fields, supported actions, and iOS/Android behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PushNotification
  participant NativePlugin
  participant NativeWrapper
  participant KlaviyoSDK
  participant AppListener
  PushNotification->>NativePlugin: user taps notification or action button
  NativePlugin->>NativeWrapper: push action event payload
  NativeWrapper->>KlaviyoSDK: onPushActionEvent
  KlaviyoSDK->>KlaviyoSDK: filter and decode event
  KlaviyoSDK-->>AppListener: KlaviyoPushAction
Loading

Suggested labels: SDK Auto Integration

Suggested reviewers: ajaysubra

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: bridging push open web URL actions to Dart.
Description check ✅ Passed The description follows the template and covers the required sections with adequate implementation, testing, versioning, and issue context.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/open-web-url

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Checkov (3.3.8)
android/build.gradle

Traceback (most recent call last):
File "/usr/local/bin/checkov", line 2, in
from checkov.main import Checkov
ModuleNotFoundError: No module named 'checkov'


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 0e58931. Configure here.

Logger.klaviyoFlutterSDK.notice("Flutter not ready. Caching push action event.")
}
cachedPushAction = eventPayload
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale iOS push action cache

Medium Severity

When Flutter is not ready, forwardPushAction stores a push action in cachedPushAction but returns early when the latest tap has no typed action without clearing that cache. After a second notification open overwrites cachedOpenedNotification, onListen can deliver a mismatched push_open_web_url or button event from an earlier tap.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0e58931. Configure here.

@morrilltim

Copy link
Copy Markdown
Author

This PR will need the updated Android and iOS SDK's

@morrilltim
morrilltim marked this pull request as ready for review July 22, 2026 20:10
@morrilltim
morrilltim requested a review from a team as a code owner July 22, 2026 20:10
@klaviyoit
klaviyoit requested a review from ajaysubra July 22, 2026 20:10

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/src/klaviyo_sdk.dart`:
- Around line 440-447: Add the required trailing comma after the multiline
callback argument in the expand call handling KlaviyoPushAction.fromMap within
the event transformation chain, preserving the existing error handling and
behavior.

In `@lib/src/services/klaviyo_native_wrapper.dart`:
- Line 356: Update the native push action logging around the `_logger.info` call
so it no longer emits the raw `eventData` payload at INFO. Log only the event
type or button identifier, or redact URL fields; retain the full payload only
behind explicitly enabled debug logging.
- Around line 354-358: Update the native push-event flow around handleIntent()
and onListen() to queue push_notification_opened and push_action_button_tapped
payloads when eventSink is unavailable. Store them in a native pending queue,
then flush and clear that queue when onListen() initializes the stream listener,
preserving normal immediate delivery after subscription.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 33364b69-56b0-4545-b4fc-138b5104b813

📥 Commits

Reviewing files that changed from the base of the PR and between 149c257 and 0e58931.

📒 Files selected for processing (15)
  • .gitignore
  • CHANGELOG.md
  • README.md
  • android/build.gradle
  • android/src/main/kotlin/com/klaviyo/klaviyo_flutter_sdk/KlaviyoFlutterSdkPlugin.kt
  • example/lib/main.dart
  • example/lib/tabs/profile_tab.dart
  • ios/klaviyo_flutter_sdk.podspec
  • ios/klaviyo_flutter_sdk/Sources/klaviyo_flutter_sdk/KlaviyoFlutterSdkPlugin.swift
  • lib/klaviyo_flutter_sdk.dart
  • lib/src/klaviyo_sdk.dart
  • lib/src/models/klaviyo_push_action.dart
  • lib/src/services/klaviyo_native_wrapper.dart
  • test/klaviyo_push_action_test.dart
  • test/push_action_stream_test.dart

Comment thread lib/src/klaviyo_sdk.dart
Comment on lines +440 to +447
.expand((event) {
try {
return [KlaviyoPushAction.fromMap(event)];
} catch (e) {
_logger.warning('Dropping malformed push action event: $e');
return [];
}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate klaviyo_sdk.dart =="
fd -a 'klaviyo_sdk\.dart$' .

echo "== Extract relevant lines =="
if [ -f lib/src/klaviyo_sdk.dart ]; then
  sed -n '425,455p' lib/src/klaviyo_sdk.dart | cat -n
fi

echo "== Check require_trailing_commas config references =="
rg -n "require_trailing_commas|trailing_commas|avoid_trailing_commas|dart format" . -g '!build/**' -g '!dist/**' -g '!node_modules/**' || true

echo "== Check local analyzer/dart configuration files =="
for f in analysis_options.yaml pubspec.yaml pubspec.yaml.lock .dart_tool/package_config.json; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    sed -n '1,220p' "$f"
  fi
done

echo "== Extract surrounding stream chain with line numbers =="
nl -ba lib/src/klaviyo_sdk.dart | sed -n '420,455p'

Repository: klaviyo/klaviyo-flutter-sdk

Length of output: 4125


🌐 Web query:

Dart analyzer require_trailing_commas trailing comma lint Dart style

💡 Result:

The require_trailing_commas lint rule in Dart is designed to enforce the use of trailing commas in multi-line parameter and argument lists [1]. Key details regarding its usage and behavior: How to Enable: To enable this lint, add it to your analysis_options.yaml file under the linter rules section [1]: linter: rules: - require_trailing_commas Behavior and Limitations: - The rule encourages using trailing commas for all multi-line parameter and argument lists. If a list fits on a single line, a trailing comma is not required [1]. - It is intended to complement the Dart formatter (dart format), which also automatically manages trailing commas [1][2]. - NOTE: The rule assumes code has already been processed by dart format; it may produce false positives on unformatted code [1]. - For projects targeting Dart language version 3.7 or later (using the new "tall" style formatter), the require_trailing_commas lint rule is automatically disabled, as the formatter's automated behavior takes precedence [3]. Interaction with Formatter Configuration: In Dart 3.7 and later, the default formatter behavior is to automatically add or remove trailing commas based on whether a construct spans multiple lines [2][4]. Users who prefer manual control over line breaks—where the presence of a trailing comma forces a construct to split across lines—can opt into this behavior by configuring the formatter in analysis_options.yaml [2][5][6]: formatter: trailing_commas: preserve When this preserve setting is used, it restores the ability to force line breaks via trailing commas, which was a standard feature of the older Dart formatter [5][6]. Using the lint rule alongside this setting helps maintain consistent application of that style across the codebase [7].

Citations:


🌐 Web query:

Flutter 3.24 Flutter SDK Dart SDK version version

💡 Result:

The Flutter 3.24 SDK release is paired with Dart 3.5 [1][2]. This release was officially announced on August 6, 2024 [1][2].

Citations:


Add the required trailing comma to the multiline expand call.

The repository enforces require_trailing_commas, so keep the callable comma even though the argument happens to be a block expression:

Proposed formatting fix
-        .expand((event) {
+        .expand(
+          (event) {
           try {
             return [KlaviyoPushAction.fromMap(event)];
           } catch (e) {
             _logger.warning('Dropping malformed push action event: $e');
             return [];
           }
-      });
+          },
+        );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.expand((event) {
try {
return [KlaviyoPushAction.fromMap(event)];
} catch (e) {
_logger.warning('Dropping malformed push action event: $e');
return [];
}
});
.expand(
(event) {
try {
return [KlaviyoPushAction.fromMap(event)];
} catch (e) {
_logger.warning('Dropping malformed push action event: $e');
return [];
}
},
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/src/klaviyo_sdk.dart` around lines 440 - 447, Add the required trailing
comma after the multiline callback argument in the expand call handling
KlaviyoPushAction.fromMap within the event transformation chain, preserving the
existing error handling and behavior.

Source: Coding guidelines

Comment on lines +354 to +358
case 'push_open_web_url':
case 'push_action_button_tapped':
_logger.info('Native push action event: $eventData');
_pushActionController.add(eventData);
break;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -A20 -B8 \
  'forwardActionButtonTap|push_action_button_tapped|onListen|eventSink\?\.success' \
  android/src/main/kotlin/com/klaviyo/klaviyo_flutter_sdk/KlaviyoFlutterSdkPlugin.kt

Repository: klaviyo/klaviyo-flutter-sdk

Length of output: 7789


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Candidate files =="
git ls-files | rg '(^android/|^ios/|^lib/src/services/klaviyo_native_wrapper.dart|README|CHANGELOG|pubspec.yaml)' || true

echo
echo "== Android push click/action methods around source =="
if [ -f android/src/main/kotlin/com/klaviyo/klaviyo_flutter_sdk/KlaviyoFlutterSdkPlugin.kt ]; then
  rg -n -C12 'onNotificationOpened|onNotificationClick|Button ID|Button Action|forwardActionButtonTap|push_notification_opened|push_action_button_tapped' android/src/main/kotlin/com/klaviyo/klaviyo_flutter_sdk/KlaviyoFlutterSdkPlugin.kt
fi

echo
echo "== iOS candidate search for action-button cache behavior =="
for f in $(git ls-files | rg 'ios/.*\.(swift|m|m\.mm)$|ios/Pods/.*\.h$' 2>/dev/null || true); do
  if rg -q 'push_action_button_tapped|action.*button|Button Action|Button Link|deep_link|open_app|cache|pending'| "$f" 2>/dev/null; then
    echo "--- $f ---"
    rg -n -C8 'push_action_button_tapped|action.*button|Button Action|Button Link|deep_link|open_app|cache|pending|eventSink' "$f" 2>/dev/null || true
  fi
done

echo
echo "== Dart stream controller and event mapping =="
sed -n '1,460p' lib/src/services/klaviyo_native_wrapper.dart

Repository: klaviyo/klaviyo-flutter-sdk

Length of output: 3766


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Android listener registration and full onNotificationOpened =="
rg -n -C8 'Klaviyo\.|onNotificationOpened|KlaviyoNotificationOpened|KlaviyoNotificationAction|deepLink|Button|onActivityResult|ActivityCompat|notification' android/src/main/kotlin/com/klaviyo/klaviyo_flutter_sdk/KlaviyoFlutterSdkPlugin.kt

echo
echo "== iOS implementation relevant to notification taps/actions =="
if [ -f ios/klaviyo_flutter_sdk/Sources/klaviyo_flutter_sdk/KlaviyoFlutterSdkPlugin.swift ]; then
  sed -n '1,260p' ios/klaviyo_flutter_sdk/Sources/klaviyo_flutter_sdk/KlaviyoFlutterSdkPlugin.swift
fi

echo
echo "== lib/dart stream setup =="
sed -n '1,220p' lib/src/services/klaviyo_native_wrapper.dart

Repository: klaviyo/klaviyo-flutter-sdk

Length of output: 36139


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Android ActivityAware full implementation =="
sed -n '530,576p' android/src/main/kotlin/com/klaviyo/klaviyo_flutter_sdk/KlaviyoFlutterSdkPlugin.kt

echo
echo "== BufferedBroadcastStreamController behavior =="
if [ -f lib/src/utils/buffered_broadcast_stream_controller.dart ]; then
  sed -n '1,220p' lib/src/utils/buffered_broadcast_stream_controller.dart
fi

Repository: klaviyo/klaviyo-flutter-sdk

Length of output: 2246


Cache Android push action events before the EventChannel listener subscribes.

handleIntent() runs during Android cold-start, but eventSink is only initialized from onListen(). Both the push_notification_opened and push_action_button_tapped payloads are discarded when eventSink is null, while the Android cold-start flow can precede the Dart EventChannel subscription. Retain these native events in a native pending queue and flush them when the stream listener is added instead of relying only on the Dart buffer.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/src/services/klaviyo_native_wrapper.dart` around lines 354 - 358, Update
the native push-event flow around handleIntent() and onListen() to queue
push_notification_opened and push_action_button_tapped payloads when eventSink
is unavailable. Store them in a native pending queue, then flush and clear that
queue when onListen() initializes the stream listener, preserving normal
immediate delivery after subscription.

break;
case 'push_open_web_url':
case 'push_action_button_tapped':
_logger.info('Native push action event: $eventData');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Avoid logging raw push-action URLs at INFO.

eventData contains URLs verbatim, including schemes and query strings that may carry PII or tokens. Log only the event type/button identifier, or redact URL fields and keep the full payload behind opt-in debug logging.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/src/services/klaviyo_native_wrapper.dart` at line 356, Update the native
push action logging around the `_logger.info` call so it no longer emits the raw
`eventData` payload at INFO. Log only the event type or button identifier, or
redact URL fields; retain the full payload only behind explicitly enabled debug
logging.

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.

1 participant