Skip to content

feat(live-activities): expose Live Activities through CustomerIO.liveActivities - #617

Draft
mahmoud-elmorabea wants to merge 12 commits into
feature/live-notificationsfrom
live-activities-sdk
Draft

feat(live-activities): expose Live Activities through CustomerIO.liveActivities#617
mahmoud-elmorabea wants to merge 12 commits into
feature/live-notificationsfrom
live-activities-sdk

Conversation

@mahmoud-elmorabea

@mahmoud-elmorabea mahmoud-elmorabea commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Exposes Live Activities (iOS) / Live Notifications (Android) on the React Native SDK, consuming the native SDK-managed module so registration happens at SDK init.

Config lives under liveNotificationstypes are the reverse-DNS identifiers shared with both native SDKs; unrecognized ones are ignored. end optionally takes a final content-state so iOS can render a terminal card.

Depends on customerio-ios#1178.

…K module

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

github-actions Bot commented Jul 23, 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.


  • iOS FCM: 617.12.0 (29752921)
  • Android APN: 617.12.0 (29752921)
  • iOS APN: 617.12.0 (29752921)

@mahmoud-elmorabea
mahmoud-elmorabea changed the base branch from main to feature/live-notifications July 23, 2026 06:03
mahmoud-elmorabea and others added 2 commits July 26, 2026 07:39
…to liveNotifications

Follows the native iOS change that makes Live Activities a registerable
CustomerIOModule reached via CustomerIO.liveActivities.

- iOS: replace the held static module + initializeModule(from:) with
  module(from:) registered through sdkConfigBuilder.addModule, mirroring
  NativeLocation. Enables push-to-start on every init path.
- iOS: a nil start result (module unregistered / type not enabled) becomes a
  rejected promise instead of a crash; unknown types fail softly.
- iOS: reportDeepLinkOpen -> handleWidgetUrl, returning the redirect URL.
- Android: migrate to the dedicated CustomerIOLiveNotificationsCallback and
  setLiveNotificationCallback; createLiveNotification no longer lives on
  CustomerIOPushNotificationCallback.
- Config: `liveActivities` -> `liveNotifications`, `templates` -> `types`, and
  types are now the reverse-DNS identifiers shared by both native SDKs and the
  wire format. Unrecognized identifiers are ignored.
- Branding: add logoResource (bundled drawable), preferred over logoUrl.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ending an activity left the last content-state on screen — a Segments activity
ended at "3 of 4" and stayed there. ActivityKit keeps the previous content when
`Activity.end` is given none, and the bridge never passed one even though the
native handle has always accepted it.

`end` now takes an optional payload, mirroring `update`, so callers can show a
terminal state (e.g. all segments complete).

iOS-only by nature: Android's `endLiveNotification(activityId)` takes no final
state and renders its own terminal presentation (the notification stops being
ongoing), which is why Android already looked correct. The Android bridge
accepts the argument for signature parity and ignores it, the same way branding
is Android-only and ignored on iOS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mahmoud-elmorabea mahmoud-elmorabea changed the title feat(live-activities): expose Live Activities / Live Notifications SDK module feat(live-activities): expose Live Activities through CustomerIO.liveActivities Jul 27, 2026
mahmoud-elmorabea and others added 9 commits July 27, 2026 08:41
TEMPORARY (REL-1). Live Notifications don't exist in any released native SDK,
so CI cannot build these branches against published versions.

- Android: pin the `live-notifications-review-fixes-SNAPSHOT` branch snapshot and
  add the Sonatype snapshots repository to resolve it.
- iOS: bump the native pin to 4.6.0, matching the version the Live Activities
  podspecs declare on the native branch. The sample Podfile supplies those pods
  via a :git/:branch override, since the LA subspecs are not on trunk yet.

Revert all of this — snapshot repository, snapshot pin, and Podfile overrides —
once Live Notifications ships in released native SDKs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Regenerates the api-extractor report, which had no Live Activities entries.

Also fixes what the report surfaced: `{@link start}` and `{@link logoUrl}` were
unqualified and unresolvable, and LiveActivityTemplate, LiveActivitySegmentsPayload
and LiveActivityCountdownTimerPayload were missing `@public` — the `@public` in the
file header sat on a detached comment block and applied to nothing, so it is removed.

Formats storage.ts, whose import was collapsed past the 80-char print width.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…odule log

`update` rejected on an id this process didn't start while `end` resolved, and
Android resolves for both because it routes the id straight to the native SDK.
"Unknown" is not proof of caller error either: the map is process-local, so an
activity started before a restart, or started by a push, legitimately isn't in it.
Both now resolve and log instead.

The push-module lookup used runCatching around an `as?` cast, which yields null
rather than throwing, so its onFailure branch was unreachable and the "push module
is not initialized" error never reached anyone. Replaced with an explicit null check.

Renames the Kotlin config parameter to liveNotificationsConfig and corrects a KDoc
that named a `liveActivities` config key that does not exist: config and wire
vocabulary is liveNotifications, the API accessor stays liveActivities.

Documents why Live Activities registration is unconditional on Android, unlike the
Location module beside it, and that customTypes is Android-only in practice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Forwarding opened URLs to handleWidgetUrl is mandatory on iOS or Live Activity taps
go unattributed, and nothing in the SDK can do it for the host. Android needs no
equivalent, and the Expo config plugin injects it automatically — so this is the one
platform where an integrator has to act, and it was undocumented.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Custom activity types were Android-only in practice and start-only. iOS rejected
them outright, because ActivityKit needs a concrete Swift type to register an
activity and observe its push-to-start token, and a metatype cannot cross a bridge.

The native SDK now ships CIOCustomAttributes, whose content is an untyped map, so
the SDK owns the Swift type and JavaScript supplies the values. Naming the type via
`liveNotifications.customType` registers it under the app's own reverse-DNS
identifier, which means iOS custom activities get push-to-start and metrics for the
first time, and Android and iOS finally share one code path.

`customType` is singular because an activity's reported type is resolved from its
attributes type: registering the one type twice would attribute every event to
whichever identifier came first, so the limit is expressed in the API rather than
left as a silent mis-attribution.

Removes `startCustom`. `start`/`update`/`end` with a LiveActivityTemplate.Custom
payload replace it and, unlike it, work on both platforms and support updates.
Android routes those through the native map overloads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…idn't start

`update` was made to resolve on an unknown id, on the reasoning that the id might
belong to a push-started activity legitimately absent from the process-local map.
That reasoning was wrong: the bridge exposes no listener, so an id only ever reaches
JavaScript as the return value of `start` and a push-started id can never be passed
back in.

What remains is an update that did not happen being reported as success — and
Android performs it in the same situation, since it routes the id straight to the
native SDK. Resolving here made one call return success on both platforms while only
one of them did anything.

`end` keeps resolving: re-ending an already-ended activity is a legitimate no-op.
Nothing about an update that never applied is idempotent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Android returned a real activity id for a type that was never enabled. The native
start call mints one regardless, and the handler then drops the notification at debug
level, so from JavaScript the call looked like it worked and nothing rendered. It now
rejects with the same code and message iOS already used.

`customActivityType` was only assigned when `customType` was present, so a
re-initialize that dropped it kept the stale value. Android would then start under an
identifier no longer allowlisted — straight into the silent drop above. Both platforms
now assign unconditionally.

iOS coerced missing required fields to "" and 0 while Android rejected them, so an
untyped caller got a blank card on one platform and an error on the other. iOS now
rejects too, naming the field.

`end` removed the handle before awaiting, so a throw lost it and the retry took the
unknown-id path and reported success for an activity still on screen. It now forgets
the handle only once the end succeeds.

Drops the `withNativeModule` wrapper: the module is registered unconditionally on both
platforms, so its null branch and __DEV__ warning were unreachable and its comment
wrong. `getEnforcing` matches every other module here except Location, which really
can be absent.

`types` no longer accepts `Custom`, which both platforms silently dropped, and the
custom `data` doc now states that nested values are unsupported and diverge.

Also: import order, and the README snippet was missing `import customerio_reactnative`
and the subspec prerequisite that the sample app has.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…r have seen

The previous commit recorded the enabled activity types while applying the wrapper's
own config, then rejected any start whose type wasn't in that set. That holds only
when the SDK config is built from JavaScript. It isn't always: the Expo config plugin
generates native code that calls `.enableLiveNotificationTypes(...)` straight on the
app's config builder, so the wrapper's hook never runs, the recorded set stays empty,
and every Android start would have been rejected — built-in templates included.

Android now reads `SDKComponent.pushModuleConfig.liveNotificationTypes`, which is
authoritative whoever built the config, and derives the custom identifier from it when
the wrapper didn't see one: with a single custom type it is the one enabled identifier
no built-in LiveNotificationType claims.

iOS drops the pre-check for the same reason and lets the SDK answer — a nil handle
already means "not registered", on every initialization path. That leaves the stored
identifier unread, so it is removed.

Caching what the wrapper happened to observe was the mistake; asking the SDK is both
shorter and correct.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e branch

The per-PR branch this snapshot was named after was deleted when it merged, so
its artifact is frozen. The base feature branch it merged into publishes a
snapshot from the same open PR and now carries the review-fix API this wrapper
calls.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant