@intempt-technologies/react-native
v0.2.0
Published
Intempt SDK for React Native — event tracking, identity, consent and personalization
Readme
@intempt-technologies/react-native
Intempt SDK for React Native. Wraps intempt-swift on iOS and intempt-android on Android.
Not yet published to npm. Both native prerequisites are satisfied — see Status. The JavaScript layer, the bridge contract and the test suite are complete and reviewable now; publishing this package is the one remaining step.
Status
| Piece | State |
|---|---|
| JavaScript / TypeScript layer | complete |
| TurboModule spec + codegen config | complete |
| Contract fixture corpus | complete, 32 fixtures over 29 methods |
| iOS native module | complete, typechecked against Intempt 0.2.0 |
| Android native module | complete against intempt-android 3.1; 3 push methods reject |
| iOS distribution | published — Intempt 0.2.0 on CocoaPods trunk |
| Android distribution | published — intempt-android 3.1.0 on Maven Central |
| npm distribution | not yet published — release workflow ready, no tag cut yet |
Install
npm install @intempt-technologies/react-nativecd ios && pod installNo JavaScript dependencies. Everything this package needs is generated natively.
Requirements: React Native 0.76+, iOS 15.1+, Android API 24+.
Where this SDK runs — and where it does not
This SDK is mobile-only: Android and iOS devices, simulators and emulators. It is a wrapper over the native Intempt SDKs, so it works exactly where they do and nowhere else.
- No web. react-native-web has no native module. On any platform without one, importing
the SDK is safe, but
init()(and every other call) rejects with anIntemptErrorwhoseisUnsupportedis true — catch it and fall back to intemptjs. A cross-platform app should split at bundle time (analytics.native.ts/analytics.web.ts); a runtimePlatform.OScheck is too late for web bundlers that cannot resolve the native import. - No desktop. react-native-windows / react-native-macos are not supported — same
isUnsupportedrejection as web. - No servers. Backend tracking belongs to the Node.js SDK (or PHP/Python), with server credentials — never this package.
- No Expo Go. Native modules require a dev build:
npx expo prebuild, thenexpo run:android/expo run:ios. - Production only. The delivery endpoint (
https://api.intempt.com) is compiled into the native SDKs and is not configurable. Credentials from a non-production environment will queue events locally and deliver nothing. - Platform gaps are errors, not crashes. A method the current platform's native SDK does
not implement (for example push methods on Android before
intempt-androidcovers them) rejects withisUnsupported— the same shape as the wrong-platform case, so one branch handles both.
Quick start
import { init } from '@intempt-technologies/react-native';
const intempt = await init({
apiKey: 'yourPrefix.yourSecret',
orgId: 'your-org',
projectId: 'your-project',
sourceId: 'your-source',
});
await intempt.track('Signed up', { plan: 'pro', seats: 3, trial: false });init() resolves to an instance. Every method on it returns a Promise.
Verifying it worked
track() resolves to whether the event was accepted into the queue — not whether
it was delivered.
const queued = await intempt.track('Signed up');
if (!queued) {
// opted out, invalid property, encoding failure, or storage unavailable
}
const delivered = await intempt.flush();
console.log(`${delivered} events delivered`);API
Identity
await intempt.identify('user-123', { userAttributes: { email: '[email protected]' } });
await intempt.group('acct-9', { accountAttributes: { tier: 'enterprise' } });
await intempt.getProfileId();
await intempt.getSessionId();
await intempt.logOut(); // rotate identity, keep the queue
await intempt.reset(); // rotate identity AND empty the queuelogOut() exists so the next person using a shared device does not inherit the previous
identity. reset() additionally discards events not yet delivered. They are not
interchangeable.
Events
await intempt.track('Viewed pricing', { source: 'nav' });
await intempt.record('Renewed', {
userId: 'user-123',
accountId: 'acct-9',
data: { mrr: 120 },
});Property values may be strings, numbers, booleans, null, Date, arrays or nested
objects. Date crosses the bridge as ISO 8601 and is re-typed natively.
Commerce
await intempt.productView('sku-1');
await intempt.productAdd('sku-1', 2);
await intempt.productOrdered([
{ productId: 'sku-1', quantity: 2 },
{ productId: 'sku-2', quantity: 1 },
]);Consent
import { ConsentAction } from 'intempt-react-native';
await intempt.consent(ConsentAction.Accept, 1798761600, { email: '[email protected]' });Three behaviours to know:
- Consent transmits even when the user is opted out — a withdrawal has to reach the server.
- It goes to its own endpoint, unbatched.
Rejectopts out;Acceptopts in. You do not need to calloptOut()yourself.
Opt in / out
await intempt.optOut(); // stops collection AND discards the queue
await intempt.optIn();
await intempt.hasOptedOut();
await intempt.isOptedIn();optOut() discards events already collected. Setting a flag alone would leave events
gathered before the objection to be uploaded after it. Queued consent records are
preserved — they are the evidence of the decision.
Feature flags
// Ask for a KEY, never a mode. Whether the key names an experiment, a personalization
// or a flag is the platform's business — its serving query filters on channel and
// status and never on mode, so this call does not change when that does.
const on = await intempt.boolVariation('new_checkout', { userId: 'user-123' }, false);
const copy = await intempt.stringVariation('checkout_copy', { userId: 'user-123' }, 'Buy now');
const limit = await intempt.numberVariation('free_shipping', { userId: 'user-123' }, 50);
// A payload is arbitrary JSON. You branch on it; there is no visual editor for a
// native surface, so the value is authored as a payload in the studio.
const theme = await intempt.variation<{ accent: string }>(
'checkout_theme',
{ userId: 'user-123' },
{ accent: '#000' }
);
const all = await intempt.allFlags({ userId: 'user-123' });defaultValue is required, and it is a real decision. It is what renders when Intempt
cannot be reached — a 5xx, a timeout, an unknown key. Choose the behaviour you already have.
A flag lookup never throws for a service failure on either platform.
A wrong-typed value falls back; it is never coerced. A flag configured as a string and
read with boolVariation returns your default, not true. Boolean('false') is true, and
a silent coercion is indistinguishable from a deliberate value.
A programming error does throw. A blank key, a key outside ^[a-zA-Z0-9_-]+$ (the server's
own pattern), or an undefined default is something you can fix, so it fails at the call site
rather than quietly returning a default that looks like a flag being off.
FlagContext takes userId and profileId, both optional. Omit profileId and the native
SDK supplies the device identifier it already holds — the one that survives sign-in, and
therefore the one that keeps a person's assignment stable across it.
variationDetail is deliberately absent. See docs/CONVENTIONS.md for why, and for the
requirement that is still open at the platform.
Both native pins resolve to a release that carries the flag surface, as of 2026-08-31:
Intempt 0.2.0on CocoaPods trunk andcom.intempt.sdk:intempt-android:3.1.0on Maven Central. Before those releases existed the pins selected 0.1.0 and 3.0.4, and a consumer's first build failed withcannot find 'FlagContext' in scope.npm run check:native-pinsre-measures both registries on every CI run — it downloads what each pin selects and looks for the symbol, so a version number is never taken as evidence.
Recommendations
const products = await intempt.products({ feedId: 'feed-1', count: 10 });Recommendation feeds are a different thing from flags and from assignment, and are here.
products() defaults fields to productId, title, price, imageUrl, url.
Do not widen it by omission. An unfielded request returns every catalog column including raw ML embedding vectors — measured at 443x the payload for the same ten products, 503 bytes against 222,919. Ask for the columns your screen renders.
Automatic events
await intempt.setAutomaticEvents({
sessions: true,
versionChanges: false,
appStateChanges: false,
});Only sessions is on by default. An SDK that silently emits events you never asked for
is how an event-volume bill surprises someone.
Autocapture
Different from automatic events, and easy to confuse with them. Automatic events are lifecycle facts the SDK already knows. Autocapture hooks the view layer — on iOS it swizzles UIKit — so it installs nothing until you start it.
await intempt.autocapture.configure({
screenViews: true,
controlInteractions: true,
});
await intempt.autocapture.start();
await intempt.autocapture.isRunning();
await intempt.autocapture.stop();configure() alone changes nothing. start() is the point at which instrumentation is
installed.
The two options map onto finer native ones. On iOS, screenViews covers screen
appearances and exits; controlInteractions covers button presses and value changes.
iOS's rawTouches is deliberately not exposed here — a tap on a control already emits
its own event, so enabling raw touches alongside it double-counts every button press.
Push
await intempt.setPushToken(hexToken);
await intempt.trackPushOpen(notification.data);
await intempt.trackPushReceived(notification.data);iOS: setPushToken takes the APNs token as a hex string, since Data has no bridge
representation.
Android: registration needs Google Play Services. An emulator running the default
system image has none — use a google_apis image, or token registration fails in a way
that is hard to read.
Multiple instances
const eu = await init({ ...config, instanceName: 'eu' });
const us = await init({ ...config, instanceName: 'us' });Each instance has its own credentials, queue and identity.
Errors
Every rejection is an IntemptError with a code.
import { IntemptError, IntemptErrorCode } from 'intempt-react-native';
try {
await intempt.track('e');
} catch (error) {
if (error instanceof IntemptError) {
if (error.isUnsupported) {
// contract method not on this platform yet
} else if (error.isRetryable) {
// transport or 5xx; error.retryAfter may be set
}
}
}A 401 is terminal, not retryable — a bad credential cannot succeed on retry. The
queued events are kept, because the data is valid and the integration is what is broken.
Platform gaps
A contract method missing on one platform rejects with unsupported_on_android or
unsupported_on_ios plus the method name. It never resolves silently.
Currently unsupported on Android, pending intempt-android 3.0: reset,
getProfileId, getSessionId, flush, getFlushInterval, setFlushInterval,
experiments, products, getAutomaticEvents, setAutomaticEvents, the whole
autocapture object, setPushToken, trackPushOpen, trackPushReceived.
Android also ignores the credentials passed to init() until 3.0 — it reads
android/app/src/main/assets/intempt-config.json. init() fails loudly when that file
is absent rather than reporting success and sending events nowhere.
The contract
This package implements
intempt-swift/docs/SDK-API-CONTRACT.md,
the surface every Intempt client SDK conforms to. docs/CONTRACT.md beside it defines
the wire.
Conformance is enforced by a fixture corpus, not by review:
node scripts/check-corpus.mjs # no dependencies; runs before npm install
npm run typecheck # tsc
npm test # 57 tests
./scripts/typecheck-ios.sh # the Swift bridge against the real SDKtypecheck-ios.sh resolves every Intempt symbol the bridge uses against an actual
intempt-swift build. It does not verify React Native itself — the promise blocks are
stubbed, so @objc export shape, the RCT_EXTERN_METHOD declarations, autolinking and
codegen still need a real pod install and an Xcode build.
Adding a method to the TurboModule spec without a fixture fails the build.
Contributing
npm install
npm run typecheck
npm test # 131 tests
npm run mutation # Stryker; gate is 95, currently 99.03
./scripts/typecheck-ios.sh # Swift bridge against the real SDKThe package runs on Node 18+. The dev toolchain needs Node 22+ — Stryker refuses anything older, which is how CI failed while it passed locally on 23.
Design and open questions: docs/superpowers/specs/2026-08-15-intempt-reactnative-design.md.
License
Apache 2.0. See LICENSE and NOTICE — the package structure is adapted from mixpanel-react-native, also Apache 2.0.
