@galinum/react-native with your project publishable key and API origin.
Expo and bare apps use the same public installation lifecycle,
push, and in-app contracts on self-hosted Galinum and Galinum Cloud.
The app owns login, permission timing, product consent, and navigation.
The integration has six steps: install the native dependencies, create one
client, register notification presentation, confirm identity and consent,
register the tap handler once your router is ready, and mount in-app messaging.
Finish with a correlated test send to a selected device.
Install the native dependencies
Use a native development build. Expo Go cannot load Galinum’s native journal. Rebuild after installing native dependencies or changing native configuration. A JavaScript export alone does not verify native linking or notification delivery. The package manifest and Expo example manifest define the example dependency set: React Native 0.86.3, React 19.2.3, and Expo 57.0.20. Follow your React Native and Expo versions’ platform requirements; the SDK’s Gradle minimum alone does not set your app’s minimum OS. The package setup guide lists exact peer versions and the native configuration in detail.Expo
Install the SDK withexpo-notifications, expo-secure-store, expo-crypto,
expo-dev-client, react-native-mmkv, and react-native-nitro-modules.
Register the config plugins in this order, and set the Android package and iOS
bundle identifier to the app ID you pass to the client:
npx expo prebuild, then build a development client with
npx expo run:android or npx expo run:ios.
On Android, the Galinum plugin registers the Galinum FCM service and adds
notification capture to MainActivity. Galinum receives data-only FCM messages
and captures both activity creation and new intents. Keep that integration when
customizing the generated project; a JavaScript listener alone cannot capture
every cold-start interaction.
On iOS, the plugin adds GalinumReceiptStore beside the autolinked
GalinumJournal pod. It does not create a Notification Service Extension.
Enable Push Notifications in the signed target. Match the APNs topic and
the client’s environment to the signing entitlement. Notification images need
a Notification Service Extension built on the package’s separate receipt
component, which keeps installation credentials and the journal key out of the
extension. Follow the package setup guide for the extension target.
The Expo adapter reads getDevicePushTokenAsync: APNs on iOS and FCM on Android.
Galinum sends directly through those providers and never registers an Expo Push
Service token. See Expo Notifications.
Bare React Native
UsecreateBareAdapter from @galinum/react-native/bare. This entry point does
not import Expo modules. Install @react-native-firebase/app,
@react-native-firebase/messaging, react-native-permissions,
react-native-keychain, react-native-get-random-values, react-native-mmkv,
and react-native-nitro-modules at the versions in the package setup guide.
Register the target platforms in Firebase with matching app IDs. Install
google-services.json on Android and GoogleService-Info.plist on iOS.
Initialize Firebase as
React Native Firebase describes.
On Android, target SDK 33 or later, declare POST_NOTIFICATIONS, and replace
the React Native Firebase receiver with Galinum’s in AndroidManifest.xml.
Declare xmlns:tools="http://schemas.android.com/tools" on the root manifest
element. Place these receivers inside application:
MainActivity before the
superclass calls. Import android.os.Bundle and android.content.Intent:
WAKE_LOCK
permission. The existing onCreate(Activity, Bundle) and onNewIntent hooks
capture cold launches and later taps. Keep the supplied Bundle intact for
Android’s saved-state recovery. No additional host lifecycle hook, service
declaration, or Expo dependency is required.
On iOS, enable Push Notifications and add the permissions library’s
Notifications handler. Use iOS 16.4 or later for the receipt pod. For
React Native Firebase 26.4.0 with static frameworks, set these Podfile options:
GalinumJournal autolinking. Add the separate receipt pod inside the app target:
pod install and rebuild.
$RNFirebaseDisableSPM = true selects the CocoaPods path for this RNFirebase setup.
The SDK supports static-framework headers without an app header-search workaround.
Initialize other notification libraries before Galinum. If the app assigns a
notification-center delegate later, call [GalinumNotifications install] after
that assignment.
The bare adapter reads
getAPNSToken() on iOS and getToken() on Android. An iOS Firebase
registration token is not an APNs token. Firebase auto-init and automatic
remote registration remain application settings; product consent controls
whether Galinum synchronizes an eligible token. See
React Native Firebase messaging.
Set up iOS notification images
For notification images, add a Notification Service Extension target. Link onlyGalinumReceiptStore into the extension. Subclass GalinumNotificationService
and select that subclass as the extension’s principal class. Embed the extension
in your application target.
Set GalinumReceiptAppGroup and GalinumReceiptKeychainGroup in both targets’
Info.plists. Use matching, dedicated App Group and Keychain Sharing entitlements.
Set GalinumNotificationService to YES in the extension Info.plist. Keep
installation credentials and the journal key outside these receipt groups.
The app creates a separate device-only receipt key. The extension writes encrypted,
authenticated receipt files; the app imports them into its journal. The extension
cannot open the journal or send installation API requests. Missing receipt setup,
attachment failure, or extension expiration preserves the original notification text.
Rich-image capability requires the embedded extension and receipt-key setup.
Background alert presentation remains controlled by iOS. Neither provider
acceptance nor an SDK receipt establishes that the extension executed or an image
appeared. Validate the signed extension with your app’s notification delivery path.
Image formats
Managed campaign media accepts PNG, JPEG, WebP, and GIF. Android notification images load over HTTPS, at most 1 MiB, without redirects, and decode to a still bitmap; a GIF shows its first frame. iOS notification images need the service extension, which accepts JPEG, PNG, and GIF up to 5 MiB. Failed attachment loads preserve notification text. In-app media renders through React Native’sImage. Animated GIF playback on
Android needs Fresco’s animated-gif module. Enable GIF support in the generated
Expo project with expo.gif.enabled=true in android/gradle.properties.
For a bare React Native 0.86.3 app, add this dependency to
android/app/build.gradle:
react-native/gradle/libs.versions.toml and check the resolved dependency graph.
This module enables in-app animation; Android notification images remain still.
See React Native Image support.
Create one client
Create the client outside React renders. Use one client per project, app, and environment in one JavaScript runtime, with a distinctstorageKey.
Keep provider credentials and project secrets on the server.
apiBase with your server’s origin, without a path.
Use HTTPS outside loopback development. A physical device’s localhost is the
device itself, so choose an API address reachable from that device.
The provider starts the client without prompting. Without React context, await
client.start(). The provider does not dispose the shared client on unmount;
call dispose() when its application owner shuts down permanently.
Register notification presentation
notifications describes foreground policy, Android channels, named actions, and
iOS categories. Category actions references the named action IDs in order.
Native setup reports the capabilities it registered through installation facts.
Do not advertise a channel, category, action, or rich-image capability that the
native integration cannot support.
Keep the Expo adapter’s permission channel consistent with the configured Android
channel. Campaign android.channelId must match a registered channel. Campaign
iOS categories must match the exact ordered action IDs and rendered titles.
Android validates registered action IDs and displays the payload’s personalized
titles. An iOS personalized title must also match its registered category title. See native actions and content.
Choose foreground: 'display' to permit foreground notification presentation,
or 'suppress' when your foreground experience handles the update. This policy
does not change product consent or OS permission. Suppression does not mean the
provider failed or the device did not receive the notification.
Android supports up to three action buttons; iOS categories support up to four.
The OS can show fewer actions. Notification images follow the
image format rules above. In-app message images use a separate
renderer and need no push extension.
Confirm identity, permission, and consent
After the app resolves authentication, awaitclient.identify(userId, traits?).
On an anonymous launch or logout, await client.reset(). Restoring saved SDK
state does not confirm that the application is still signed in as that user.
Never infer identity from a notification’s payload.
Initialization never requests permission. Invoke requestPermission() from your
app’s explanation or button. Invoke setConsent(true) only after the user’s
separate product opt-in. OS authorization does not imply product consent.
The two operations are independent; do not couple them in a startup effect.
syncDevice() refreshes permission and native token state without prompting.
Call it after returning from OS settings. The adapter also listens for provider
token refresh and uploads a changed token while the installation stays eligible.
An unavailable token read preserves an existing registration. Unchanged tokens
do not increase the token revision.
To opt a user out, await setConsent(false). This clears the server token and
restricts native display before network reconciliation completes. OS permission
and the provider’s device token remain untouched. On iOS, incomplete notification
cleanup rejects the operation. Follow the iOS cleanup recovery steps
before reporting opt-out as complete.
Call recordForegroundActivity() only for actual app use. Initialization,
identification, tracking, and token callbacks do not establish activity for
last_active device selection. Events and in-app messaging do not require push
permission or push consent.
Switching users clears product consent and prior-user activity. Reset immediately
invalidates old session callbacks and waits for durable local restriction and
remote binding reconciliation. Await reset before reporting the local restriction
and remote unbind as complete. A network or storage failure needs recovery and retry.
Handle taps and actions after router readiness
ANotificationInteraction contains stable id, kind, optional actionId,
userId, bindingGeneration, targetId, attemptId, and test. It also carries
destination, string-map data, title/body, and receipt/interaction timestamps.
Destinations are { kind: 'website' | 'app', url }.
Register client.setNotificationHandler only when your router can accept navigation.
Registration declares router readiness. The app must also explicitly confirm
identity. Retained interactions from another user or binding generation cannot
become actions for the new account.
stopHandling() when the router stops being ready.
Handlers run at least once. Successful completion acknowledges handling; a
thrown error leaves the interaction pending for later dispatch. Process failure
between a side effect and acknowledgement can run that side effect again. Use
interaction.id as the durable deduplication key for application effects.
Use the handler’s captured session for events and other delayed SDK operations.
Its methods reject after an account change or invalidated handler. A late direct
client.track() call would target the user current at invocation. The native
journal records tap/action order before invoking the handler, so later events
retain their causal order.
For push conversion tracking, link the campaign to a goal with a stored target
event. Record the actual product action through the SDK after the tap or action.
The ordered sender places that event after its interaction for server attribution.
Opening a notification alone does not prove that the target action occurred.
Test notifications never convert a campaign.
Render in-app messages
Native and web render theweb_inapp campaign channel. There is no separate
native_inapp channel. A decision returns { userId, entryId, requestId, messages };
each message carries delivery, campaign, variant, and content identity.
See the shared decision and feedback protocol.
Use the client’s existing ports for in-app integration. client.inApp provides
session snapshots, subscriptions, and fresh decisions. client.feedback provides
local completion checks, durable feedback admission, and flushing through the
same client’s sender. Reuse these ports instead of creating another store or sender.
Keep one in-app controller per client outside React renders. Its lifecycle and
rendering hosts must share that controller. Generate a unique ID for every entry
and request. Use a framework UUID, or combine client.inApp.getSnapshot().owner
with an increasing counter. Keep that counter for the owner’s lifetime, including
controller remounts. A pathname or a counter restarted on each render is not unique.
Supply the current route key, normalized screen path, and router readiness to
the in-app lifecycle. Use paths such as /exports that match campaign pages.
A new route entry or foreground return needs a fresh server decision for the
confirmed user. Renders, cached content, and old responses do not authorize a new
message. Failed or timed-out decisions settle empty; a later entry can try again.
Compose the controller with the client created above. Keep the ID counter beside
the client so screen or rendering-host remounts do not reset it.
ScreenMessages. Confirm authentication through
identify() separately. Replace example with your registered app scheme and
connect it to your router. Website destinations open through React Native Linking.
For custom navigation, implement openDestination with your application’s router.
Keep the controller alive across screen changes. Call inAppController.dispose()
when its client owner shuts down permanently, alongside client.dispose().
The native renderer supports toast and modal, managed image media, and
light, dark, or auto theme selection. Toasts use compact image thumbnails;
modals use larger images. Image loading failure leaves the message copy and
controls usable. Provide meaningful image alt text or decorative: true.
A custom renderer receives the message and asynchronous dismiss, click, and
retry actions. Use those actions to preserve feedback and completion ordering.
Returning null, undefined, or false skips that candidate without a shown
report. A committed native presentation reports shown after layout or modal
presentation. Keep custom output visible and accessible; layout is not proof
that a person read it.
One entry consumes at most one committed message. Durable terminal feedback
requires its same-entry shown predecessor before click/dismiss can complete.
The renderer keeps failures available for retry. It rechecks the rendering host
after feedback waits before starting navigation. Feedback retains the captured
user across logout and restart. Local completion blocks redisplay immediately;
other clients suppress it after server acknowledgement and their next decision.
Web and native share the project/user/campaign delivery and variant. There is
no global display lease: two already-authorized entries can display concurrently.
Remote completion does not retract an already-painted message. Do not promise
exactly-once display across devices or offline clients.
Durable events and recovery
track(event, props?, { eventId }?) returns { eventId, state }. queued confirms
an encrypted local transaction. acknowledged reports a stored server
acknowledgement. Await client.flush() for acknowledgements through its captured
watermark. Keep a business event ID and identical data for uncertain retries.
EventAdmissionError.eventId retains that ID when admission is uncertain.
SecureStore/Keychain holds installation credentials. Native code owns the separate
journal key. SQLCipher stores operational state and ordered pending work. Missing
keys, missing journal state, and storage pressure fail visibly without clearing
rows or resetting sequence cursors. Do not repair these failures by silently
wiping storage or starting another writer.
Use useGalinum() operations or capture client.session() before delayed work.
Snapshots contain sanitized status and installation facts, never authentication
capabilities or token values. The package recovery guide
covers scope changes, key rotation, uncertain writes, and retry rules.
On Android, the SDK retains pending tap capture through Activity saved-state
restoration and deduplicates committed interactions. A failed notification removal
retains its recovery record for a later capture, startup, or control operation.
A normal app update preserves the installation and journal. Keep the app ID,
signing identity, SDK scope, and storage namespace unchanged. Do not clear app
data to upgrade. Recovery cannot reconstruct records already lost by an older build.
Android records the affected notification handles with each control change.
Required cancellation completes before revocation or recovery reports success.
If cancellation fails, setConsent(false) rejects with notifications_unavailable.
Keep the opt-out selected, preserve storage, and retry the operation after
notification services recover. Restart also resumes pending cleanup. Same-user
confirmation preserves valid notifications, and completed cleanup does not become
a new sweep of later notifications.
Recover iOS notification cleanup
On iOS, the journal saves required notification cleanup together with the user or consent change. The cleanup request identifies the installation and affected binding generations. Native response capture persists independently of OS notification queries. A cleanup timeout does not discard a captured tap. Native presentation and routing wait for required cleanup. Routing also requires confirmed identity and router readiness. Cleanup removes affected pending notifications before delivered notifications, then verifies both lists. Incomplete cleanup stays in the journal across restart. Later cancellation continues the same request. Generation checks protect newer bindings and other installations. Expired callbacks cannot cancel newer notifications. If OS notification cleanup times out,setConsent(false) rejects with
GalinumError.code === 'notifications_unavailable'. Keep the user’s opt-out
selected and show that cleanup is incomplete. Retry and await setConsent(false)
after notification services recover. If reset fails, retry and await reset().
Restart also recovers pending cleanup. Preserve storage and the user’s consent choice
during recovery.
Successful iOS revocation and reset require completion of their notification cleanup.
The cleanup covers notifications already pending or delivered in the captured
scope. It cannot recall arbitrary future provider messages or prevent their
OS-controlled background presentation.
Diagnose native delivery
The campaign supervision dashboard shows stored push
content and the public inspection records. Author campaigns through the management API.
Use a correlated selected-device test.
Keep its request ID and compare provider acceptance, SDK receipt, and actual
on-device presentation separately. Exercise foreground, background, cold launch,
permission revocation, opt-out, and account switching in your signed builds.