Version 5.3.0#547
Conversation
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 0d378e7. Configure here.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat(forms): add form lifecycle event hooks (#539) Add registerFormLifecycleHandler public API on KlaviyoSDK exposing a FormLifecycleEvent enum (formShown, formDismissed, formCtaClicked) so host apps can observe in-app form lifecycle, matching the Android SDK. Event identity flows directly from bridge messages, fixing a stale-name bug where the form version's internal name was reported instead of the marketer-visible name. Includes bridge-message field validation (MAGE-484).
| case .formCtaClicked: return "form_cta_clicked" | ||
| case .formShown: return "formShown" | ||
| case .formDismissed: return "formDismissed" | ||
| case .formCtaClicked: return "formCtaClicked" |
There was a problem hiding this comment.
Public eventName strings silently changed to camelCase
Low Severity
The public eventName property values changed from snake_case (form_shown, form_dismissed, form_cta_clicked) to camelCase (formShown, formDismissed, formCtaClicked). Unlike the String? → String type changes that produce compile-time errors, this is a silent runtime behavior change. Any existing code comparing eventName against the old string values (e.g., for analytics routing or dashboard tracking) will silently stop matching without any compiler warning.
Reviewed by Cursor Bugbot for commit 082f13c. Configure here.
* Implement dynamic push action buttons (OneSignal-style)
This commit implements a hybrid approach for push notification action buttons,
supporting both dynamic (unlimited customization) and predefined (fallback) categories.
## What's New
### Dynamic Action Buttons (Primary)
- **Fully customizable button labels** per notification (e.g., "Shop Now", "Add to Cart")
- **No manual registration required** - categories registered automatically in NSE
- **Per-button deep links** - each button can have its own destination URL
- **iOS 15+ icon support** - SF Symbols on buttons using NSInvocation reflection
- **Localization-ready** - button labels come from server payload
- **FIFO category pruning** - maintains 128 category limit automatically
### How It Works
1. Push arrives with `action_buttons` array in payload
2. NSE intercepts, parses buttons, generates unique category ID
3. Registers category dynamically before notification displays
4. User taps button → tracks `$opened_push_action` event with action_id
### Implementation Details
**New Files:**
- `Sources/KlaviyoSwiftExtension/KlaviyoCategoryController.swift`
- Manages dynamic category lifecycle (registration, persistence, pruning)
- Uses app group UserDefaults for persistence
- FIFO pruning at 128 categories
- Smart merge to avoid overwriting existing categories
- `Sources/KlaviyoSwiftExtension/KlaviyoActionButtonParser.swift`
- Parses action button definitions from push payload
- Creates UNNotificationAction instances
- Handles button reversal (2-button iOS convention)
- iOS 15+ icon support via NSInvocation reflection
- `PUSH_ACTION_BUTTONS_PAYLOAD_SPEC.md`
- Comprehensive payload specification for backend team
- Validation rules and examples
- Migration guide from predefined to dynamic
- Troubleshooting guide
**Modified Files:**
- `Sources/KlaviyoSwiftExtension/KlaviyoExtension.swift`
- Added `handleActionButtons()` method
- Integrates dynamic category registration into NSE flow
- Respects developer-set categories (no overwriting)
- `Sources/KlaviyoSwift/Utilities/UNNotificationResponse+Klaviyo.swift`
- Added `isActionButtonTap`, `actionButtonId`, `actionButtonURL`, `actionButtonLabel`
- Supports both dynamic and predefined payload formats
- Extracts action-specific deep links and labels for analytics
- `Sources/KlaviyoSwift/Klaviyo.swift`
- Updated notification response handler to detect action button taps
- Added `handleActionButtonTap()` method
- Tracks `$opened_push_action` events with action metadata
- `Sources/KlaviyoSwift/Models/Event.swift`
- Added `_openedPushAction` event type
- Maps to `$opened_push_action` in Klaviyo
### Payload Format
**Dynamic Buttons (Recommended):**
```json
{
"aps": {
"alert": "Flash Sale - 50% Off!",
"mutable-content": 1
},
"body": {
"_k": "unique_notification_id",
"url": "myapp://home",
"action_buttons": [
{
"id": "com.klaviyo.action.shop",
"label": "Shop Now",
"url": "myapp://sale/flash",
"icon": "cart.fill"
},
{
"id": "com.klaviyo.action.later",
"label": "Remind Later",
"url": "myapp://reminders"
}
]
}
}
```
**Predefined Categories (Fallback):**
- Existing predefined categories remain unchanged
- No breaking changes to current implementations
### Event Tracking
**Action Button Tap:**
- Event: `$opened_push_action`
- Properties: `action_id`, `action_label` (dynamic only), all notification properties
- Allows filtering by button type and A/B testing button labels
### Benefits Over Predefined Approach
✅ **Unlimited customization** - Any button labels, not limited to 4 combinations
✅ **Better for ecommerce** - Brand-appropriate labels ("Shop Now" vs generic "View")
✅ **Localization** - Server sends translated labels per user
✅ **A/B testing** - Test different button copy easily
✅ **Better UX** - No registration call needed from app developers
✅ **Production-proven** - OneSignal uses this approach at massive scale
### Backwards Compatibility
- Predefined categories still work unchanged
- Apps can use both approaches simultaneously
- Dynamic is primary, predefined is fallback (when NSE unavailable)
### Technical Approach
Based on research of OneSignal's iOS SDK implementation:
- Uses per-notification unique categories (not predefined sets)
- Registers categories in NSE (not at app launch)
- FIFO pruning prevents category bloat
- Smart merging respects developer's custom categories
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix compilation errors in action button implementation
- Simplified icon creation code using direct method invocation instead of NSInvocation
- Fixed optional binding error in KlaviyoExtension (categoryIdentifier is non-optional)
- Build now succeeds without errors
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Add comprehensive test payloads for dynamic push action buttons
Includes 10 ready-to-use test payloads covering:
- Basic 2-button layouts
- SF Symbols icons (iOS 15+)
- 3-button configurations
- Single button CTAs
- E-commerce scenarios (cart, back-in-stock, etc.)
- Predefined category fallback
- Hybrid approach (both formats)
- Localization examples
- Error case handling
Also includes:
- Quick test script for apns-cli
- Verification checklist
- SF Symbols reference for e-commerce
- Troubleshooting guide
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Add JSON test payload files for apns-cli
Created 10 ready-to-use JSON payload files in test-payloads/ directory:
- 1-basic-two-buttons.json - Simple flash sale with 2 buttons
- 2-with-icons.json - Order delivered with SF Symbols icons
- 3-three-buttons.json - New arrivals with 3 buttons
- 4-single-button.json - Cart reminder with single CTA
- 5-abandoned-cart.json - Cart recovery scenario
- 6-back-in-stock.json - Product availability alert
- 7-predefined-fallback.json - Predefined category (no mutable-content)
- 8-hybrid.json - Both dynamic and predefined formats
- 9-localization-spanish.json - Spanish localization example
- 10-error-case.json - Invalid payload for error testing
Can be used directly with apns-cli:
apns-cli send --payload test-payloads/1-basic-two-buttons.json ...
Includes README.md with quick start guide and test script.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Add send-test.sh script for easy payload testing
* Remove predefined categories support (#503)
* Remove predefined category support and icon support, refactor test payloads
* run pre commit
* Delete spec.md file
* Remove separate opened push action event (#505)
* Add ActionType to button parser (#504)
* Remove predefined category support and icon support, refactor test payloads
* run pre commit
* Delete spec.md file
* Add ActionType to KlaviyoCore and expose to KlaviyoSwiftExtension
* Move ActionType to KlaviyoSwift
* Add ActionType helper
* Add ActionType to parser
* Prevent open_app buttons from deferring to overall deep link
* Fix logic on validation of openApp
* Add tests
* Move ActionType from KlaviyoSwift to KlaviyoCore
* Remove unreachable code
* Move validation to helper function, add test
* Make logic more concise
* Move parser tests to separate KlaviyoSwiftExtensionTests target, remove PoC test assets
* Update event schema for push action buttons (#506)
* Update event schema with Action button metadata
* Add tests
* Fix casing
* Remove TEST_PAYLOADS.md
* Add Button ID to opened push event
* Simplify button category registration (#508)
* Refactor isKlaviyoNotification helper to use it in multiple dicts
* Use proper klaviyo identifier
* Simplify category registration to hardcoded value
* Fix tests
* nit: fix file headers
* Safely register different categoryIdentifiers, ensure thread safety
* Use new registerCategory with com.klaviyo.buttom with identifier format
* Add KlaviyoSwiftExtension LoggeR (#510)
* Add push action button to README (#512)
* Add README section
* Add sample payload and TOC
* Add placeholder link
* Fix presumed release number, fix copy
* Clean up old UNNotifcationCategories (#511)
* Move KlaviyoCategoryController to KlaviyoCore
* Add pruneCategory method
* Prune category on opened push event
* Prune category if push was dismissed from Notification Center
* put prefix on KlaviyoCategoryController
* Add pruning against all displayed notifs
* Return true
* Rename KlaviyoCategoryController to KlaviyoCategoryManager
* Condense into a defer
* fix: fix test setup and refactor button parser (#555)
* fix: route pruneCategory through injectable environment to fix CI test crashes
On Xcode 16.4, UNUserNotificationCenter.current() throws
`bundleProxyForCurrentProcess is nil` when called from the test runner
(no host app). The defer block in handle(notificationResponse:) calls
KlaviyoCategoryManager.shared.pruneCategory() which hits this path,
crashing 6 tests on CI.
Fix by adding a `pruneCategory` closure to KlaviyoSwiftEnvironment
(matching the existing `setBadgeCount` pattern) and stubbing it to a
no-op in tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor: deduplicate action button lookup and validate deep link URLs
Address cursorbot feedback on PR #502:
1. Extract matchingActionButton helper in UNNotificationResponse+Klaviyo
to eliminate triplicated payload traversal across actionButtonURL,
actionButtonLabel, and actionButtonType.
2. Add URL(string:) validation in isValidActionURLCombination so
deep_link buttons with malformed URLs are rejected at parse time
rather than silently failing on tap.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor: consolidate isKlaviyoNotification onto notification types (#556)
Move isKlaviyoNotification off the raw userInfo dictionary and onto
UNNotificationContent (KlaviyoCore) and UNNotification (KlaviyoSwift),
so callers work with the most natural object rather than reaching deep
into the object graph.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Ajay Subramanya <ajay.subramanya@klaviyo.com>
Co-authored-by: Claude <noreply@anthropic.com>
* Add FormLayout structs * Add presentation style to InAppFormsConfig * Add InAppWindowManager * Remove misreturn, make InAppWindowManager a singleton * Add presentWithLayout event * Create helper to determine if floating form window is active * Implement branching logic for modal vs window presentation in IAFPresentationManager * Mock custom layout coming from js * Remove unused property * Prevent mislayout during orientation changes * Clean up guards * Reorganize code * Unify legacy present with presentWithLayout action, clean up mock, fix test * Address cursor comments * Parse actual layout data coming from js * Fix formWillAppear test structure and uncomment log * Address bugbot comments * Bump formWillAppear to v2 * Tidy up Margins * Fix centering for top and bottom (cursor comment) * Respond to open comments, tidy up docs * Remove redundant effectiveWidth/effectiveHeight * fix(forms): assign currentFormId/Name in flexible form path and update handshake test Flexible form presentation was not setting currentFormId/currentFormName, causing dismissForm/destroyWebView to lose form identity on dismiss if the bridge sends nil identifiers. Mirrors the existing modal path behaviour. Also updates the handshake snapshot test to expect formWillAppear v2, matching the version bump already in IAFNativeBridgeEvent.swift. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * MAGE-323: safe zone handling (#541) * MAGE-323: safe zone handling * use max rather than sum for user offset and safezone * offset is user + safezone * add safezone consideration for left and right sides * fix: floor available dimensions at zero to prevent negative CGRect - availableWidth and availableHeight can go negative when margins plus safe area insets exceed screen bounds (e.g. small devices in landscape) - Wrap both in max(0, ...) so clampedWidth/clampedHeight stay non-negative -Claude * Handle keyboard and orientation changes smoother (#540) * Better keyboard handling * fix: address Cursor BugBot review feedback in InAppWindowManager - Remove unused `viewController` weak property (dead store) - Guard against keyboard adjustment for fullscreen forms to prevent off-screen shift -Claude * fix(forms): clamp keyboard avoidance shift to safe area top Tall bottom-anchored forms could be pushed off-screen when the keyboard appeared because the upward shift was unbounded. Clamp the adjusted origin.y to the safe area top so the form stays visible. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(forms): decode formWillAppear payload at bridge layer instead of handler Move formWillAppear from opaque Data to typed args (formId, formName, layout), matching formDisappeared and openDeepLink. This decodes the payload in IAFNativeBridgeEvent.init(from:) rather than deferring to handleNativeBridgeEvent, removing the do/catch and simplifying the handler. Add Equatable conformance to FormLayout types so the enum can synthesize Equatable. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Amber Wallace <amber.wallace@klaviyo.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
There are 4 total unresolved issues (including 2 from previous reviews).
❌ 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 c3a7a52. Configure here.
* refactor(forms): simplify keyboard handling in InAppWindowManager Move keyboard observers to singleton init so they persist across form presentations. Track the latest keyboard frame in currentKeyboardFrame and centralize avoidance logic in updateWindowFrame, replacing the per-presentation setupObservers/removeObserver pattern. Also adds keyboardWillChangeFrame observation, uses keyboardFrame.origin.y for accurate keyboard top calculation, and removes makeKeyAndVisible. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(forms): initialize InAppWindowManager earlier for keyboard observers Access InAppWindowManager.shared during SDK registration so keyboard notification observers are set up before any form is presented. Without this, a form shown while the keyboard is already visible would not receive prior keyboardWillShow events and would render without accounting for the keyboard. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * rename keyboardEvent Co-authored-by: Andrew Balmer <ab1470@users.noreply.github.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Andrew Balmer <ab1470@users.noreply.github.com>
…560) * feat(forms): expose device info to onsite JS via data-klaviyo-device Adds a `data-klaviyo-device` attribute on the in-app forms template head that publishes screen dimensions, safe-area insets, orientation, and device pixel ratio using CSSOM conventions. This lets onsite-in-app compute flexible-form dimensions at HTML parse time without querying potentially-stale `window.screen.*` before view-hierarchy attachment. The attribute is injected via a `.atDocumentStart` `WKUserScript` so it is set before any inline `<script>` in the template runs, and it is re-published on orientation transitions (`viewWillTransition`) and safe-area inset changes (`viewSafeAreaInsetsDidChange`) to keep onsite in sync with the device state. * fix(forms): harden device-info push and add test coverage - Guard `viewSafeAreaInsetsDidChange` and the `viewWillTransition` coordinator completion on `view.window != nil && !webView.isLoading` so preload-phase changes don't trigger noisy `evaluateJavaScript` failures. - Extract `document.head.setAttribute('data-klaviyo-device', ...)` into `DeviceInfo.asAttributeAssignmentScript()` so the injection-time and runtime-push call sites share one JS-escaping contract. - Log `toJsonString()` encode failures at `.error` instead of silently falling back to `"{}"`. - Add test coverage for the portrait-bounds landscape-swap inverse, `dpr` clamp at zero native scale, and stable JSON key ordering. - Note wire-contract justification on the `top`/`dpr` identifier_name SwiftLint suppressions. * feat(forms): rename layout offsets key and add addSafeAreaInsetsToOffsets flag Updates the formWillAppear layout payload contract to match the new onsite naming. Reads `offsets` first and transparently falls back to the legacy `margin` key, logging a one-shot deprecation notice on the first fallback hit per session. Adds `addSafeAreaInsetsToOffsets` (default `true`) to the layout payload. When `false`, the SDK positions the form using the provided offsets as-is without adding the window's safe-area insets — lets onsite own safe-area handling end-to-end when it has already baked the insets into its offsets. FULLSCREEN continues to fill `screenBounds` and ignores offsets and the flag. * refactor(forms): rename Margins struct to Offsets Drop the Margins typealias-based compat shim in favor of a direct rename now that all call sites use the Offsets name via the typealias. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(forms): clarify deviceInfoWKScript staleness window * Fix this compile error * fix(forms): use windowScene key window bounds; drop lock around deprecation flag * refactor(forms): rename marginX locals to offsetX for naming consistency Local variables in calculateFrame and its test helper still used the old margin terminology. Renamed to offset to match the wire key and struct. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(forms): drop dimension-swap heuristic now that window.bounds is source of truth * refactor(forms): drop JS escape layer; inject device info via JSON.stringify Instead of embedding the JSON payload inside a single-quoted JS string literal and escaping single quotes / backslashes, embed the raw JSON as a JS object expression and wrap with JSON.stringify at runtime. JSON is a subset of JS, so the engine parses the object literal natively and then stringifies it back for the attribute value. Removes the klaviyoJsSingleQuoteEscaped extension and its escape-layer tests — replaced by a single script-shape assertion. The WKUserScript initial-injection path uses the same asAttributeAssignmentScript helper and benefits automatically. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(forms): flip landscape CSSOM mapping to match WebKit Safari UIInterfaceOrientation.landscapeRight (home button on left, angle 90°) should map to landscape-primary, and .landscapeLeft (home button on right, angle 270°) to landscape-secondary. This matches how iOS Safari itself populates screen.orientation.type. Verified empirically on device — the previous mapping was inverted. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(forms): don't read device bounds/insets from our own overlay window The form's overlay UIWindow (created by InAppWindowManager at windowLevel .normal + 1) becomes key whenever the user interacts with a form input. At that point, DeviceInfo.current() was picking up our own overlay's bounds / insets and publishing them as the 'screen', shrinking reported dimensions to the flyout's frame. - Dimensions now come from scene.coordinateSpace.bounds (the scene's full drawable rect), which is unaffected by which window is key. - Safe-area insets come from the host app's window — our overlay is explicitly excluded via !== match on InAppWindowManager.currentFormWindow. - nativeScale moves to scene.screen (window-agnostic). Also drops the legacy 'margin' wire-key fallback now that onsite is fully migrated to 'offsets' in production. Removes the now-dead FormLayoutDeprecationLogger + resetForTesting hook. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
KlaviyoSwiftExtension now depends on KlaviyoCore, so the CocoaPods publish workflow needs --include-podspecs='*.podspec' to resolve the dependency from local files during validation (before anything is published to the CDN). Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>


Description
Bump SDK version from 5.2.3 to 5.3.0 for the upcoming release.
Due Diligence
Release/Versioning Considerations
PatchContains internal changes or backwards-compatible bug fixes.MinorContains changes to the public API.MajorContains breaking changes.Changelog / Code Overview
Version bump from 5.2.2 → 5.3.0 across:
Version.swiftKlaviyoCore,KlaviyoSwift,KlaviyoForms,KlaviyoSwiftExtension,KlaviyoLocation)Test Plan
Related Issues/Tickets