Skip to content

docs(readme): update for 0.2.0 (Android 4.4.0 / iOS 5.3.0)#90

Merged
ab1470 merged 2 commits into
rel/0.2.0from
ab/MAGE-454/readme-0.2.0-updates
May 4, 2026
Merged

docs(readme): update for 0.2.0 (Android 4.4.0 / iOS 5.3.0)#90
ab1470 merged 2 commits into
rel/0.2.0from
ab/MAGE-454/readme-0.2.0-updates

Conversation

@ab1470

@ab1470 ab1470 commented May 1, 2026

Copy link
Copy Markdown
Contributor

Description

Update the README to document the new features that landed in the underlying native SDK releases consumed by Flutter 0.2.0 — Android 4.4.0 and iOS 5.3.0. Tracks MAGE-454.

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).

Docs-only change. No tests. The new push action buttons and floating/flyout form layouts work via the underlying native SDKs without any Flutter API surface, so iOS/Android parity is inherited.

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.
    • Targeting rel/0.2.0 so the README updates only go live with the official 0.2.0 release.
  • This is planned work for an upcoming release.

Changelog / Code Overview

Native release note Flutter README treatment
Push action buttons (Android #440/#442, iOS #502) New Action Buttons subsection under Push Notifications — no integration code required
Floating forms (Android #441) / Flyout forms (iOS #535) Inline callout in In-App Forms intro
Form lifecycle hooks (Android #434, iOS #539) New Form Lifecycle Events subsection + Models entry, replacing the older raw `onFormEvent` example with a typed `onFormLifecycleEvent` pattern-match example
`data-klaviyo-device` / safe-area / `addSafeAreaInsetsToOffsets` (Android #443, iOS #560) Skipped — internal to native webview rendering, no Flutter API surface
`content-available` docs (iOS) Already covered in existing Silent Push section

Specific edits:

  • `README.md`: TOC entries for Action Buttons and Form Lifecycle Events
  • `README.md`: New Action Buttons subsection under Push Notifications
  • `README.md`: In-App Forms intro paragraph notes modal/full-screen/floating/flyout support
  • `README.md`: New Form Lifecycle Events subsection with typed `onFormLifecycleEvent` example
  • `README.md`: API Reference Properties row swapped `onFormEvent` → `onFormLifecycleEvent`
  • `README.md`: Added `FormLifecycleEvent` sealed-class entry under Models

The raw `onFormEvent` stream is still exported from the SDK for forward-compat and debugging, but is no longer surfaced in user-facing docs.

Test Plan

  • Render the README on GitHub and verify TOC anchors (`#action-buttons`, `#form-lifecycle-events`) jump to the right sections
  • Confirm the typed `onFormLifecycleEvent` example compiles in the example app
  • Visually scan for broken anchor links and ensure deep-link callouts cross-reference correctly

Related Issues/Tickets

- Add Action Buttons subsection under Push Notifications
- Note floating/flyout form layouts in In-App Forms intro
- Replace raw onFormEvent example with typed onFormLifecycleEvent
- Add FormLifecycleEvent types to API Reference
@ab1470
ab1470 marked this pull request as ready for review May 1, 2026 22:06
@ab1470
ab1470 requested a review from a team as a code owner May 1, 2026 22:06
@klaviyoit
klaviyoit requested a review from belleklaviyo May 1, 2026 22:06

@ajaysubra ajaysubra 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.

LGTM. @belleklaviyo anything to add for flyout forms and @aahiltn anything to add for content-available?

Comment thread README.md Outdated
@ab1470
ab1470 requested a review from belleklaviyo May 4, 2026 15:17
@ab1470
ab1470 merged commit c07c94b into rel/0.2.0 May 4, 2026
12 checks passed
@ab1470
ab1470 deleted the ab/MAGE-454/readme-0.2.0-updates branch May 4, 2026 15:31
@ab1470 ab1470 mentioned this pull request May 4, 2026
12 tasks
ab1470 added a commit that referenced this pull request May 5, 2026
* chore: bump version to 0.2.0 and remove interactive confirm from bump script

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: allow re-initialization with a new API key (#80)

* Fix local SDK setup script and bump AGP version for compatibility (#76)

* fix: allow re-initialization with a new API key

Remove the early-return guards from both KlaviyoSDK.initialize() and
KlaviyoNativeWrapper.initialize() so that calling initialize() again
with a different API key forwards the new key to the native layer,
matching Android and iOS SDK behavior.

The event channel listener is still set up only once to avoid duplicate
events.

Closes MAGE-507

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix(review): move _apiKey assignment after native call, eagerly init wrapper

Address review findings:
- _apiKey is now set only after the native initialize() succeeds, so a
  failed re-init preserves the previous key instead of corrupting state
- _nativeWrapper is now a final field (eagerly constructed singleton)
  instead of being re-assigned on every initialize() call
- Added test for failed re-initialization preserving previous apiKey

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix: dart format test file

-Claude

* fix(bugbot): separate event listener flag, await async test assertion

Two BugBot findings:
- Use dedicated _eventListenerAttached flag instead of _isInitialized
  to guard event channel subscription, so a failed first init doesn't
  leave the flag unset and cause duplicate subscriptions on retry
- Fix failed-reinit test to use `await expectLater(future, throwsA(...))`
  instead of the unawaited `expect(() => future, throwsA(...))` which
  passed trivially without validating the actual error behavior

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* feat: Add form lifecycle event bridge for in-app forms (#73)

* feat: add form lifecycle event bridge for in-app forms

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: configure deps for form lifecycle hooks testing

iOS SDK: feat/form-lifecycle-hooks branch
Android SDK: 0a48d79980dc3f276422cf9c42ff145eb39f364a

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: use camelCase for event names across bridge and Dart

Align with RN SDK and iOS native eventName convention.
Bridge wire format: formShown, formDismissed, formCtaClicked.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(forms): validate required bridge message fields (MAGE-484) (#78)

* fix(forms): throw on missing required fields in bridge message parsing

FormLifecycleEvent.fromMap now validates that required fields (formId,
formName, event type, and buttonLabel for CTA events) are present and
non-empty, throwing ArgumentError instead of silently falling back to
empty strings. The call site in KlaviyoSdk wraps parsing in try/catch
to log and drop malformed events rather than crashing the stream.

Also makes deepLinkUrl nullable on FormCtaClicked since it is genuinely
optional — a CTA may not have a deep link configured.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: use expand() for cleaner null-free stream filtering

Replace map+where+cast chain with expand() which naturally emits 0 or 1
elements, eliminating the nullable intermediate type. Also fix test using
empty formName in equality assertion for consistency with validation rules.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: unregister form lifecycle handler on engine detach

Addresses Dan's review feedback on PR #75 about cleaning up the
lifecycle handler when the Flutter app stops. On Android, we now
unregister in onDetachedFromEngine; on iOS, in onCancel (stream
handler teardown). Both are best-effort to avoid crashing during
plugin teardown.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(forms): revert deepLinkUrl to required — native SDKs only dispatch CTA events when URL exists

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* revert: remove unnecessary lifecycle handler cleanup on engine detach

The unregisterFormLifecycleHandler() calls in onDetachedFromEngine (Android)
and onCancel (iOS) are unnecessary — when the engine detaches the event sink
is already null so lifecycle events would no-op. Reaching into the native SDK
during teardown is fragile and not standard Flutter plugin cleanup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(forms): allow empty buttonLabel on CTA events

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* docs(forms): remove internal implementation details from public API docs

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix(forms): default buttonLabel to empty string instead of rejecting null

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(forms): dispatch eventSink on main thread, unregister handler on detach

- Android: wrap eventSink.success() in Handler(Looper.getMainLooper()).post {}
  to guarantee EventChannel calls happen on the UI thread
- Android: call Klaviyo.unregisterFormLifecycleHandler() in onDetachedFromEngine
  to prevent plugin instance being kept alive by the Klaviyo singleton
- iOS: wrap eventSink call in DispatchQueue.main.async for thread safety

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix(example): guard lifecycle subscription on SDK initialization

* fix(sdk): eagerly initialize native wrapper to allow pre-init stream subscription

KlaviyoNativeWrapper is a safe singleton — its constructor only creates
stream controllers, not platform channel calls. Making _nativeWrapper a
final field instead of late means onFormLifecycleEvent can be subscribed
before initialize() is called, matching native SDK behavior on Android/iOS.

Reverts unnecessary isInitialized guard added to example app.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix(forms): guard unregisterFormLifecycleHandler in onDetachedFromEngine

* fix(forms): address review findings for form lifecycle hooks

- Prevent duplicate handler registration by unregistering before re-registering
  on both Android and iOS
- Narrow catch-all in onDetachedFromEngine to catch MissingKlaviyoModule
  specifically, with a warning log for unexpected exceptions
- Allow empty formName (only reject null, matching buttonLabel treatment)
- Validate outer 'type' field in FormLifecycleEvent.fromMap
- Clarify docs on buttonLabel default and deepLinkUrl presence
- Add stream integration tests for filter, error-drop, and ordering

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix(forms): add logging to MissingKlaviyoModule catches matching geofencing pattern

Add Registry.log calls to all forms-related MissingKlaviyoModule catches
for consistency with the geofencing error handling pattern. User-facing
errors log at error level; cleanup/teardown paths log at verbose level.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix(forms): remove unnecessary nested try-catch around unregisterFormLifecycleHandler

The outer catch already handles MissingKlaviyoModule, and registerForInAppForms
is called before the unregister—so if forms is missing, we never reach this code.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(forms): wrap subscribeToFormLifecycleEvents in canImport guard

The entire method body needs the #if canImport(KlaviyoForms) guard since
registerFormLifecycleHandler and unregisterFormLifecycleHandler are symbols
from KlaviyoForms that must be stripped by the compiler when excluded.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: target native SDK release branches (Android rel/4.4.0, iOS rel/5.3.0)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(forms): guard Logger call with iOS 14 availability check

The Logger.klaviyoFlutterSDK extension is @available(iOS 14.0, *), so
the call in the #else branch of subscribeToFormLifecycleEvents needs
a matching if #available guard to match all other call sites in the file.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(android): emit push_notification_opened for tapped Klaviyo pushes (#83)

* fix(example): defer notification permission to Push tab (#84)

The example app's _setupFCM ran firebase_messaging.requestPermission()
at app launch, prompting the user for the Android POST_NOTIFICATIONS
permission before they had a chance to initialize the SDK with their
API key.

The Push tab already requests permission via permission_handler at
the right time (when the user taps Enable Push Notifications), so
the call in _setupFCM was redundant. Both APIs target the same
POST_NOTIFICATIONS permission on Android 13+. Token-refresh and
foreground-message listeners are unaffected — they don't depend on
permission state.

Fixes MAGE-547.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(android): emit push_notification_opened for tapped Klaviyo pushes

The Klaviyo Android SDK namespaces push-notification extras with
`com.klaviyo.` when building the tap PendingIntent
(`KlaviyoRemoteMessage.appendKlaviyoExtras`), but `handleIntent`
was checking `extras.containsKey("_k")` — the unprefixed key —
so the check always failed and the event was dropped silently.

Switch the guard to look for `com.klaviyo._k`, and strip the
prefix from keys forwarded to Dart so the payload matches the
iOS `userInfo` shape. Fixes MAGE-512.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(android): use Constants.PACKAGE_PREFIX / TRACKING_PARAMETER

Replace inlined "com.klaviyo." / "_k" literals in handleIntent with
references to com.klaviyo.core.Constants, so the Flutter plugin
tracks the source of truth in the core SDK instead of duplicating
it. Addresses PR #83 review feedback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(android): use Klaviyo.isKlaviyoNotificationIntent for guard

Replace the manual Constants.PACKAGE_PREFIX + Constants.TRACKING_PARAMETER
composition in handleIntent with the SDK's public extension
Klaviyo.isKlaviyoNotificationIntent, which performs the same check
and is the documented entry point for "did this intent originate
from a Klaviyo push?". The prefix-strip loop still uses
Constants.PACKAGE_PREFIX since there's no equivalent SDK helper
for stripping keys.

Addresses PR #83 review feedback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: consume KlaviyoSwift ~> 5.3.1 and Android 4.4.0 (#88)

* consume Swift 5.3.0 and Android 4.4.0

* chore(ios): bump KlaviyoSwift dependency to ~> 5.3.1

5.3.0 has an upstream issue (MAGE-565): KlaviyoSwiftExtension 5.3.0
depends on KlaviyoCore 5.3.0, which uses UIApplication.shared in
DeepLinkHandler.swift — that symbol is unavailable in app-extension
contexts, so the example app's NotificationServiceExtension target
fails to compile. The iOS team is publishing a 5.3.1 patch release
to fix this; pin to ~> 5.3.1 now so we pick it up the moment it lands.

iOS build verification deferred until 5.3.1 is published. Android
side (klaviyoSdkVersion = "4.4.0") was already verified.

References MAGE-454, MAGE-565.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(scripts): peg example app version to plugin version (#89)

* chore(scripts): peg example app version to plugin version

Extend sync_version.sh to also write the plugin version into
example/pubspec.yaml (preserving any existing +<buildnumber> suffix), and
sync the current value to 0.2.0+1. The pre-commit sync-sdk-version hook
already runs this script on root pubspec.yaml changes, so future bumps
stay in sync automatically.

* chore(scripts): also sync MARKETING_VERSION in example pbxproj

The RunnerTests and NotificationServiceExtension targets have hardcoded
MARKETING_VERSION values in project.pbxproj (their xcconfig base is the
CocoaPods-generated Pods-*.xcconfig, which doesn't include
Generated.xcconfig, so they can't reference $(FLUTTER_BUILD_NAME)).
Sed-update those literal numeric values to the plugin version. The main
Runner target is unchanged — it reads $(FLUTTER_BUILD_NAME) via
Info.plist as before.

* fix(scripts): support pre-release versions in MARKETING_VERSION sync

The previous character class [0-9.] only matched purely numeric versions,
so once a pre-release value like 0.3.0-alpha.1 landed in the pbxproj
(via bump_version.sh which supports semver pre-release tags), subsequent
syncs couldn't find the value to replace and it'd go stale. Broaden to
[0-9A-Za-z.-] so SemVer pre-release identifiers round-trip correctly.
The class still excludes $/(/)/quotes, so $(FLUTTER_BUILD_NAME) refs on
the main Runner target remain untouched.

* fix(scripts,ci): strip build suffix from root version, extend drift check

- sync_version.sh: strip any +<buildnumber> suffix from the root
  pubspec.yaml version up front. The plugin convention is to omit it,
  but if present we'd reassemble "$VERSION+$BUILD_NUMBER" into a
  malformed "0.3.0+2+1" when writing example/pubspec.yaml. Pre-release
  identifiers like 0.3.0-alpha.1 are preserved.

- version-check.yml: also diff example/pubspec.yaml and
  example/ios/Runner.xcodeproj/project.pbxproj after running the sync
  script. Without this, drift in either file would slip past CI if
  someone bypasses the pre-commit hook with --no-verify.

* chore(example): bind Runner target MARKETING_VERSION to FLUTTER_BUILD_NAME

The main Runner target had no MARKETING_VERSION build setting, so
Xcode's General → Identity Version field rendered blank even though
Info.plist's CFBundleShortVersionString correctly resolved to
$(FLUTTER_BUILD_NAME) at build time. The Identity panel binds to the
build setting specifically, not Info.plist.

Add MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)"; to all three Runner
configs (Debug, Release, Profile). FLUTTER_BUILD_NAME is in scope via
Generated.xcconfig, included by Debug.xcconfig and Release.xcconfig.
The sync_version.sh regex deliberately excludes $/(/)/quotes, so these
dynamic references are not rewritten — they auto-resolve from
pubspec.yaml on every Flutter build.

* added privacy descriptions

* added app description

* chore(scripts): drop build-number suffix from example pubspec

The example app is publish_to: 'none' and never ships to a store, so the
+1 build number serves no purpose — Flutter defaults FLUTTER_BUILD_NUMBER
to 1 when omitted. Drop it for consistency with the plugin's pubspec
(which has no suffix) and simplify sync_version.sh by removing the
build-number preservation branch.

* docs(agents): note new sync_version.sh targets in gotcha

This PR expanded the script to also write example/pubspec.yaml and the
example app's pbxproj MARKETING_VERSION entries, but the gotcha entry
still only mentioned the iOS plist. CLAUDE.md is a symlink to AGENTS.md
so it picks up the change automatically.

* docs(readme): update for 0.2.0 (Android 4.4.0 / iOS 5.3.0) (#90)

* docs(readme): update for 0.2.0 (Android 4.4.0 / iOS 5.3.0)

- Add Action Buttons subsection under Push Notifications
- Note floating/flyout form layouts in In-App Forms intro
- Replace raw onFormEvent example with typed onFormLifecycleEvent
- Add FormLifecycleEvent types to API Reference

* small readme update

* docs(changelog): fill in 0.2.0 entry (#92)

Replace the TODO placeholder with the full 0.2.0 changelog mirroring
the GitHub release notes — What's New, Bug Fixes, Documentation, and
updated Platform Support pinned to KlaviyoSwift 5.3.1 / Android 4.4.0.

* fix(scripts): verify newly synced files in bump_version.sh (#93)

`sync_version.sh` was extended in #89 to also sync the version into
`example/pubspec.yaml` and the hardcoded `MARKETING_VERSION` entries
in `example/ios/Runner.xcodeproj/project.pbxproj`, but the
`check_file_version` block in `bump_version.sh` was not updated to
verify those files. A silent sed-pattern mismatch in `sync_version.sh`
would leave those files stale while `bump_version.sh` still reported
"Version successfully bumped".

- Add `check_file_version` for `example/pubspec.yaml`
- Add a strict count check for `MARKETING_VERSION` entries in the
  example pbxproj — every hardcoded entry must match the new version.
  The dynamic `\$(FLUTTER_BUILD_NAME)` reference on the main Runner
  target is excluded by the same regex used in the sync script.

Verified positive (NEW_VERSION=0.2.0 → 6/6 entries pass) and negative
(NEW_VERSION=0.3.0 → 0/6 fails with ERRORS=1) cases.

Addresses cursor[bot] review on #77.

* fix(forms): tighten form lifecycle event handling for cross-platform parity (#94)

* fix(forms): reject empty formName in FormLifecycleEvent.fromMap

The `formName` validation was internally inconsistent with the rest
of the parser — `event`, `formId`, and `deepLinkUrl` all reject both
null and empty strings, but `formName` was only checking null. Tighten
it to match.

- `lib/src/models/form_lifecycle_event.dart`: extend the `formName`
  guard to include `formName.isEmpty`
- `test/form_lifecycle_event_test.dart`: replace the
  `parses empty formName` case with `throws on empty formName`,
  mirroring the existing `throws on empty formId` pattern

Addresses cursor[bot] review on #77.

* fix(ios): use explicit switch mapping for form lifecycle event names

The iOS form lifecycle handler was relying on the native Swift SDK's
`event.eventName` property for the event-type bridge string, while
the Android plugin explicitly mapped each subtype. The Dart
`FormLifecycleEvent.fromMap` parser only accepts the literal strings
`"formShown"`, `"formDismissed"`, and `"formCtaClicked"`, so any
future change to the native `eventName` property would silently drop
events on iOS (logged + swallowed by the stream's `expand` pipeline).

Replace the property access with an explicit `switch event` that
mirrors Android's `when (event)` block. The wrapper now owns its
serialization contract end-to-end:

- Cross-platform parity is enforced at the wrapper layer rather than
  inherited from a native API surface that wasn't designed for it
- Adding a new event type natively becomes a Swift compile error
  (non-exhaustive `switch`) instead of a silent runtime drop
- The `formCtaClicked` payload extraction (buttonLabel, deepLinkUrl)
  collapses into the same `switch` rather than a separate
  `if case let` afterward

Addresses cursor[bot] review on #77.

* docs(forms): add form lifecycle events section to README

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Andrew Balmer <ab1470@users.noreply.github.com>
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