Skip to content

Add AirshipEmbeddedCarousel component - #774

Open
khmMouna wants to merge 9 commits into
mainfrom
MOBILE-5981-embedded-carousel-is-not-available-in-react-native
Open

khmMouna wants to merge 9 commits into
mainfrom
MOBILE-5981-embedded-carousel-is-not-available-in-react-native

Conversation

@khmMouna

@khmMouna khmMouna commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

What do these changes do?

Adds an AirshipEmbeddedCarousel component backed by the native carousel on both platforms (AirshipEmbeddedCarousel in AirshipScenes on iOS, and in urbanairship-automation-compose on Android). It shows every pending instance for an embedded ID as swipeable pages with the default chrome (indicator, arrows).

Props: embeddedId, selection, style, currentPage (settable), and the callbacks onPageChanged, onPageCountChanged, onAvailabilityChanged, onStateChanged. Sizing follows the same rules as AirshipEmbeddedView.

  • JS: new component and codegen spec, registered in package.json.
  • iOS: Swift wrapper hosting the SwiftUI carousel with hoisted state, a Fabric/Paper view, and bridge protocols.
  • Android: ComposeView-based view and manager. Adds the Compose compiler plugin and the Compose BOM (runtime, ui, foundation) to android/build.gradle. The carousel artifact (urbanairship-automation-compose) now comes from airship-framework-proxy, bumped to 16.0.2 in gradle.properties.
  • Example app: a Carousel card on the Embedded Views screen.

Why are these changes necessary?

React Native only exposed AirshipEmbeddedView, which shows one instance at a time, so apps could not display a multi-instance embedded placement the way a native app can. Custom chrome is supplied as native view builders and does not bridge, so the default chrome is used.

Anything else a reviewer should know?

  • Depends on proxy 16.0.2, which is not released yet. The proxy change (airship-mobile-framework-proxy) adds urbanairship-automation-compose as an api dependency and updates the Android SDK to 21.0.3, so Android resolves the carousel artifact transitively. Until 16.0.2 is on Maven Central, Android builds of this branch will not resolve
Screen_recording_20261008_175034.webm
Screen.Recording.iPhone.17.Pro.08-10-2026.at.17.51.16.mp4

Adds a carousel backed by the native AirshipEmbeddedCarousel on iOS
(AirshipScenes) and Android (urbanairship-automation-compose). Every pending
instance for an embedded ID is shown as a swipeable page with the default
chrome. The current page can be set from JS via currentPage, and page,
page count and availability are reported through events.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@khmMouna
khmMouna requested a review from a team as a code owner October 8, 2026 14:53
@khmMouna
khmMouna marked this pull request as draft October 8, 2026 15:05
@khmMouna khmMouna self-assigned this Oct 8, 2026
@khmMouna
khmMouna marked this pull request as ready for review October 16, 2026 13:30
@Ulrico972

Ulrico972 commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Code Review

Took a look through the Android/iOS/JS changes — the carousel state handling (page clamping, availability reporting, event parity between platforms) looks solid and follows the existing AirshipEmbeddedView patterns closely.

One thing worth a second look before merging:

android/build.gradle unconditionally applies the Compose Kotlin compiler plugin (org.jetbrains.kotlin.plugin.compose, needs Kotlin ≥ 2.0.0) to the library module — for every consumer of react-native-airship, not just apps using the new carousel. Apps that still pin an older Kotlin version (1.9.x is still common on RN 0.72–0.74) could fail to resolve this plugin and have their Android build break entirely, not just lose the carousel.

Is raising the minimum Kotlin version here intentional and documented somewhere (CHANGELOG/README), or should the Compose dependency be scoped so it only applies when the carousel is actually used?

Also a minor nit: the compose-bom version (2025.10.01) is hardcoded rather than pulled from a property like the other versions in this file.

🤖 Generated with Claude Code

@jyaganeh

Copy link
Copy Markdown
Contributor

Sorry I'm late to this, but I think that adding Compose may cause issues because it'll require us to apply the Compose compiler to the Gradle build for all frameworks, and I'm not sure that they'll all work well with it.

I don't think it's a bad idea to try (maybe it will work ok and I'm just being overly cautious?), but we may need to go back to the Android repo to add a carousel built with XML Views if Compose can't be brought into all the frameworks cleanly (Cordova and Unity are the ones that I'm particularly unsure about).

khmMouna and others added 2 commits October 19, 2026 09:30
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…/height

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

// Falls back to the screen size until the first layout pass, matching the
// embedded view's sizing rules.
private var parentSize: CGSize {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

parentWidth/parentHeight are optional so the SDK can resolve percent placements against the SwiftUI proposal; parentSize makes them never nil, and because the guard requires both axes, a zero height replaces the width too. The wrapper is built from initWithFrame:CGRectZero, so the first layout pass sizes percent-based content against the whole screen. main has since replaced this fallback in ReactAirshipEmbeddedView.ViewModel with per-axis nil — a rebase would bring the comment above back in line too.

- (instancetype) init {
self = [self initWithFrame:CGRectZero];
if (self) {
self.wrapper = [[RNAirshipEmbeddedCarouselBridgeClass() alloc] initWithFrame:self.bounds];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

RNAirshipEmbeddedView sets clipsToBounds = YES right here for the same hosted scene content. Without it a carousel taller than the box Yoga assigned paints over whatever sits below — in the example screen, the Instance Selection card under the 300pt container.


self.viewModel.size = frame.size

self.cancellable = Publishers.CombineLatest(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No removeDuplicates, and the SDK reassigns currentPage on every pending-set change, often to the value it already held, so identical state crosses the bridge repeatedly. onStateChanged is the one callback the JS side doesn't filter against lastState, so it fires each time with a fresh object. The Android path dedupes the same stream.


@objc(setPage:)
public func setPage(_ page: Int) {
guard page != lastRequestedPage else { return }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

lastRequestedPage is never cleared, so once a page index has been requested it can't be requested again for the life of the view — and Fabric pools these, so a recycled wrapper carries the previous placement's value. Is the guard there for anything beyond redundant updateProps calls?

This branch has not been deployed

No deployments
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.

4 participants