Skip to main content
Use @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 with expo-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:
Run 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

Use createBareAdapter 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:
Galinum’s receiver handles Galinum envelopes and passes every other message to React Native Firebase. Then capture launch intents in MainActivity before the superclass calls. Import android.os.Bundle and android.content.Intent:
The SDK’s merged bare manifest adds its private ingress service and 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:
Keep GalinumJournal autolinking. Add the separate receipt pod inside the app target:
Use the installed package path for your project, then run 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 only GalinumReceiptStore 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’s Image. 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:
For another React Native version, match Fresco in the installed 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 distinct storageKey. Keep provider credentials and project secrets on the server.
For self-hosting, replace 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. After the app resolves authentication, await client.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

A NotificationInteraction 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.
This example records an application event and intentionally ignores test sends. Add your router’s destination handling inside the callback to open content. Use HTTPS for website destinations. Explicitly allow the app schemes and routes that your application supports. Recheck application identity before navigation after any asynchronous work; session guards protect SDK operations, not arbitrary application side effects. Call 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 the web_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.
Mount one lifecycle for the active route. Pass the router’s committed route key, screen path, and readiness into 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.