Skip to content

feat(in-app): allow overriding the in-app message color scheme - #660

Merged
mahmoud-elmorabea merged 5 commits into
mainfrom
mbl-2479-rn-inapp-color-scheme
Sep 17, 2026
Merged

mahmoud-elmorabea merged 5 commits into
mainfrom
mbl-2479-rn-inapp-color-scheme

Conversation

@mahmoud-elmorabea

Copy link
Copy Markdown
Contributor

Exposes the native SDKs' in-app color scheme override so a host app with its own appearance setting can pin the light or dark variant instead of always following the device.

  • inApp.colorScheme on the SDK config, and CustomerIO.inAppMessaging.setColorScheme for changing it at runtime, both taking the new CioColorScheme enum.
  • iOS needed no change on the init path — MessagingInAppConfigBuilder.build(from:) already parses the key, so only the Android half of that path was missing. The runtime setter is new on both.
  • Messages already on screen re-theme in place; that is native behavior this only reaches.

Fixes MBL-2479
Closes #659

🤖 Generated with Claude Code

Hosts with their own appearance setting could not tell the SDK which of the
light/dark variants authored in the editor to render — it always followed the
device. Adds `inApp.colorScheme` to the SDK config and a runtime
`CustomerIO.inAppMessaging.setColorScheme`, both taking the new
`CioColorScheme` enum.

Both native SDKs already implement this, including re-theming messages that
are already on screen, so this only exposes what is there. iOS needed no
change on the init path: `MessagingInAppConfigBuilder.build(from:)` already
parses the `colorScheme` key out of the wrapper config, so only the Android
half of that path was missing.

The enum's string values are the wire contract both native SDKs match
lowercase, and each resolves an unrecognized value to `auto`. A re-cased value
would therefore render the device's theme with no error raised, so the values
are pinned by test and validated in JavaScript, which is the only layer that
can report the mistake to the developer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Sample app builds 📱

Below you will find the list of the latest versions of the sample apps. It's recommended to always download the latest builds of the sample apps to accurately test the pull request.


Builds are in progress. This comment will be updated when they finish.

  • Android APN: 660.5.0 (29827606)
  • iOS APN: 660.5.0 (29827606)

mahmoud-elmorabea and others added 2 commits September 17, 2026 12:45
Adds `CioColorScheme`, the `inApp.colorScheme` field and `setColorScheme` to
the checked-in public API report, and gives the new `@param` the hyphen TSDoc
requires — api-extractor treats that warning as a failure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…on Android

Review follow-ups on the color scheme override. Both were platform
divergences in the new wrapper code, not in the native SDKs.

iOS dropped a `setColorScheme` call made before `CustomerIO.initialize` in
near silence: it forwards through the module's `implementation?`, which is nil
until the SDK is initialized, and the only trace was an `.info` line that the
default `.error` log level discards. Android already logged that case at
error, so the same mistake was actionable on one platform and invisible on the
other. Both wrappers now guard on `hasBeenInitialized` and log at error, and
still complete the call, which is what Android does.

Android could crash on a null scheme. Codegen declares the parameter non-null,
so the non-null Kotlin type meant `Intrinsics.checkNotNullParameter` threw
before the body ran — reachable from untyped JavaScript passing a stored theme
that is null on first launch, where iOS merely logs. The override is now
nullable, like every other argument-taking override in that class, and a null
value is reported rather than thrown. The mapper lost its logging so each
caller can describe its own fallback: initialization drops to the device
appearance, the setter keeps the scheme the app already chose.

Also drops three tests that could not fail. `initialize` forwards the config
verbatim, so asserting `colorScheme` on the forwarded payload only re-read the
literal the test itself built — it stayed green whether or not the Android key
and either native mapper worked. Removed rather than reworked: the integration
points that can actually break live on the native side of each bridge, and
this package has no harness for them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mahmoud-elmorabea
mahmoud-elmorabea marked this pull request as ready for review September 17, 2026 11:32
@mahmoud-elmorabea
mahmoud-elmorabea requested a review from a team as a code owner September 17, 2026 11:32

@mrehan27 mrehan27 left a comment

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.

Color scheme wiring looks right — same wire values as customerio-flutter#410, and the strict setter / lenient init split is consistent on both platforms.

cioNativeiOSSdkVersion is still = 4.8.0 while 4.8.1 is out and customerio-flutter#410 takes it. Worth bumping here so both wrappers ship the same native iOS version; Android is already on 4.21.1.

Picks up the native SDK's swift-eventsource pin. 4.8.0 declared it as
`.upToNextMajor(from: "3.3.0")`, and LaunchDarkly's 3.3.1 raised that
package's iOS floor to 15.0; 4.8.1 pins it back to exactly 3.3.0.

This wrapper resolves the native SDK through CocoaPods rather than
Package.swift, so it was not affected by that resolution break — this keeps
the pin current alongside the same bump on the Flutter side, and duplicates
the bot's PR #661, which can be closed.

`package.json`'s cioNativeiOSSdkVersion is the only pin site; the podspec and
the example app's Podfile both read it from there.

Verified locally: pod install upgrades the CustomerIO pods to 4.8.1 and the
example app's SDK target still builds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
/// it to `.auto`, so a typo cannot quietly undo a scheme the app set correctly earlier — the
/// Android bridge behaves the same way.
@objc(setColorScheme:)
public func setColorScheme(_ colorScheme: String) {

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.

Could we make this parameter optional and handle nil before mapping? JavaScript callers can still pass null or undefined at runtime even though the Codegen spec says string; the React Native bridge forwards that as nil, and bridging it into Swift non-optional String can trap before colorScheme(fromRawValue:) runs. Android explicitly accepts nullable input in this PR, so the platforms currently diverge. A String? plus an early guard would keep the invalid-input path non-fatal on iOS too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in dac2ef9 — good catch, the divergence was real.

The Swift parameter and its mapper now take String?, and nil is reported like any other unrecognized value (Unrecognized in-app colorScheme 'nil'…) rather than trapping.

One deviation from the suggestion worth flagging: I left the ObjC forwarder in NativeMessagingInApp.mm on the protocol's nonnull signature rather than annotating it nullable. The generated RNCustomerIOSpec.h sits inside NS_ASSUME_NONNULL_BEGIN, so the declaration there is implicitly nonnull and a nullable implementation would conflict with it. ObjC does not enforce nonnull at runtime, so a JavaScript null still arrives as nil and is forwarded to Swift, which is where the tolerance now lives.

Verified on a simulator: calling the setter before CustomerIO.initialize logs at error, and a bad value leaves the current scheme in place instead of resetting it to auto.

Comment thread src/customerio-inapp.ts
* @param colorScheme - scheme to render with; `CioColorScheme.Auto` returns to following
* the device appearance
*/
setColorScheme(colorScheme: CioColorScheme) {

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.

Could we apply the same validation to the runtime setter as well? validateColorScheme only runs from validateConfig, so initialization warns for colorScheme: DARK, while CustomerIO.inAppMessaging.setColorScheme(DARK) forwards the invalid value without a JavaScript warning. Since this public method is also reachable from untyped JavaScript, reusing the validator here would keep the two entry points consistent and make the mistake visible before it reaches the native fallback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in dac2ef9. setColorScheme now runs the validator too, so both entry points warn.

I split the validator rather than reusing it as-is, because the two paths have genuinely different semantics:

  • Unrecognized value — initialization resolves to auto natively, while the setter leaves whatever scheme the app already chose in place. The warnings now say which of those happened, so the message matches the actual outcome.
  • Absent value — legitimate in the config (the native SDKs already default to AUTO, so it is skipped), but a mistake in the setter, where there is nothing to fall back to. The setter therefore reports null and undefined instead of skipping them, which also covers the null case from the Swift thread.

Added tests for all three cases.

Both from PR review.

The iOS setter took a non-optional `String` while Android took a nullable one,
so the platforms disagreed on the same input: untyped JavaScript can pass null
or undefined, the bridge forwards it as nil, and bridging nil into a
non-optional `String` traps before the value can be reported. The Swift
parameter and its mapper are now optional, and nil is logged like any other
unrecognized value. The ObjC forwarder keeps the Codegen protocol's nonnull
signature — ObjC does not enforce it at runtime, so nil still reaches Swift,
and annotating it nullable would only conflict with the generated header.

`setColorScheme` also now validates its argument. `validateColorScheme` ran
only from `validateConfig`, so `initialize` warned about a bad value while the
public setter forwarded one silently. The validator is split in two because
the native fallback differs per entry point: an unrecognized value at
initialization resolves to `auto`, while the setter leaves the scheme the app
already chose in place. Absent is also treated differently — legitimate in the
config, where the native default stands, but a mistake in the setter, which
has nothing to fall back to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mahmoud-elmorabea
mahmoud-elmorabea merged commit 223363c into main Sep 17, 2026
18 of 19 checks passed
@mahmoud-elmorabea
mahmoud-elmorabea deleted the mbl-2479-rn-inapp-color-scheme branch September 17, 2026 15:20
cio-mobile-release Bot pushed a commit that referenced this pull request Sep 17, 2026
## [6.12.0](6.11.0...6.12.0) (2026-09-17)

### Features

* **in-app:** allow overriding the in-app message color scheme ([#660](#660)) ([223363c](223363c))
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.

Ability to override in app messages theme

3 participants