Skip to content

fix(forms): floating form offset clamping + data-klaviyo-device device info#443

Draft
evan-masseau wants to merge 8 commits intorel/4.4.0from
ecm/form-rendering-tweaks
Draft

fix(forms): floating form offset clamping + data-klaviyo-device device info#443
evan-masseau wants to merge 8 commits intorel/4.4.0from
ecm/form-rendering-tweaks

Conversation

@evan-masseau
Copy link
Copy Markdown
Contributor

Description

Floating-form rendering tweaks: clamp offsets so they can't push the form off-screen, expose a data-klaviyo-device attribute on <head> for onsite JS, harden the device-info push path for thread-safety, and rename the margin layout key to offsets plus a new opt-out flag for SDK-managed safe-area insets.

Due Diligence

  • I have tested this on an emulator and/or a physical device.
  • 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 compatible with all Android versions the SDK currently supports.

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.
  • This is planned work for an upcoming release.

Changelog / Code Overview

Four closely-related areas of work on floating-form rendering:

  1. Offset clamping (65152a74a, 3cefafc0a)

    • Clamp floating-form width/height so offsets can never drive the computed size negative.
    • Clamp the bottom-gap used for keyboard-overlap math to >= 0 so we don't spuriously report overlap when the form sits above the keyboard.
  2. Device info attribute (54472e68b)

    • Expose a data-klaviyo-device attribute on <head> carrying JSON-encoded device info (DPR, viewport, orientation, safe-area insets) so onsite JS can read it directly.
    • Pushed on orientation changes and safe-area inset updates without reloading the template.
  3. Device-info push hardening (fb3a0bf96)

    • pushDeviceInfo now dispatches the entire snapshot/serialize/escape/evaluate chain onto the UI thread. DeviceInfoProvider.current() reads UI-thread-only APIs (Display.rotation, decorView.rootWindowInsets) and silently degrades off-thread.
    • Added test coverage: sub-unit density flooring, odd density rounding, asymmetric landscape cutouts, toJson determinism.
    • Documented the natural-landscape caveat in the rotation→orientation mapping (matches our product scope of natural-portrait phones).
  4. Wire rename + safe-area opt-out (178ebb6f5)

    • Rename wire key marginoffsets to match Android internal naming.
    • Read offsets first; fall back to margin for backward compatibility with older onsite payloads. Emits a one-time verbose deprecation log the first time the fallback is hit.
    • New addSafeAreaInsetsToOffsets flag (default true) lets onsite opt out of SDK-managed safe-area insets entirely. When false, the SDK treats offsets as absolute distances from the screen edge and skips safe-area in both position math and available-space clamping. Keyboard-overlap math mirrors the same semantic.

Wire-contract changes for Fender

  • marginoffsets (with backward-compat fallback and one-time deprecation log)
  • addSafeAreaInsetsToOffsets: Boolean optional, defaults to true (preserves existing behavior)

Test Plan

  • ./gradlew :sdk:forms:testDebugUnitTest passes (new coverage in FormLayoutTest, FloatingFormWindowTest, DeviceInfoTest)
  • ./gradlew ktlintCheck clean
  • Manual device verification:
    • Floating form with large offsets clamps to screen bounds instead of overflowing
    • data-klaviyo-device attribute appears on <head> and updates on rotation
    • addSafeAreaInsetsToOffsets: false payload from Fender positions the form at absolute edge-relative offsets (no safe-area padding added)
    • Legacy margin payload still renders correctly and emits the deprecation log once per session

Related Issues/Tickets

Related to MAGE-541

evan-masseau and others added 5 commits April 22, 2026 15:45
Bring Android floating-form offset handling in line with iOS. Offsets
(plus safe-area insets) are treated as the margin from the screen edge
and take priority over the requested form size: if form dimensions plus
margins would exceed the screen, the form now shrinks to fit. If it
already fits, margins anchor it away from the edge without shrinking.

- Introduce calculateLayoutParams() that folds safe-area + user offsets
  into available width AND height, clamps requested dims via min(), and
  returns gravity-relative x/y consistent with Android's WindowManager
  model.
- Respect left/right offsets for horizontally-centered positions (TOP,
  BOTTOM, CENTER) and top/bottom offsets for CENTER by shifting toward
  the smaller margin (matches iOS calculateFrame semantics).
- FULLSCREEN continues to ignore offsets and fill the screen.
- Update calculateFormBottomGap() to account for the asymmetric-margin
  shift on CENTER and for the clamped form height, keeping the
  keyboard-shift overlap math correct.
- Remove the now-dead FormPosition.isHorizontallyCentered() helper.
- Expand FloatingFormWindowTest coverage for clamping, asymmetric
  centering, safe-area + offset interaction, and the FULLSCREEN
  short-circuit.
Forms taller than the available screen area (or CENTER with heavily
asymmetric margins) could produce a negative calculateFormBottomGap
value, which in turn over-shifted the flyout when the keyboard opened.
Floor the gap at zero so keyboard-shift math stays well-defined.
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 re-published whenever insets change (via the existing
`setOnApplyWindowInsetsListener`) and on orientation changes (via the
presentation manager's `ConfigurationChanged` handler), so onsite stays
in sync with the device state.
…sets flag

Rename the formWillAppear wire key `margin` to `offsets` to match the
Android internal naming. Read `offsets` first and fall back to `margin`
for backward compatibility with older onsite payloads, emitting a
one-time verbose deprecation log the first time the fallback is hit.

Add a new optional `addSafeAreaInsetsToOffsets` flag (default true) that
lets onsite opt out of the SDK's safe-area handling. When false, the SDK
treats offsets as absolute distances from the screen edge and skips safe
area entirely for both position math and available-space clamping, so
onsite is fully responsible for any safe-area inset it wants to apply.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@evan-masseau
Copy link
Copy Markdown
Contributor Author

bugbot run

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

✅ 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 178ebb6. Configure here.

@evan-masseau
Copy link
Copy Markdown
Contributor Author

bugbot run

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

✅ 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 178ebb6. Configure here.

evan-masseau and others added 2 commits April 22, 2026 23:25
…rotation lag

ConfigurationChanged fires before the old activity is destroyed, so
Display.rotation/rootWindowInsets still reflect the prior orientation
when read from Registry.lifecycleMonitor.currentActivity at that moment.
Pushing device info then left `data-klaviyo-device` one rotation behind
on every subsequent rotation.

Replace the immediate pushDeviceInfo() call in onConfigurationChanged
with a one-shot ActivityObserver that waits for the next Resumed event
(when the new activity's Display metrics are fresh), with a safety
timeout to unregister if the new activity never resumes.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Rotation-to-Resumed is typically sub-300ms. 5s was overspecified and meant
stale attribute data could linger for 5s in the pathological destroyed-
without-replacement path. 1s comfortably exceeds real recreation time.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…ally

BugBot flagged that clearTimers() does not reset deviceInfoPushObserver /
deviceInfoPushTimeout. That is deliberate — the data-klaviyo-device
attribute should stay fresh on the preloaded webview whether or not a
form is presenting. dismiss() does not destroy the webview, and
pushDeviceInfo already guards against a null webview.

Also remove the diagnostic console-log snippet from the HTML template.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@evan-masseau
Copy link
Copy Markdown
Contributor Author

bugbot run

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

✅ 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 cb57acc. Configure here.

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