Intempt SDK for React Native. Wraps intempt-swift on iOS and intempt-android on Android.
Not yet releasable. One native prerequisite is outstanding — see Status. The JavaScript layer, the bridge contract and the test suite are complete and reviewable now.
| 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.1.0 |
| Android native module | complete against intempt-android 3.0; 3 push methods reject |
| iOS distribution | published — Intempt 0.1.0 on CocoaPods trunk |
| Android distribution | blocked — needs intempt-android 3.0.0 on Maven Central, latest is 2.0.1 |
npm install intempt-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+.
import { init } from 'intempt-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.
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`);await intempt.identify('user-123', { userAttributes: { email: 'a@b.com' } });
await intempt.group('acct-9', { accountAttributes: { tier: 'enterprise' } });
await intempt.alias('user-123', 'anon-abc');
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.
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.
await intempt.productView('sku-1');
await intempt.productAdd('sku-1', 2);
await intempt.productOrdered([
{ productId: 'sku-1', quantity: 2 },
{ productId: 'sku-2', quantity: 1 },
]);import { ConsentAction } from 'intempt-react-native';
await intempt.consent(ConsentAction.Accept, 1798761600, { email: 'a@b.com' });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.
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.
const products = await intempt.products({ feedId: 'feed-1', count: 10 });Experiment and personalization assignment is not in the mobile SDKs — it is an intemptjs capability. Recommendation feeds are a different thing 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.
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.
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.
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.
const eu = await init({ ...config, instanceName: 'eu' });
const us = await init({ ...config, instanceName: 'us' });Each instance has its own credentials, queue and identity.
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.
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.
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.
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.
Apache 2.0. See LICENSE and NOTICE — the package structure is adapted from mixpanel-react-native, also Apache 2.0.