Skip to content

SDK-5788: React Native SDK - #37

Draft
akashvercetti wants to merge 52 commits into
release/native-display-v1from
SDK-5788-react-native-sdk
Draft

SDK-5788: React Native SDK#37
akashvercetti wants to merge 52 commits into
release/native-display-v1from
SDK-5788-react-native-sdk

Conversation

@akashvercetti

Copy link
Copy Markdown

No description provided.

CTLalit and others added 19 commits May 8, 2026 12:46
Lands the .github/workflows/docs.yml file on the default branch so the
"Run workflow" button appears in the Actions UI. This is a small,
infra-only PR cherry-picked ahead of the full docs PR (#27) so we can
trigger workflow_dispatch against the feat branch and produce a first
gh-pages deploy without waiting for the v1 release stack to merge.

The workflow itself does nothing destructive on main pushes until the
docs PR cascade lands (Dokka + DocC + Docusaurus all live on the docs
branch). The only intended use of this workflow file on main right now
is workflow_dispatch from the feat branch.

Once the docs PR merges into main via the cascade, the workflow file
content here will match the docs PR — no merge conflict.

Jira: https://wizrocket.atlassian.net/browse/SDK-5784

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
chore(SDK-5784): add docs CI workflow scaffolding (infra-only)
@akashvercetti
akashvercetti requested a review from CTLalit May 25, 2026 07:35
akashvercetti and others added 10 commits May 26, 2026 15:20
feat: Native Display SDK — initial release
… action.metadata (#55)

* feat: remove elementID transport marker — attribution fields flow via action metadata

wzrk_element_id (and wzrk_btn_text, wzrk_activity_type, wzrk_data) are now
injected by the BE into each action's metadata field. ActionAttributionExtras
already spreads action.metadata into the extras map, so these keys reach Core
SDK via additionalProperties without a dedicated elementID parameter.

- Remove wzrk_btn_id transport marker from ActionAttributionExtras (Android + iOS)
- Update NativeDisplayBridge reflection probe to 2-arg Core SDK method signature
- Simplify invokeClickedEvent — pass all sanitized extras directly
- Remove sendThreeArgSelector (dead code) from iOS bridge
- Update NativeDisplayRenderer call sites to match new from() signature
- Update all tests to match new API and 2-arg selector

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* rename wzrk_btn_text→wzrk_c2a and wzrk_activity_type→wzrk_act

* rename wzrk_btn_text→wzrk_c2a and wzrk_activity_type→wzrk_act

* rename wzrk_btn_text→wzrk_c2a and wzrk_activity_type→wzrk_act

* fix: replace string literals with enum values for type-correct compilation

- IndicatorPosition.BOTTOM / IndicatorShape.CIRCLE in sample gallery configs
- TextAlign.CENTER replaces "center" strings in sample and StylePropertiesTest

* fix: add Dokka plugin to :sdk so dokkaHtml CI task resolves

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): skip docs site job when website/ content is absent

The site job references website/package-lock.json for npm caching, but
that content only lands when the docs/native-ui-kit-site branch is merged.
PRs touching android/sdk/** triggered the job and failed immediately on
the setup-node cache-path resolution step.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): gate docs site job via a pre-check job output

hashFiles() is not valid in job-level if conditions. Replace it with a
check-website job that shells out to test for website/package.json and
exports a has_website output; the site job gates on that output via
needs.check-website.outputs.has_website.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(SDK-5846): add metadata to OpenUrl action and harden metadata parsing (Android + iOS)

- Action.OpenUrl gains metadata: Map<String, String>? so attribution fields
  (wzrk_element_id, wzrk_c2a, wzrk_act, wzrk_data) reach Core SDK for URL
  button clicks — previously they were silently dropped
- CustomAction now uses CustomActionSerializer (Android) / custom init(from:)
  (iOS) replacing the auto-synthesised Codable that crashed when a metadata
  value was a JSON object; values that are objects/arrays are serialised to
  compact JSON strings so the map stays Map<String,String> without throwing
- ActionAttributionExtras spreads OpenUrl.metadata into attribution extras,
  matching the existing CustomAction.metadata spreading
- OpenUrlSerializer URL resolution hardened to handle plain strings,
  platform objects, and legacy {text,replacements} Ultron format without crashing
- 4 new unit tests: string metadata round-trip, wzrk_data as JSON object for
  CustomAction and OpenUrl, and null value dropping

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(SDK-5846): fix cache bridge architecture and element-click attribution (Android + iOS)

Android:
- Fix updateDisplayUnits proxy handler — was casting ArrayList<CleverTapDisplayUnit>
  to JSONArray (always null); now extracts JSON via getJsonObject() reflection
- Change onServerUpdate signature from (JSONArray) to (List<String>) to match
- Simplify coreSdkCacheProxy lambda now that list is pre-extracted
- Revert wireListener to early-return when cache is attached; remove activeCache
  field (Core SDK holds the cache strongly — extra reference was dead weight)
- Fix invokeClickedEvent gate: prefer element-clicked method whenever available,
  passing empty map when extras is absent (Core SDK still enriches from cached JSON)
- Add diagnostic logging for element-clicked method resolution

iOS:
- Fix handleServerCacheUpdate: replace wrong [[String:Any]] cast with performSelector
  extraction (json/jsonObject) matching the delegate fallback path
- Remove activeCache field from CleverTapAutoWire — Core SDK holds cache strongly
  via @Property(nonatomic,strong); bridge also holds it; extra reference was redundant
- Move isCacheAttached flag to NativeDisplayBridge; set it in attachCache
- Fix invokeClickedEvent gate: prefer element-clicked path whenever available
- Replace perform(_:with:with:) with class_getMethodImplementation + unsafeBitCast
  for the 2-arg void ObjC call — perform misinterprets the empty return register

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…id (#54)

* Fix BOX hit-testing and video controls tap handling for ios and android
Video controls — tap not registering on AndroidView and tap unreliable on UIViewRepresentable
Bindings — non-string JSON values cause parse failure
VariableEvaluator — numeric booleans not evaluated correctly

* fix(ios): handle Double in asBool and cancel stale video control timers

- Add Double case to asBool(_:) so numeric JSON values like 1.0 are treated as truthy instead of falling through to nil
- Replace untracked asyncAfter dispatches with cancellable DispatchWorkItem in VideoPlayerView and VideoFullscreenView to prevent stale timers hiding controls prematurely

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: CTLalit <144685420+CTLalit@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…tation (#56)

* SDK-5847: Fix Android sample ND views disappearing on device rotation (Android)

On rotation the Activity recreates, destroying all Compose `remember`/
`mutableStateOf` state. Bridge callbacks (onNativeDisplaysLoaded) fire
only once — so the received units were never re-delivered, leaving the
canvas blank after rotation.

Fix: move received units and log messages into ViewModels that survive
configuration changes.

- New CleverTapIntegrationViewModel: holds receivedUnits + logMessages
  as StateFlows; CleverTapIntegrationScreen collects via collectAsState()
- New BridgeIntegrationViewModel: holds the NativeDisplayBridge instance
  (so the same bridge survives rotation) plus all UI state flows;
  bridge.clear() deferred to onCleared() instead of DisposableEffect
- XmlFeedViewModel: added receivedUnits StateFlow + setUnits(); Fragment
  collects it in a second repeatOnLifecycle coroutine to restore canvas
  on view recreation without waiting for a new bridge callback
- Added lifecycle-viewmodel-compose dependency (reuses lifecycle 2.8.7)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(android): improve ExoPlayer lifecycle and video rendering robustness

- Switch PlayerView surface from SurfaceView to TextureView (via ct_player_view.xml)
  so parent alpha/graphicsLayer transforms are respected; fixes overlapping video
  bleed-through when tabs are hidden with graphicsLayer { alpha = 0f }
- Replace 100ms polling loop with Player.Listener callbacks (onIsPlayingChanged,
  onVolumeChanged, onPlaybackStateChanged) to eliminate unnecessary recompositions
- Add onPlayerError listener to catch async decoder failures (e.g. 4K H.264
  NO_EXCEEDS_CAPABILITIES) and surface them as a visible error state instead of
  silently stalling
- Remove inline AndroidView from composition when fullscreen is active using
  if (!isFullscreen) instead of view.player = null
- Configure AudioAttributes (USAGE_MEDIA + AUDIO_CONTENT_TYPE_MOVIE) with
  handleAudioFocus=true so video correctly pauses on audio interruptions
- Drop manual isMuted = !isMuted toggle from click handlers; onVolumeChanged
  fires synchronously so the manual toggle was immediately inverting the state

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(android-sample): decouple navigation and improve MainActivity structure

- Replace NavController params with callback lambdas in BannerDetailScreen,
  BannerShowcaseScreen, JSONViewerScreen, and DemoScreenContainer; screens no
  longer import NavController and are independently testable
- Add Routes object centralising all route strings and builder functions;
  eliminates raw string literals scattered across NavHost declarations
- Add MainTab enum and replace 5 repetitive NavigationBarItem blocks with a
  forEachIndexed loop over MainTab.entries
- Switch selectedTab to rememberSaveable { mutableIntStateOf(0) } so the active
  tab survives screen rotation and process death
- Replace TabContent visibility wrapper with a plain when(selectedTab) expression;
  TabContent function deleted as it no longer had a role after the conditional
  composition fix from the previous commit

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* SDK-5847: Address CodeRabbit PR #56 review comments

- VideoRenderer: add `import android.util.Log`, replace all qualified calls
- VideoRenderer: `errorMessage` keyed on `videoUrl` (remember → remember(videoUrl))
- VideoRenderer: clear errorMessage on STATE_READY recovery
- VideoRenderer: rebind `view.player = exoPlayer` in AndroidView.update for both
  inline and fullscreen PlayerView instances
- BridgeIntegrationScreen: move bridge.addListener into DisposableEffect keyed on
  (bridge, listenerRegistered) so listener re-attaches after rotation
- CleverTapIntegrationViewModel: atomic log append via _logMessages.update { }
- MainActivity: Uri.encode route params; omit filename query param when null

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Update readme

* removes unwanted docs

* Update README.md
* SDK-5859: Support custom URL schemes in open_url action (Android + iOS)

Replace the scheme allowlist (http/https/tel/mailto) with a blocklist
of dangerous schemes (javascript/data/file) in both ActionHandler
implementations. For non-http/https schemes the OS resolves the handler
directly — enabling deep-links into other apps (e.g. myapp://, fb://)
without going through Chrome Custom Tabs / SFSafariViewController.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* SDK-5859: Match Core SDK openUrl behavior exactly (Android + iOS)

Replace the previous allowlist/blocklist scheme validation and custom
routing with an implementation identical to Core SDK:

Android: strip \n/\r, extract query params as Bundle extras, use
Intent.ACTION_VIEW, prefer own app's handler (mirrors
InAppActionHandler.openUrl / setPackageNameFromResolveInfoList).

iOS: call UIApplication.shared.open(url, options: [:]) directly,
matching CleverTap.m openURL:forModule: (openURL:options:completionHandler:).

Only the customTabsEnabled flag introduces a branch — Custom Tabs on
Android / SFSafariViewController on iOS. Everything else, including
custom app schemes (myapp://, fb://, spotify://) and http/https, goes
through the same OS-level dispatch as Core SDK.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Empty imageview removed when failure or no image
Adds explicit clear background to make UI similar to android
…ndroid and iOS (#60)

* feat(SDK-5860): add NDLogger with client-configurable log level for Android and iOS

Replace raw android.util.Log / print() calls across both SDKs with a
structured NDLogger that respects a configurable level. Clients can now
silence or amplify SDK output independently of the Core SDK.

Android:
- New internal NDLogger object (OFF/INFO/DEBUG/VERBOSE, default INFO)
- New public NDLogLevel enum exposed via NativeDisplayBridge.setLogLevel()
- CleverTapAutoWire syncs Core SDK's getDebugLevel() as default on auto-wire
  when the client has not set an explicit level
- Replaced ~60 Log.* calls across handler, renderer, bridge, placement, internal

iOS:
- New internal NDLogger enum with matching levels (default .info)
- CTNDLogLevel typealias + NativeDisplayBridge.setLogLevel(_:) as public API
- CleverTapAutoWire reads debugLevel via KVC on auto-wire as default
- Replaced ~51 print() calls across Bridge, Handlers, Placement

Client call always wins regardless of call order relative to auto-wire.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(SDK-5860): address CodeRabbit review — thread safety and cross-platform parity

Android NDLogger:
- Guard setLevel and syncFromCoreSdk under stateLock to eliminate the
  check-then-write race between @volatile fields

iOS NDLogger:
- Add NSLock protecting setLevel and syncFromCoreSdk writes
- Add syncFromCoreSdk(_ level:) that updates _level without setting
  explicitlySet, matching Android's re-sync semantics

iOS CleverTapAutoWire:
- Switch syncLogLevelFromCoreSdk to call NDLogger.syncFromCoreSdk
  so auto-wire does not permanently lock out future Core SDK syncs
- Change unknown-level fallback from .info to .debug (matches Android)
- Clarify comment to reflect actual explicitlySet behavior

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(SDK-5860): add NDLogger.swift to Xcode project — resolves 'cannot find NDLogger in scope'

The .xcodeproj explicitly enumerates source files and does not auto-discover
new subdirectories. Added Internal/ group and NDLogger.swift PBXFileReference
+ PBXBuildFile so Xcode targets compile the file alongside the rest of the SDK.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Adds oslog for iOS

* update android logs with [NativeDisplay] tag and class name

* Adds fix for italic style in ios

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Sonal Kachare <sonal@clevertap.com>
…ment taps (#61)

* SDK-5862: fire "Notification Clicked" attribution for IMAGE taps (Android + iOS)

IMAGE elements with click actions were executing the action (open URL, etc.)
but silently skipping the "Notification Clicked" system event, so click-through
attribution was never recorded in CleverTap analytics.

Android: extend onSystemClick guard in NativeDisplayRenderer from isButton to
isButton || isImage — one-line change, no impact on shouldApplyClickable.

iOS: add onSystemClick hook to TappableModifier / applyTappable (default nil,
zero impact on containers and non-image elements); RenderNode passes the
system-event closure for IMAGE nodes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* SDK-5862: guard image attribution against missing onClick action

onSystemClick was wired for all IMAGE nodes, so tapping an image that
has a componentListener but no server onClick action would fire
"Notification Clicked" with empty extras.

Android: use a `when` expression so the image branch only produces a
closure when node.actions[ON_CLICK] != null.

iOS: add a `guard let onClick` in the closure — returns early when no
onClick action is defined, preventing a spurious attribution event.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Sonal-Kachare and others added 4 commits June 10, 2026 15:02
* feat(SDK-5860): add NDLogger with client-configurable log level for Android and iOS

Replace raw android.util.Log / print() calls across both SDKs with a
structured NDLogger that respects a configurable level. Clients can now
silence or amplify SDK output independently of the Core SDK.

Android:
- New internal NDLogger object (OFF/INFO/DEBUG/VERBOSE, default INFO)
- New public NDLogLevel enum exposed via NativeDisplayBridge.setLogLevel()
- CleverTapAutoWire syncs Core SDK's getDebugLevel() as default on auto-wire
  when the client has not set an explicit level
- Replaced ~60 Log.* calls across handler, renderer, bridge, placement, internal

iOS:
- New internal NDLogger enum with matching levels (default .info)
- CTNDLogLevel typealias + NativeDisplayBridge.setLogLevel(_:) as public API
- CleverTapAutoWire reads debugLevel via KVC on auto-wire as default
- Replaced ~51 print() calls across Bridge, Handlers, Placement

Client call always wins regardless of call order relative to auto-wire.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(SDK-5860): address CodeRabbit review — thread safety and cross-platform parity

Android NDLogger:
- Guard setLevel and syncFromCoreSdk under stateLock to eliminate the
  check-then-write race between @volatile fields

iOS NDLogger:
- Add NSLock protecting setLevel and syncFromCoreSdk writes
- Add syncFromCoreSdk(_ level:) that updates _level without setting
  explicitlySet, matching Android's re-sync semantics

iOS CleverTapAutoWire:
- Switch syncLogLevelFromCoreSdk to call NDLogger.syncFromCoreSdk
  so auto-wire does not permanently lock out future Core SDK syncs
- Change unknown-level fallback from .info to .debug (matches Android)
- Clarify comment to reflect actual explicitlySet behavior

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(SDK-5860): add NDLogger.swift to Xcode project — resolves 'cannot find NDLogger in scope'

The .xcodeproj explicitly enumerates source files and does not auto-discover
new subdirectories. Added Internal/ group and NDLogger.swift PBXFileReference
+ PBXBuildFile so Xcode targets compile the file alongside the rest of the SDK.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Adds oslog for iOS

* update android logs with [NativeDisplay] tag and class name

* Adds fix for italic style in ios

* Adds push support

* Fixes a crash in syncLogLevelFromCoreSdk as performselector breaks for primitive type
Update package.swift path

* update debug mapping comment

---------

Co-authored-by: CTLalit <144685420+CTLalit@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
… small fixes (#63)

* SDK-5862: fire "Notification Clicked" attribution for IMAGE taps (Android + iOS)

IMAGE elements with click actions were executing the action (open URL, etc.)
but silently skipping the "Notification Clicked" system event, so click-through
attribution was never recorded in CleverTap analytics.

Android: extend onSystemClick guard in NativeDisplayRenderer from isButton to
isButton || isImage — one-line change, no impact on shouldApplyClickable.

iOS: add onSystemClick hook to TappableModifier / applyTappable (default nil,
zero impact on containers and non-image elements); RenderNode passes the
system-event closure for IMAGE nodes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* SDK-5862: guard image attribution against missing onClick action

onSystemClick was wired for all IMAGE nodes, so tapping an image that
has a componentListener but no server onClick action would fire
"Notification Clicked" with empty extras.

Android: use a `when` expression so the image branch only produces a
closure when node.actions[ON_CLICK] != null.

iOS: add a `guard let onClick` in the closure — returns early when no
onClick action is defined, preventing a spurious attribution event.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: rebrand sample apps to "CT Display Lab" with Firebase FCM setup

- Rename app to "CT Display Lab" on Android (strings.xml) and iOS (CFBundleDisplayName)
- Add launcher icons (mdpi–xxxhdpi) with adaptive icon support (back/fore layers + anydpi-v26 XML)
- Update iOS AppIcon.appiconset with new 1024px icon
- Add Firebase Messaging dependency (BOM 33.7.0) and google-services plugin to Android sample
- Add POST_NOTIFICATIONS permission and FCM service to AndroidManifest
- Add google-services.json to .gitignore

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address CodeRabbit review comments

- Fix iOS/Android parity: fire onSystemClick unconditionally before
  notifyComponentListener in TappableModifier.swift so "Notification
  Clicked" attribution always fires on click, matching Android behavior
- Replace hardcoded CleverTap credentials with placeholders in
  AndroidManifest.xml and iOS Info.plist
- Remove empty colors.xml (unused after adaptive icon switched to PNG background)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: bump sample app versions to 2.0 (build 15) for QA distribution

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…#64)

* feat: add ObjC sample app with full SDK integration

- Restructure ObjC sample with 4-tab layout (Events, Slots, UIKit, More)
- Add RootTabBarController, BridgeIntegrationViewController, CleverTapIntegrationViewController, UIKitDemoViewController, SlotDemoViewController
- Remove unused screens (Browser, Arrangements, Animations, Home, TestCases)
- Add NDSlotPlaceholderView in NDDisplayHelper for slot-driven placeholder management
- Make NativeDisplayBridge, NativeDisplaySlotManager, NDLogLevel @objc-compatible
- Fix SlotDemoView AsyncImage frame/clipping in Swift sample
- Clean up NDDisplayHelper: remove unused arrangement-override, font-demo, and no-placeholder slot methods

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Update creds in sample app objc

* Adds code review fixes

* Fix coderabbit review comments

* Adds license and updates podspec
Fix: pod lib lint

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b1a1dc73-59e1-4c83-a98c-60b075f0cef1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch SDK-5788-react-native-sdk

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.

akashvercetti and others added 19 commits June 15, 2026 12:40
…hey were leaking through and breaking evaluateBoolean.
…tribution survives, and accepted the legacy nested {text} URL shape.
… with every click event so the dashboard can slice by button on RN like it can on native.
…Notification Viewed/Clicked itself, so hosts don't double-report.
…#66)

* Adds fake wzrk_id '0_0' for preview and test feature
Removes missing wzrk_id return nil check

* fix: update parser tests to expect fallback id '0_0' when wzrk_id is missing

Aligns tests with the behaviour change in the prior commit — missing
wzrk_id now yields a unit with id '0_0' instead of returning nil/null.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

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

* docs: restructure README integration + iOS UIKit attribution parity

- README: replace Quick Start with a structured Integration section.
  Approach 1 (slot-based, recommended) lays out the three steps —
  initialize the bridge, link with CleverTap Core, drop a slot view —
  with Compose+XML on Android and SwiftUI+UIKit on iOS. Approach 2
  covers custom rendering for hosts that need to inspect units or run
  standalone, and a Fetch-on-demand subsection documents the pull-mode
  bridge API. A new Event hooks section consolidates both
  NativeDisplayActionListener and the previously undocumented
  NativeDisplayComponentListener.

- iOS UIKit: add NativeDisplayUIView(unit:...) and updateUnit(_:) so
  UIKit hosts get full Notification Viewed / Clicked attribution
  without manually plumbing unitId through unit.config. Mirrors the
  Android NativeDisplayViewGroup.setUnit(unit) ergonomics and matches
  the SwiftUI NativeDisplayView(unit:) shape.

- Android VideoRenderer: add explicit no-op Player.Listener overrides
  to guard against AbstractMethodError when a host app brings a newer
  androidx.media3 version than the one the SDK was compiled against.

- build.gradle.kts: clarifying comment on the compileOnly
  androidx.fragment:fragment-ktx dependency (K1 compiler needs it on
  the classpath to resolve Core SDK supertypes).

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

* fix: stop firing iOS "Notification Clicked" twice on IMAGE element taps

Commit d939123 (SDK-5862) added the system-event call after the
component-listener check; a follow-up in 9a740a5 added a second call
*before* the listener (so attribution still fires when a listener
consumes the event, matching Android) but forgot to remove the
original post-listener call. On every IMAGE click that wasn't consumed
by a listener, both invocations ran -> two "Notification Clicked"
events. Removing the duplicate; the pre-listener call is the one that
matches Android semantics.

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

* fix: prevent AbstractMethodError from media3 Player.Listener default methods

Java 8 interface default methods are not honored on devices with
minSdk < 24, so any Player.Listener method added in a newer media3
version than the SDK was compiled against can crash with
AbstractMethodError the first time ExoPlayer invokes it. With
compileOnly media3 the SDK can't pin the version, so this is real for
clients that bring a newer media3.

- Introduce Media3PlayerListener: an open class that explicitly
  overrides every Player.Listener method with a no-op, bypassing the
  default-method path entirely.
- Switch VideoPlayerWithMedia3's listener to extend Media3PlayerListener
  so only the callbacks we care about (isPlaying / volume / state /
  error) carry behavior; everything else is a real Kotlin no-op
  override, safe across media3 versions.
- Annotate the new class and the surfaces that touch it with
  @UnstableApi (Cue / CueGroup / DeviceInfo etc. are unstable in media3).

Also bump android-sample to 2.1 (versionCode 16) for the QA build that
carries this fix.

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

* docs: prioritize Compose/SwiftUI, add Obj-C slot/cell support + campaign section

Restructure the integration section so Jetpack Compose + SwiftUI are the
default visible path. XML, UIKit (Swift), and UIKit (Obj-C) variants are
tucked under <details> collapsibles per step to shrink the scrollable
surface area while staying one click away.

Finish the Objective-C surface for Approach 1 (slot-based) by annotating
NativeDisplaySlotUIView (init/properties) and the UITableView /
UICollectionView cell wrappers (configure) with @objc. The bridge- and
NDLogLevel-level @objc work was already shipped in PR #64; these slot
and cell additions complete the slot-based flow from pure Obj-C.

Approach 2 stays Swift-only — NativeDisplayUnit is a Swift struct and
cannot bridge.

Add a "Creating a Native Display campaign" section pointing to
https://docs.clevertap.com/docs/native-display and the Advanced Builder,
framing dashboard authoring as the preferred path (targeting, scheduling,
A/B, attribution) while leaving the standalone-JSON option open.

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

* fix: adds missing method impl

- desugaring issue with 'default void' methods of interface.

* feat: UIKit/XML slot demos + fix SwiftUI-in-cell sizing

iOS SDK — fix slot/cell sizing in self-sizing UITableView/UICollectionView

- NativeDisplayUIView now sets UIHostingController.sizingOptions =
  [.intrinsicContentSize] (iOS 16+) so SwiftUI's preferred size flows
  into the hosting view's intrinsicContentSize independent of current
  bounds. Without this, percent-width / aspect-ratio campaign content
  locked in against an empty cell's near-zero bounds and rendered tiny.

- All four cell utility classes (slot × {table, collection} +
  non-slot × {table, collection}) now embed NativeDisplayUIView instead
  of independently reimplementing the UIHostingController bridge. One
  source of truth for SwiftUI hosting; ~80 LOC of duplication removed.

- Each cell resolves the enclosing UIScrollView's bounds and passes
  them as parentSize to NativeDisplayUIView(unit:parentSize:). This
  seeds the renderer's nativeDisplayParentSize environment override,
  bypassing the GeometryReader fallback that was measuring empty cell
  bounds on first SwiftUI layout.

- Each cell exposes a two-stage post-configure notification driving the
  enclosing container to re-measure: synchronous layoutIfNeeded on the
  cell to kick SwiftUI's first layout pass, then async beginUpdates /
  endUpdates (table) or invalidateLayout() (collection) to re-query
  row/item sizes once intrinsicContentSize is correct.

- NativeDisplayTableViewCell and NativeDisplayCollectionViewCell gain
  configure(with: NativeDisplayUnit, ...) overloads so attribution
  (Notification Viewed / Notification Clicked) fires when these cells
  host bridge-delivered content.

iOS sample

- New UIKit Slots bottom-nav tab demoing NativeDisplaySlotTableViewCell.
  Mirrors the SwiftUI SlotDemoView 1:1: 20-row UITableView with a
  header card (Slot Demo title + description + Fetch Slot Data button
  firing the hardcoded campaign events) followed by the same 4-slot /
  15-content interleave at the same indices. Custom AppContentTableViewCell
  matches the SwiftUI card visuals.

- UIKitTestViewController (existing UIKit Approach-2 tab) rewritten to
  use a UITableView + the SDK's NativeDisplayTableViewCell instead of
  ad-hoc UIScrollView + UIStackView + NativeDisplayUIView arranged
  subviews. ~50 LOC shorter, idiomatic UIKit, benefits from the cell
  sizing fixes above.

Android sample

- New XML Slots bottom-nav tab. RecyclerView-driven slot demo mirroring
  the Compose SlotDemoScreen: header row + same 19-row interleave +
  SlotFeedAdapter with NDSlotViewHolder hosting NativeDisplaySlotView
  widgets.

- XmlFeedFragment (XML Test tab / Approach 2) reverted from the
  slot-based experiment back to NativeDisplayViewGroup — the View-
  system equivalent of Compose's NativeDisplayView. Bridge listener
  feeds units into a LinearLayout canvas with dynamically-added
  widgets. Matches the Compose Events demo pattern in the Views system.

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

* fix: bump Android SDK Compose BOM to 2024.12.01 to match sample

The Android sample app uses composeBom 2024.12.01 (resolves
compose-foundation 1.7.x), while the SDK module was on 2024.06.00
(compose-foundation 1.6.x). Both modules contribute BOM constraints
to the multi-module classpath and Gradle picks the higher — so the
runtime ends up with 1.7.x, but the SDK bytecode was compiled with a
hard-coded reference to HorizontalPager-xYaah8o (the 1.6 mangled
name where the param is `beyondBoundsPageCount`). In 1.7 it was
renamed to `beyondViewportPageCount`, generating a new mangled name.
Opening any gallery in the sample crashed with:

  NoSuchMethodError: No static method HorizontalPager-xYaah8o(...)
  at GalleryRendererKt.RenderSnappingGallery(GalleryRenderer.kt:164)

The SDK's HorizontalPager / VerticalPager call sites only use the
stable params (state, modifier, contentPadding, pageSpacing) — no
beyondBoundsPageCount usage — so bumping the BOM is a source-
compatible change: same code, the compiler now emits a 1.7-mangled
HorizontalPager reference that matches what's on the classpath at
runtime.

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

* refactor: drop Pager dependency from gallery renderer for Compose flexibility

Replaces HorizontalPager / VerticalPager / rememberPagerState with
LazyRow / LazyColumn + rememberLazyListState + rememberSnapFlingBehavior
— all of which have been signature-stable across compose-foundation
1.5+ — and reverts the SDK's Compose BOM back to 2024.06.00.

Context
-------

The previous commit bumped the SDK BOM to 2024.12.01 to fix a
NoSuchMethodError on HorizontalPager when client and SDK ran against
different Compose versions. That worked but transitively forced
clients onto Compose 1.7+ via Gradle's max-wins resolution.

The audit (see PR description) confirmed Pager was the *only* ABI-
volatile API the SDK was using. Removing it lets the SDK be compiled
against an older, more permissive BOM and stay forward-compatible
across all Compose minor versions in our range — no version pinning,
no transitive upgrade pressure on client apps.

What changed in GalleryRenderer.kt
----------------------------------

- rememberPagerState        → rememberLazyListState
- HorizontalPager           → LazyRow + rememberSnapFlingBehavior
- VerticalPager             → LazyColumn + rememberSnapFlingBehavior
- pagerState.currentPage    → derivedStateOf { LazyListState.currentPageByViewportCenter() }
- pagerState.animateScrollToPage → lazyListState.animateScrollToItem
- pagerState.pageCount      → container.children.size (already available)

The "current page" calc walks layoutInfo.visibleItemsInfo and picks
the item whose extent covers the viewport center — keeps indicator /
arrow state correct under peek configurations (where firstVisibleItemIndex
lags behind by one item).

Pages are sized explicitly inside a BoxWithConstraints (page width =
container width - peekBefore - peekAfter) since LazyRow doesn't shrink
items to fit contentPadding the way Pager does automatically.

RenderGalleryArrows and RenderGalleryIndicators now take currentPage
+ pageCount as plain Ints instead of a PagerState reference, so they're
independent of which scroll primitive drives them.

Behavioural delta
-----------------

Close-enough parity with Pager. Snap timing/inertia differ slightly
(foundation snap fling vs. Pager's custom decay) but visual outcome
is the same. Auto-scroll, peek, indicators, arrows, infinite-scroll
wrap — all preserved.

Verified
--------

- ./gradlew :sdk:assembleDebug — BUILD SUCCESSFUL
- ./gradlew :app:assembleDebug — BUILD SUCCESSFUL

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

* fix: drop rememberSnapFlingBehavior — return type changed in compose-foundation 1.7

Second ABI break uncovered after removing Pager:
NoSuchMethodError on rememberSnapFlingBehavior. compose-foundation
1.7 widened the function's return type from FlingBehavior to
TargetedFlingBehavior, and the JVM treats return type as part of the
method signature — so a 1.6-compiled call doesn't resolve against
1.7 runtime.

Replaced with a manual snap built on Compose-1.0-stable primitives:

  LaunchedEffect(lazyListState) {
    snapshotFlow { lazyListState.isScrollInProgress }
      .filter { !it }
      .collect {
        val target = lazyListState.currentPageByViewportCenter()
        if (firstVisibleItemIndex != target || scrollOffset != 0) {
          lazyListState.animateScrollToItem(target)
        }
      }
  }

isScrollInProgress, animateScrollToItem, snapshotFlow, and the
layoutInfo accessors used by currentPageByViewportCenter have all
been signature-stable since Compose 1.0 — they're foundation
primitives, not the kind of API Compose tends to rename.

Behavioural delta: standard fling decay completes, then a small
animateScrollToItem correction lands on the nearest item. Slight
visual "snap-correct" jump where Pager would have flung smoothly to
the boundary — acceptable parity per the brief.

Also removed:
- @file:OptIn(ExperimentalFoundationApi) — no remaining
  experimental APIs after rememberSnapFlingBehavior is gone.
- Unused import of rememberSnapFlingBehavior.

Verified
--------
- ./gradlew :sdk:assembleDebug — BUILD SUCCESSFUL
- ./gradlew :app:assembleDebug — BUILD SUCCESSFUL

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

* feat: stamp ND SDK version on display-unit viewed and clicked analytics

The CleverTap server cannot currently identify which ND SDK build produced a
display-unit analytics event. Both bridges now attach `nd_lib_v_name` and
`nd_lib_v_code` to viewed and clicked attribution payloads via a non-`wzrk_`
namespace so the dashboard can slice events by ND SDK version.

Clicked events ride the existing element-aware path
(`pushDisplayUnitElementClickedEventForID` / `recordDisplayUnitElementClickedEventForID:additionalProperties:`)
which already accepts an additionalProperties bag. Viewed events add a new
reflection probe for the assumed 2-arg Core SDK overload
(`pushDisplayUnitViewedEventForID(String, HashMap)` /
`recordDisplayUnitViewedEventForID:additionalProperties:`) with the same
memoised cache, rebind invalidation, and graceful legacy fallback as clicks.

Version source-of-truth lives in `/VERSION` (bumped 0.1.0 -> 1.0.0). Android
reads it into `BuildConfig.ND_LIB_VERSION_NAME` / `ND_LIB_VERSION_CODE`
(monotonic code derived as M*10000 + m*100 + p). iOS mirrors via a small
`NativeDisplaySDKVersion` constants file because SPM/static-framework
distributions can't rely on `Bundle.infoDictionary`.

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

* fix(ios): slot cell width-fill + height cache to kill fast-scroll jump

Two cell-sizing bugs surfaced via UIKitSlotDemoViewController:

1) Tiny render after Fetch Slot Data. NativeDisplayRenderer Priority 1
   (explicit parent size) skipped the .frame(width:) wrapper that the
   GeometryReader fallback applied, so SwiftUI sized the root to its
   intrinsic (small) width and centered it in the hosting view. Mirror
   the GeometryReader path so the explicit-size branch also pins the
   width to the offered parent width.

2) Visible enlarge on fast scroll. Recycled slot cells started at
   estimatedRowHeight, then jumped once the SwiftUI host's intrinsic
   size settled one runloop later. Add a per-slotId measured-height
   cache on NativeDisplaySlotManager; the cell installs a placeholder
   contentView height constraint (priority 999) on configure(slotId:)
   when the cache has a value, writes the new measurement back from
   the stage-2 async pass in notifyEnclosingTableViewOfHeightChange,
   and deactivates the placeholder so the displayView's intrinsic
   size owns sizing thereafter. Fully internal — no client-app API
   change.

Sample side: point NativeDisplaySample.xcodeproj at the local
Package.swift via XCLocalSwiftPackageReference instead of pulling the
SDK from github.com main, so future SDK edits build into the sample.

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

* feat(samples): event-screenshot automation suite + events-log hide toggle

Adds 4 instrumented tests per platform (one per tab — Events + Slots in
both Compose/XML on Android, SwiftUI/UIKit on iOS) that fire a fixed list
of 22 events and screenshot/video the resulting UI. Replaces the older
header1-5/footer1-5-only CampaignScreenshot* tests.

Sample-app fixes that fell out of writing the suite:
- Events-log panel gets a hide/show toggle so screenshots aren't obscured
- iOS SwiftUI Send button now dismisses the keyboard via FocusState
- UIKit Events tab pins to safeAreaLayoutGuide (was clipping under notch)

Automation:
- Android: ./gradlew :app:automationScreenshots → ~/Desktop/nd-automation-output/android/
  Per-test MP4 via adb screenrecord, screenshots via BasicScreenCaptureProcessor.
- iOS: ios-sample/run_automation_with_video.sh "iPhone 16"
  Per-test MP4 via xcrun simctl io recordVideo, screenshots extracted from
  xcresult via the modernized pull_screenshots.sh (Xcode 16+ API).

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

* fix(ios): register NativeDisplaySDKVersion.swift in xcodeproj so DocC build resolves it

SPM globs ios/Sources/** so swift build picks the file up, but the docs CI uses
xcodebuild against the .xcodeproj which keeps an explicit file list — the file
was committed in a0f79de but never wired into project.pbxproj, leaving
ActionAttributionExtras.swift unable to resolve NativeDisplaySDKVersion under
xcodebuild.

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

* fix(ios): honor listeners on cell reuse + thread CT instance through viewed-event

Two related correctness fixes flagged by CodeRabbit on PR #65.

1. NativeDisplayUIView stored actionListener/componentListener as `let`, so a
   recycled cell that reconfigured with new listeners would silently keep the
   ones from the initial bind — taps routed to a stale host. Make the listener
   properties mutable, expose listener-aware overloads
   `updateConfig(_:actionListener:componentListener:)` and
   `updateUnit(_:actionListener:componentListener:)`, and have all four cell
   classes plus NativeDisplaySlotUIView use them on the existing-displayView
   branch. The single-arg `updateConfig(_:)` / `updateUnit(_:)` stay for
   external callers that don't need to rebind listeners.

2. NativeDisplayBridge.pushViewedEvent captured `ct` then called the private
   invokeViewedEvent which re-read `cleverTapInstance` — `attach`/`detach`
   between the two reads could send `seedIfNeeded` and the event to different
   instances or drop the event. Pass the already-captured `ct` through, mirroring
   the symmetric clicked-event path.

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

* chore: address CodeRabbit cleanups on sample build + automation wrapper

Three independent fixes flagged on PR #65 that don't change SDK behavior:

- android-sample/build.gradle.kts: preserve the source's relative path under
  the AGP output dir when copying automation artifacts to the Desktop. Flat
  copy (`File(dest, src.name)`) silently overwrote same-named PNGs/MP4s from
  different test classes.

- ios-sample .xcodeproj: drop three orphan Swift package product
  dependencies (`PKG001`, `57C0F6DA*`, `57C0F6EE*`) that had no `package`
  field and were triple-linking `CleverTapNativeDisplay` / `CleverTapSDK` in
  the app target's Frameworks build phase. Keep the valid pair
  (`57C009F8` → clevertap-ios-sdk remote, `57C009FB` → local ".."), so the
  app target now links each product exactly once.

- run_automation_with_video.sh: resolve the simulator UDID once up front
  and pin `simctl boot`, `simctl io recordVideo`, and the `xcodebuild`
  destination to that UDID. Previously `recordVideo` used `booted` while
  `xcodebuild` targeted by device name — on a dev machine with multiple
  booted sims the two could diverge. Also guard `cd "$SCRIPT_DIR"` with
  `|| exit 1` (shellcheck SC2164).

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

* Updates objc app, make ND SDK objc compatible

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Sonal Kachare <sonal@clevertap.com>
#70)

* SDK-5883: per-impression Notification Viewed + rotation hardening (Android + iOS)

Three correctness issues converging on Notification Viewed attribution and
Android rotation:

1. ActionHandler was rebuilt on every recomposition when callers passed
   inline-lambda listeners — its firedSystemEvents dedupe set was reset each
   time, making dedupe a silent no-op and churning a fresh CoroutineScope
   per recomposition.
2. Notification Viewed semantics were inconsistent across platforms: UIKit
   fired per impression (correct), SwiftUI sometimes dropped legitimate
   impressions due to the broken dedupe, Android double-fired on rotation
   because the deduped state lived on the wrong instance.
3. NativeDisplayViewGroup blanked out on rotation — eager state clear in
   onDetachedFromWindow, bridge-listener race ahead of canvas attach, and no
   onSaveInstanceState rehydration for the standard "VG declared in XML
   with android:id" pattern.

Android SDK:
- ActionHandler: drop firedSystemEvents and the deduplicate parameter on
  fireSystemEvent; demote listener/componentListener to internal var.
- NativeDisplayRenderer: remember(unitId) only; SideEffect-push listeners.
  Extract Notification Viewed into a private TrackNotificationViewed
  composable; cache findActivity via remember(context); key the viewed
  LaunchedEffect/DisposableEffect on unitId (was node.id).
- New ViewedUnitsTracker (bridge pkg): process-singleton set gating
  Notification Viewed. DisposableEffect.onDispose only removes when
  Activity.isChangingConfigurations == false. Rotation/locale/uimode flips
  do not re-fire; LazyColumn scroll-out-in, navigation, fresh launch do.
- NativeDisplayViewGroup: drop eager state clear in onDetachedFromWindow.
  Add onSaveInstanceState/onRestoreInstanceState that persist unitId in a
  BaseSavedState and rehydrate via NativeDisplayBridge.getNativeDisplayForId.
  Listeners intentionally not persisted (Parcelable cannot safely carry
  Activity/Fragment closures).

iOS SDK:
- ActionHandler: same removals (firedSystemEvents, deduplicate:) and
  listener decoupling. No rotation tracker needed — SwiftUI doesn't re-fire
  .onAppear on rotation, UIKit cells aren't rebound.
- NativeDisplayRenderer: drop deduplicate: true at both .onAppear sites.

Sample app (android-sample, XmlFeedFragment):
- by viewModels() -> by activityViewModels() so receivedUnits survives
  XmlFeedScreen's remove+add transaction on rotation.
- Bridge listener writes only to the VM; the lifecycle-gated StateFlow
  collector is the single renderUnits entry point.
- distinctUntilChanged keyed on unit ids absorbs Core SDK redeliveries
  with structurally-different list instances carrying the same wzrk_ids.
- canvas.doOnAttach defer in renderUnits to cover the lifecycle window
  where the fragment reaches STARTED before the canvas finishes its first
  layout pass.

Tests:
- Removed dedupe-specific tests in ActionHandlerSystemEventTest.kt and
  AttributionTests.swift; replaced with assertions that repeated fires
  reach both listener and bridge each time.
- Added ViewedUnitsTrackerTest.kt covering add-if-new, remove,
  idempotence, independent ids.

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

* chore(ios-sample): bump CleverTap iOS SDK to 7.7.1 and fix placeholder event name

- Package.swift: bump CleverTap iOS SDK constraint from 7.0.0 to 7.7.1 so the
  sample picks up recent Core SDK fixes.
- SlotDemoView.swift: replace stray "asd" placeholder event with "Footer1"
  to match the rest of the demo's event-firing sequence (Header1, Header2,
  Footer5, etc.).

Sample-only — no SDK surface impact.

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

* SDK-5883: address CodeRabbit review — pending restore, empty emissions, stale docs

NativeDisplayViewGroup.onRestoreInstanceState used to silently drop the saved
unitId when NativeDisplayBridge.getNativeDisplayForId returned null (bridge not
yet initialised, or unit not yet loaded into the cache). The XML view would
then stay blank after rotation in those races. Now the restored id is stashed
in unitIdState regardless of the bridge state, and onAttachedToWindow retries
the bridge lookup when the parent enters the window. A host-driven setUnit
still overwrites the stash if the host reaches the view first.

XmlFeedFragment's StateFlow collector skipped empty emissions, leaving stale
widgets visible when the bridge / VM transitioned from "has units" back to
"none". renderUnits already handles the empty case (clears the canvas, surfaces
emptyCanvasText), so the guard was masking a real correctness gap. Drop it.

ActionHandler.fireSystemEvent kdoc still pointed at LaunchedEffect(node.id) as
the cadence mechanism, but the renderer was refactored in c7f5a19 to gate
Notification Viewed via TrackNotificationViewed keyed on unitId with
ViewedUnitsTracker. Update the doc so the documented contract matches reality.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Adds @objc from Slot feature and updates obj sample app

* Removes unwanted logs
* docs: split integration guides by platform + document Obj-C surface

* Fixes pod lint warnings
Fixes an app bug in objc sample app

* Update INTEGRATION_IOS.md

* Fixes code rabbit comment
…#74)

* Adds privacyInfo.xcprivacy file

* Updates minimum swift language version to 5.5 compatible with iOS 15.0

* Update INTEGRATION_IOS.md
…release-preflight fixes (#73)

* chore(publish): wire Maven Central publishing + align Android/iOS conventions

Publishing infra (Android):
- Replace manual maven-publish with Vanniktech Maven Publish 0.32.0
  (matches Core SDK; targets new Central Portal)
- Add plugin alias to libs.versions.toml; wire credentials via
  local.properties (gitignored) and ORG_GRADLE_PROJECT_* env vars on CI
- Drop unused POM_*/GROUP/VERSION_NAME keys from gradle.properties
  (Vanniktech reads from the DSL block, not these legacy keys)

Versioning (decoupled per platform):
- Android: inline libraryVersion = "1.0.0" in sdk/build.gradle.kts;
  no longer reads root /VERSION so each platform owns its own release path
- iOS: update NativeDisplaySDKVersion.swift comment — lockstep is now
  iOS-internal (podspec only), not cross-platform via /VERSION

Naming alignment (consistent across iOS, Android, packages):
- Maven artifactId: clevertap-native-ui-kit → clevertap-native-display-sdk
  (matches Core SDK family naming: clevertap-android-sdk, -geofence-sdk, -hms-sdk)
- Android namespace: com.clevertap.android.nativeui → com.clevertap.android.nativedisplay
  (aligns generated BuildConfig/R package with Kotlin sources and iOS module
   name CleverTapNativeDisplay)
- Update 3 production + 1 test file with new BuildConfig/R imports

Drop media3-ui dep + fix verifyReleaseResources:
- VideoRenderer used PlayerView inflated from ct_player_view.xml which
  referenced media3-ui's surface_type/use_controller attrs; with media3-ui
  declared compileOnly, AAPT couldn't resolve them and assembleRelease failed
- Replace PlayerView + XML with plain TextureView constructed in code +
  ExoPlayer.setVideoTextureView(); SDK already draws its own Compose
  controls overlay so PlayerView's built-in controller wasn't needed
- Delete ct_player_view.xml; remove compileOnly(libs.androidx.media3.ui).
  Consumers no longer need media3-ui — only media3-exoplayer (+ media3-hls
  for HLS sources). Slimmer required runtime surface.

Published coordinate: com.clevertap.android:clevertap-native-display-sdk:1.0.0

Verified: ./gradlew :sdk:clean :sdk:assembleRelease :sdk:testReleaseUnitTest passes.

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

* chore(ios): release-preflight fixes — typed literals + parse-queue test

VariableEvaluator: type unquoted numeric/boolean literals so template
expressions like `{{count == 10}}` compare Int-to-Int instead of
Int-to-String. Quoted literals (e.g. `"10"`) keep their string form.

NativeDisplayBridge: expose internal `_runOnParseQueueAsync` test helper.
`parseQueue.sync` from the main thread can be GCD-optimized to run inline
on the caller, which defeats `Thread.isMainThread` assertions. Tests
inspecting thread identity must dispatch async to land on the real worker.

Tests:
- BridgeTests: rewrite parse-queue thread test to use async dispatch +
  XCTestExpectation; assert label and main-thread state captured on the
  actual worker thread.
- ConfigParserTests + BridgeTests: add `"id": "root"` to inline test JSON
  so the parser's required-id check passes.

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

* chore(android): action critical + cleanup lint findings from IDE inspection

Critical (correctness):
- ActionAttributionExtras.sanitize(): replace HashMap.putIfAbsent (API 24+)
  with a manual containsKey check. minSdk is 23 — calling putIfAbsent on
  Android 6 would NoSuchMethodError at runtime.
- NativeDisplayRenderer: BoxWithConstraints lambda now uses the scope's
  Dp `maxWidth`/`maxHeight` shortcuts instead of `constraints.maxWidth`,
  so lint sees the receiver as used and the scope's invariants are
  honored. Behavior identical; drops the now-unused `Constraints` import.
- sdk/build.gradle.kts: enable `vectorDrawables.useSupportLibrary` so the
  `android:fillType="evenOdd"` paths in the volume-tint icons render
  correctly on API 23 (attr is API 24+ natively).

Build hygiene:
- Move 5 hardcoded coordinates (clevertap-android-sdk, fragment-ktx,
  play-services-tasks, recyclerview) to the version catalog. Drop unused
  catalog entries (`androidx-media3-ui` after the media3-ui dep removal;
  `android-application` plugin alias — sample apps have their own catalog).

Compose / Kotlin convention:
- VideoPlayer: move `modifier` to the first optional parameter position
  per Compose guidelines. Sole caller uses named args, no break.
- HtmlRenderer: drop unused `val context = LocalContext.current`, the
  redundant `@Suppress("UNUSED_PARAMETER") webView` parameter, and 2
  unused imports (`Uri`, `LocalContext`).
- Uri.parse(...) → String.toUri() via androidx.core.net (3 sites:
  ActionHandler, VideoRenderer ×2).
- Action.kt + NativeDisplayConfigParser.kt: 2 IntroduceWhenSubject
  refactors and 2 unused imports removed.
- NativeDisplayUnitCacheImpl: drop redundant `org.json.` qualifier.
- StyleResolver: `styleClasses` ctor param loses redundant `val` (used
  only at init for the styleClassMap derivation).
- sdk AndroidManifest: drop unused `xmlns:android` declaration.

Lint suppressions with rationale (NOT silent ignores):
- NativeDisplayViewGroup.onDetachedFromWindow: empty override is the
  *documented design* — comment explains why we deliberately don't clear
  state on detach. @Suppress("RedundantOverride") + comment preserved.
- CleverTapAutoWire.wireListener / ReflectionSeeder.logOnce: each always
  returns a constant Boolean, but that constant IS the success contract
  the public callers use. @Suppress("FunctionOnlyReturningConstant")
  with comments documenting the convention.

Test:
- ActionAttributionExtrasTest: add named args to Action.OpenUrl call and
  restore the missing `com.clevertap.android.nativedisplay.BuildConfig`
  import (got stripped from the test file at some point — caught by the
  build after these fixes).

Verified: ./gradlew :sdk:clean :sdk:assembleRelease :sdk:testReleaseUnitTest passes.

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

* chore(samples): align SDK coordinate with new published artifactId

The publishing setup renamed the Maven artifactId
  com.clevertap.android:native-display-sdk
  → com.clevertap.android:clevertap-native-display-sdk
to match the Core SDK family naming convention (`clevertap-*-sdk`). The
two sample apps both declared the SDK dependency and registered a
composite-build `dependencySubstitution` rule using the old GAV. The
build kept working because substitution matches whatever GAV the app
declares — but anyone copying the sample's `implementation(...)` line
into a real consumer app would hit a 404 on Maven Central.

Update 4 sites to the new artifact name:
  - android-sample/settings.gradle.kts        (substitution)
  - android-sample/app/build.gradle.kts       (implementation)
  - android-xml-sample/settings.gradle.kts    (substitution)
  - android-xml-sample/app/build.gradle.kts   (implementation)

Refresh the xml-sample's substitution comment to reflect the actual
purpose (route the published coordinate to the local :sdk project for
working-tree development).

Verified: both samples' `./gradlew :app:assembleDebug` passes.

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

* chore(publish): align POM display name with artifactId

Rename POM <name> from "CleverTap Native UI Kit" → "CleverTap Native
Display SDK" so the artifact's display label (shown on Maven Central,
mvnrepository.com, and in IDE dep tooltips) matches the artifactId
`clevertap-native-display-sdk` and the iOS module `CleverTapNativeDisplay`.

The repo name itself stays as `clevertap-native-ui-kit` — that's the
URL slug for the umbrella project that hosts both Android and iOS SDKs.

Verified: `./gradlew :sdk:generatePomFileForMavenPublication` emits
`<name>CleverTap Native Display SDK</name>`.

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

* fix(android-video): preserve aspect ratio + minor cleanups

Aspect-ratio regression fix:
- After dropping media3-ui in the publishing PR, the bare TextureView
  stretched to fill its container, ignoring the source's intrinsic
  ratio. media3-ui's PlayerView had previously wrapped the surface in
  AspectRatioFrameLayout which listened to onVideoSizeChanged and
  resized internally — that was the FIT behaviour users saw before.
- Restore it by:
  - Tracking video aspect in state, updated from
    Player.Listener.onVideoSizeChanged (multiplied by
    pixelWidthHeightRatio for anamorphic content; guarded for
    zero/NaN edge cases).
  - Applying Modifier.aspectRatio(videoAspect) to the TextureView
    in both the inline and fullscreen render sites.
  - Wrapping the inline player in an inner Box with
    contentAlignment = Alignment.Center so the aspect-sized view
    centres within the host bounds (letter/pillar-boxing).
  - Passing videoAspect through to FullscreenVideoContent.
- Default 16:9 used until the first frame is decoded.

Style cleanups (no behaviour change):
- Use imported Image instead of fully-qualified
  androidx.compose.foundation.Image inside VideoControlIcon.
- VideoPlayerWithMedia3 visibility: internal → private (only called
  from VideoPlayer within this file).

Verified: ./gradlew :sdk:assembleRelease :sdk:testReleaseUnitTest passes.

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

* chore(release): address CodeRabbit findings on PR #73

CodeRabbit flagged 5 findings; 1 was already fixed in 02b26d9 (video
aspect ratio). The remaining 3 distinct issues are addressed here:

1. (Major) android/sdk/api/sdk.api was stale after the namespace flip.
   The baseline still referenced `com/clevertap/android/nativeui/BuildConfig`
   and `Media3PlayerListener` as a public class — both wrong for the
   renamed module. Regenerated via `./gradlew :sdk:apiDump`.

2. (Trivial → API surface win) Media3PlayerListener was declared
   `open class` (public + open), exposing an internal Media3
   compatibility shim as a published SDK API and obligating us to
   maintain it forever. It's only used internally inside VideoRenderer
   as `object : Media3PlayerListener() { ... }`. Changed visibility to
   `internal open class` so it drops out of the API dump.

3. (Minor) iOS NativeDisplaySDKVersion.swift had inconsistent comments:
   the file-level doc said "iOS version is owned independently of
   Android" but the `name` field comment still said it "mirrors
   `/VERSION` and the podspec". Removed the `/VERSION` mention from the
   field comment so both blocks tell the same story.

Net effect on api/sdk.api: -45 lines (Media3PlayerListener and all its
40+ inherited overrides drop out of the published surface; BuildConfig
package path updates from `nativeui` → `nativedisplay`).

Verified: ./gradlew :sdk:assembleRelease :sdk:testReleaseUnitTest :sdk:apiCheck passes.

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

* chore(android): ship consumer ProGuard rules in the AAR

The SDK's build.gradle.kts referenced `consumer-rules.pro` and
`proguard-rules.pro` but neither file existed, so the published AAR
shipped no `proguard.txt`. Every consumer enabling R8/ProGuard/DexGuard
on their release build would have hit silent breakage:

  - kotlinx-serialization (30+ @serializable classes, 7 sealed
    hierarchies): R8 obfuscates field names → JSON keys no longer match
    → MissingFieldException at runtime → blank UIs.
  - Sealed-class polymorphism (Action / Background / NativeDisplayNode):
    R8 renames subclass names → JSON `type` discriminator doesn't match
    → wrong subtype instantiated.
  - Public listener interfaces (NativeDisplayActionListener,
    NativeDisplayComponentListener, ...): R8 renames methods → consumer
    overrides silently never invoked.

For 1.0.0 we take the safe path: keep the entire
`com.clevertap.android.nativedisplay.**` package tree as-is. The SDK
contributes ~900 KB AAR regardless; method-count overhead is minor for
a UI rendering SDK. Future tightening (surgical -keep rules scoped to
serialization + reflection + public API) is gated on a CI smoke test
that builds a consumer app with `isMinifyEnabled = true`.

Also adds standard kotlinx-serialization rules to retain the synthetic
`$$serializer` companions explicitly (R8 sometimes inlines and drops
these even with `-keep class ... { *; }`) and `-dontwarn` lines for
the three `compileOnly` deps that may be absent at consumer build time.

`proguard-rules.pro` is created as a placeholder for the SDK's own
release variant (`isMinifyEnabled = false` today; file exists so the
`proguardFiles(...)` reference doesn't dangle).

Verified: AAR now ships `proguard.txt` (2.3 KB); republished to
`~/.m2` and confirmed contents.

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

* fix(android): keep Core SDK reflective targets so element-level attribution survives R8

Bug: in a consumer release build (R8 / minifyEnabled = true), the
NativeDisplayBridge's reflective lookup for
`CleverTapAPI.pushDisplayUnitElementClickedEventForID(String, HashMap)`
returns null, so attribution silently degrades to
`pushDisplayUnitClickedEventForID(String)` and the per-element extras
map never reaches the server.

Root cause: Core SDK's own consumer-rules.pro keeps several sub-packages
(FCM, ExoPlayer integration, push templates) but does NOT keep
CleverTapAPI itself. R8 in the consumer's build sees no direct call to
the element-clicked method (we invoke it via reflection by name only),
so it's eligible for stripping/renaming. The bridge's `getMethod(...)`
probe then returns null and we fall back to the legacy unit-level path.

Fix: ship targeted -keepclassmembers rules from our SDK's
consumer-rules.pro for every Core SDK method we look up reflectively:
  - CleverTapAPI.pushDisplayUnitElementClickedEventForID(String, HashMap)
  - CleverTapAPI.pushDisplayUnitViewedEventForID(String, HashMap)
  - CleverTapAPI.pushDisplayUnitClickedEventForID(String)
  - CleverTapAPI.pushDisplayUnitViewedEventForID(String)
  - CleverTapAPI.setDisplayUnitCache(...) / setDisplayUnitListener(...)
  - CleverTapAPI.getDefaultInstance(Context) / getDebugLevel()
  - CleverTapDisplayUnit.toDisplayUnit / getUnitID / getJsonObject
  - DisplayUnitCache interface (for the dynamic Proxy)
  - CTWebInterface (HTML JS bridge)

Listed individually rather than as a blanket `-keep class CleverTapAPI`
so consumer R8 still shrinks the rest of Core SDK normally.

Also adds the CleverTap official integration rules to the Compose sample
app's proguard-rules.pro (Core SDK + Firebase / FCM + Parcelable /
Serializable safety) so the sample serves as a working integration
template for consumers reading our code.

Verified: rebuilt and re-published to ~/.m2; AAR's bundled proguard.txt
now contains the new -keepclassmembers rule for
pushDisplayUnitElementClickedEventForID.

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

* fix(ios-video): surface AVPlayerItem failures instead of silent black surface

Cross-platform parity with Android's error overlay. Previously, an
unsupported format (.webm, .mkv, DASH .mpd), 404, decoder failure, or
network drop on iOS left the VIDEO element rendering as a silent black
square — AVPlayer fails silently by design. No crash, but no UX signal
either, so brands couldn't tell a broken URL from a loading delay.

PlayerManager additions:
- `@Published var errorMessage: String?` — drives the new error overlay.
- KVO observation on `AVPlayerItem.status` via NSKeyValueObservation —
  catches load-time failures (unsupported format, 404, malformed
  manifest). Closure may fire on a background thread, so we dispatch
  to main before mutating @published state.
- NotificationCenter observer on `.AVPlayerItemFailedToPlayToEndTime` —
  catches mid-playback failures (network drop, decoder error after
  start) that don't transition `status` back to `.failed`.
- Both paths log via NDLogger.e with the underlying NSError's
  localizedDescription for diagnostics.
- `cleanup()` invalidates the KVO observation and removes the new
  notification observer alongside the existing observers.
- `errorMessage` is reset to nil on each new `setupPlayer()` call so a
  fresh URL clears any prior error state.

UI changes:
- VideoPlayerView (inline) and VideoFullscreenView (modal) both render
  a DarkGray-on-white error overlay when `playerManager.errorMessage`
  is non-nil. Inline matches Android's style (small text, padded);
  fullscreen scales the text up to match the larger surface.
- Controls are hidden while the error overlay is showing — there's
  nothing meaningful to play / mute / expand.

Net effect: both platforms now show identical "your video failed"
messaging for every category of failure. Neither platform crashes; iOS
no longer silently swallows the failure.

Verified: xcodebuild -scheme CleverTapNativeDisplay -destination
'generic/platform=iOS Simulator' build → ** BUILD SUCCEEDED **.

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

* chore(video): align Android/iOS failure logging + drop marginal iOS observer

Two cleanups on top of 0ccd8e8:

1. Synonymous logging — both platforms now emit "Playback error: <details>"
   so a developer grepping logs sees the same prefix regardless of
   platform. The on-screen overlay text was already identical
   ("Video playback failed"); now the log lines match too.

   Android:
     before: "Playback error (${error.errorCodeName})"
     after : "Playback error: ${error.errorCodeName} — ${error.message}"

   iOS:
     before: "AVPlayerItem failed: ${msg}"
     after : "Playback error: ${detail}"

2. Drop iOS AVPlayerItemFailedToPlayToEndTime observer. The KVO probe
   on AVPlayerItem.status already catches the 95% case (load-time
   failures: unsupported format, 404, malformed manifest, decoder
   error). The notification observer added a second path for
   mid-playback drops that wouldn't transition `.status` back to
   `.failed` — real but rare, and not worth the extra observer
   lifecycle. Easy to add back later if a customer hits it.

Removed from PlayerManager:
  - failureObserver: NSObjectProtocol? field
  - NotificationCenter.default.addObserver(... .AVPlayerItemFailedToPlayToEndTime)
  - corresponding removeObserver in cleanup()

Verified: both ./gradlew :sdk:assembleRelease :sdk:testReleaseUnitTest
and xcodebuild -scheme CleverTapNativeDisplay -destination
'generic/platform=iOS Simulator' build pass.

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

* feat(bridge): tag CleverTap Core SDK with Native Display wrapper version

Calls `setCustomSdkVersion("Native Display", ND_LIB_VERSION_CODE)` on the
attached CleverTap Core SDK instance so server-side analytics can attribute
"Notification Viewed" / "Notification Clicked" events back to a specific
Native Display SDK build rather than the host integration. Without this,
ND-driven events were indistinguishable from events the host app fires
directly through Core SDK.

Android (`CleverTapAutoWire.kt`):
- Added `CUSTOM_SDK_NAME = "Native Display"` constant + `@Volatile
  taggedInstance: CleverTapAPI?` identity guard.
- New `tagCustomSdkVersion(ctApi)` helper wrapped in `runCatching` for
  graceful degradation on older Core SDK builds that lack the method.
- Invoked once inside `wireListener(...)` immediately after
  `bridge.cleverTapApi = ctApi` — single insertion point covers both the
  `tryAutoWire(context, bridge)` and `bindToInstance(...)` entry paths,
  since both funnel through `wireListener`.
- Compile-time direct call against `compileOnly` Core SDK 7.5.0; no
  reflection needed.

iOS (`CleverTapAutoWire.swift`):
- Added `customSdkName = "Native Display"`, the pre-resolved Selector
  `setCustomSdkVersion:version:`, and a `weak var taggedInstance: NSObject?`
  identity guard (weak so we don't extend the Core SDK instance lifetime).
- New `tagCustomSdkVersion(_:)` static using `responds(to:)` defensive
  probe + `class_getMethodImplementation` + `@convention(c)` IMP-cast.
  IMP-cast is required because the second argument is Obj-C primitive
  `int` (32-bit fixed-width), which `perform(_:with:with:)` can't carry
  correctly through `Any`. Cast narrows `NativeDisplaySDKVersion.code`
  (Swift `Int`, pointer-sized) to `Int32` at the call site.
- Invoked from both `tryAutoWire(bridge:)` and `bindToInstance(...)`
  after `bridge.cleverTapInstance = ...` since iOS doesn't have a shared
  `wireListener`-equivalent funnel. `tearDown()` clears `taggedInstance`
  so a subsequent re-wire (post-clear) re-tags.

Coverage audit: every assignment to the bridge's CleverTap reference
(`bridge.cleverTapApi` Android / `bridge.cleverTapInstance` iOS) on every
public entry path now invokes `tagCustomSdkVersion(...)`. Both fields are
module-`internal` so consumers cannot bypass the tagging path.

Behavior parity: both platforms emit the same success log
"Tagged Core SDK with Native Display version <code>" and the same
warn-level fallback when the method is absent.

Verified:
  - Android: ./gradlew :sdk:clean :sdk:assembleRelease :sdk:testReleaseUnitTest :sdk:apiCheck — green
  - iOS: xcodebuild -scheme CleverTapNativeDisplay -destination 'generic/platform=iOS Simulator' build — ** BUILD SUCCEEDED **

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

* feat(video-a11y): expose stable test identifiers on VIDEO element controls

TalkBack / VoiceOver labels were already wired (contentDescription on
Android, accessibilityLabel on iOS) — those drive screen-reader speech
and flip with state ("Play" ↔ "Pause"). They're not stable enough for
automation, which needs a state-independent handle.

Add stable identifiers as a separate channel so Espresso / Compose-UI
test / UI Automator / Maestro / XCUITest / Appium suites can target each
control without depending on the localized, state-mutable label.

Android — `Modifier.testTag(...)` (Compose's canonical handle):
- Six public top-level constants in `com.clevertap.android.nativedisplay.renderer`:
    ND_VIDEO_TEST_TAG_PLAY        = "nd_video_play"
    ND_VIDEO_TEST_TAG_MUTE        = "nd_video_mute"
    ND_VIDEO_TEST_TAG_ACTION_URL  = "nd_video_action_url"
    ND_VIDEO_TEST_TAG_EXPAND      = "nd_video_expand"
    ND_VIDEO_TEST_TAG_CLOSE       = "nd_video_close"
    ND_VIDEO_TEST_TAG_COLLAPSE    = "nd_video_collapse"
- `VideoControlIcon` composable accepts a required `testTag: String` and
  threads it into `Modifier.testTag(testTag)`. All 9 call sites updated.
- `sdk.api` baseline regenerated for the 6 new public constants.

iOS — `.accessibilityIdentifier(...)` (separate from `.accessibilityLabel`
which is what VoiceOver reads):
- Public `NDVideoAccessibilityID` enum with the same string values so a
  cross-platform Appium/Maestro suite uses one ID per control:
    NDVideoAccessibilityID.play        = "nd_video_play"
    NDVideoAccessibilityID.mute        = "nd_video_mute"
    NDVideoAccessibilityID.actionUrl   = "nd_video_action_url"
    NDVideoAccessibilityID.expand      = "nd_video_expand"
    NDVideoAccessibilityID.close       = "nd_video_close"
    NDVideoAccessibilityID.collapse    = "nd_video_collapse"
- `VideoControlIcon` view accepts a required `accessibilityIdentifier`
  and applies `.accessibilityIdentifier(...)` after the existing
  `.accessibilityLabel(...)`. All 9 call sites updated.

Re-use across modes: inline and fullscreen are mutually exclusive at
any moment, so PLAY / MUTE / ACTION_URL re-use the same ID across both —
a test "tap play" works regardless of mode. Mode-specific buttons
(EXPAND for inline → fullscreen entry, CLOSE / COLLAPSE for fullscreen
exit) get unique IDs.

Existing accessibilityLabel / contentDescription unchanged — TalkBack
and VoiceOver continue reading "Play" / "Pause" / etc.

Verified:
  - Android: ./gradlew :sdk:assembleRelease :sdk:testReleaseUnitTest :sdk:apiCheck — green
  - iOS:     xcodebuild -scheme CleverTapNativeDisplay -destination 'generic/platform=iOS Simulator' build → ** BUILD SUCCEEDED **

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

* Apply suggestion from @coderabbitai[bot]

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Sonal Kachare <sonalkachare29@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Repo convention is unprefixed tags (1.0.0, not v1.0.0). Update the
docs-site workflow's tag filter to match so the versioned snapshot
deploys when the release tag is pushed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`android/local.properties` was committed back in SDK-5404 with only the
`sdk.dir` line. Since then it became the natural home for per-developer
publish credentials (`mavenCentralUsername`, `mavenCentralPassword`,
`signing.*`), and developers populating it for the 1.0.0 release would
otherwise see those secrets appear in `git status` as a modification on
a tracked file — easy to accidentally stage and push.

The `.gitignore` already has `**/local.properties` (line 146) which
covers this path, but ignore rules don't apply to files already tracked.
Remove from the index with `git rm --cached` so the ignore rule kicks
in for future modifications.

Verified locally: the file stays on disk with all dev secrets intact
(`ls -l` shows 557 bytes, contents untouched), but `git status` no
longer reports it.

No secrets land in this commit — the only historical content under
`HEAD:android/local.properties` was `sdk.dir=…`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.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