SDK-5788: React Native SDK - #37
Draft
akashvercetti wants to merge 52 commits into
Draft
Conversation
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)
… gallery ancestors/parents
…vent host app crashes for malformed json
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>
* 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>
Contributor
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
…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.
…nd native renderers behave.
…the video can open a link.
…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>
* 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.