@zally-sdk/sdk-react-native
v1.7.0
Published
React Native SDK for zally – one npm package that wraps the native iOS and Android zally SDKs
Readme
@zally-sdk/sdk-react-native
React Native SDK for zally — wraps the native iOS and Android SDKs behind a single JavaScript API.
Requirements
- React Native >= 0.80
- iOS 17.0+
- Android: minSdk 23, compileSdk 36, Java 17, AndroidX, Kotlin 2.2 or later
The React Native floor is set by Android, not by this package's JavaScript.
com.zally:zally-sdk depends on androidx.core:core-ktx:1.18.0, which requires
the consuming app to compile against API 36 with Android Gradle plugin 8.9.1 or
newer. React Native 0.80 is the first release whose template ships an AGP that
new: 0.79 ships 8.8.2, 0.78 ships 8.8.0, 0.77 ships 8.7.2, 0.76 ships 8.6.0.
Raising AGP yourself on an older React Native does not work either. AGP 8.9.1
needs Gradle 8.11 or newer, and React Native's own Gradle plugin compiles with
-Werror on those releases, so it fails on Gradle's deprecation warnings before
your app is reached.
Installation
npm install @zally-sdk/sdk-react-native
# or
yarn add @zally-sdk/sdk-react-nativeiOS
Expo managed projects — add the plugin in app.json:
{
"expo": {
"plugins": ["@zally-sdk/sdk-react-native"]
}
}Then run npx expo prebuild or npx expo run:ios.
Bare React Native — set your deployment target to iOS 17.0, add the zally pod to your Podfile:
pod 'zally-sdk', :podspec => 'https://raw.githubusercontent.com/zally-ltd/zally-ios-sdk-package/3.1.0/zally-sdk.podspec'Then cd ios && pod install && cd ...
Native iOS version resolution
The native iOS version is pinned, and ZallySdk.podspec holds the pin. The Expo
config plugin reads that pin at prebuild time and writes the Podfile line above,
so Expo and bare React Native resolve the same native version from the same
wrapper version. Upgrading the native SDK therefore takes a release of this
package, the same as on Android.
The plugin previously pointed at the public podspec on its default branch, which
carried whatever version was current at pod install time. That let one wrapper
version resolve two different native versions, put an iOS SDK publish into Expo
apps without a React Native release, and made the public podspec a shared
recovery surface for anyone rolling back.
The Podfile line carries a tag ref, and that ref is the pin: it fixes what the
URL resolves to. No version requirement sits alongside it, because CocoaPods
rejects a dependency that has both an external :podspec source and a version
requirement, failing pod install before resolution starts.
Moving the pin means editing s.dependency 'zally-sdk' in ZallySdk.podspec
and the Podfile line above, then releasing this package. A unit test fails if the
two disagree, and the version check in main-build.yml fails if the pin trails
the latest published iOS SDK tag.
Upgrading from 1.6.6 or earlier (Expo): no manual action. Run
npx expo prebuild (or npx expo run:ios) and pod install as usual. The
plugin replaces its own generated block in ios/Podfile, so a Podfile carried
over from an earlier prebuild picks up the pinned line, and the changed podspec
URL makes CocoaPods re-resolve Podfile.lock. Bare React Native apps should
replace the pod line in their Podfile with the one above. If the version you
were resolving from the default branch had moved ahead of the pin, this moves
you back to the pinned version.
The plugin can only replace a generated block whose two @generated marker
comments are both intact. If the markers were edited or partly deleted, it
appends a fresh block and leaves the old pod line in place, and pod install
then fails reporting a conflicting external source for zally-sdk. Delete the
old zally block from ios/Podfile and run pod install again.
Android
No extra setup — the native dependency is resolved automatically.
Usage
import zally from '@zally-sdk/sdk-react-native';
// 1. Initialize (call once after user login)
await zally.initialize({
appUUID: 'your-app-uuid',
// Called by the SDK, not once by this library: immediately during initialize,
// and again whenever the SDK needs to recover its own tokens. Fetch a current
// JWT on every call. Returning a captured or cached one leaves the SDK unable
// to recover after it expires.
getClientToken: async () => {
return await yourBackend.getZallyToken();
},
});
// 2. Start
await zally.start();
// 3. Check state
const state = await zally.getUserState();
console.log(state?.trainingState, state?.zIdStatus, state?.isUser);
// 4. Stop (e.g. on logout)
await zally.stop();API
| Method | Returns | Description |
| --------------------------------------------------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| initialize({ appUUID, getClientToken, baseUrl? }) | Promise<void> | Initialize and authorize the SDK. Call once after user login. getClientToken is called by the SDK repeatedly, not once: see below. baseUrl requires the internal edition: see below. |
| start() | Promise<void> | Start sensor data collection and streaming. Call after initialize(). |
| stop() | Promise<void> | Stop the SDK (e.g. on user logout). |
| isInitialized() | Promise<boolean> | Whether the SDK reports itself initialized. Platform-dependent: see below. |
| isRunning() | Promise<boolean> | Whether the SDK is actively collecting data. Authoritative on iOS only: see below. |
| getUserState() | Promise<UserState \| null> | Current user state (training progress, identity verification). Maps to Android userState / iOS BiometricState. |
getClientToken is called more than once
The native SDKs take a token-supplying function and re-invoke it: once when authorizing, and again whenever they need to recover their own tokens, for example after a refresh token expires. This library passes your function through to them rather than resolving it to a string.
So getClientToken must return a currently valid JWT every time it is
called. A common mistake is to close over a token fetched at login:
// Wrong: the SDK can never obtain a fresh token, so it cannot recover.
const token = await yourBackend.getZallyToken();
await zally.initialize({ appUUID, getClientToken: async () => token });
// Right: fetch on demand.
await zally.initialize({ appUUID, getClientToken: () => yourBackend.getZallyToken() });Each request is bounded by a 30 second timeout. If your function has not returned
by then, the SDK is told no token is available and authorization fails rather
than hanging. Returning null, returning an empty string, or throwing all report
"no token" to the SDK, and each logs a warning to the console: the SDK's own
error says only "Invalid token", which does not point at a failing endpoint.
The wrapper version is reported at authorization
initialize() puts this package's version on the native bridge as
wrapperVersion, a string | null field on the options object the JavaScript
layer passes to the native module. Without it, token exchange sees only the
native iOS or Android SDK underneath this wrapper, so the SDK version support
table cannot tell a React Native consumer from a direct native integrator.
The version is read from this package's package.json and held nowhere else, so
the version reported and the version published cannot drift.
The wire contract this establishes:
wrapperVersionis a semantic version string, prereleases included, ornull- the native bridges forward it to the native SDK, which reports it to token exchange alongside the platform and native SDK version it already sends
- where both a native range and a React Native range match the same session, the React Native range is applied. A session reporting a wrapper version is a React Native consumer, so a native version marked unsupported for a reason unrelated to React Native does not halt it
Both bridges read wrapperVersion and log it, but cannot yet forward it:
ZallySDK.initialize takes (context, appId) on Android and appUUID on iOS,
and neither native SDK exposes a wrapper-version API. The forwarding, and the
support table changes behind it, land with the native SDK work. Sending the
field first fixes the contract both sides build against, and logging it at the
bridge means a support request can identify a React Native consumer today
rather than waiting for that work.
src/__tests__/nativeContract.test.ts asserts that every key in
InitializeOptions is at least named by both native bridges. Nothing else can:
there is no codegen here, keys are matched by string at runtime, and both sides
compile perfectly while a value nobody reads is dropped on the floor.
Reporting is fail open at every step. A version that cannot be read or does not
parse is sent as null and logs a warning to the console. Authorization proceeds
exactly as it would have without one, and an unrecognised or absent version is
never a reason to refuse a session.
isInitialized and isRunning differ by platform
Both reflect what each native SDK exposes, which is not the same thing.
isInitialized() is true on iOS only once authorization has succeeded. On
Android it is true once initialize() completes, which happens before
authorization. Do not treat it as "ready to authenticate" on Android.
isRunning() is authoritative on iOS, where the SDK reports sensors and
authorization together. The Android SDK exposes no running state, so this
reflects whether start() last succeeded and will not notice the SDK stopping on
its own. Never treat it as proof that data is flowing.
UserState
{
trainingState: string; // "UNTRAINED" | "TRAINING" | "TRAINED"
zIdStatus: string; // "UNKNOWN" | "USER" | "IMPOSTER" | "INACTIVE"
isUser: boolean; // true if verified as the legitimate user
isImposter: boolean; // true if potential imposter detected (Android: zIdStatus.IMPOSTER; iOS: derived from server response)
}Internal edition
This package talks to production and nothing else. zally's own apps that need
the dev stack use the internal edition, which adds an optional baseUrl to
initialize:
await zally.initialize({
appUUID,
getClientToken,
baseUrl: 'https://coreapi-dev.zally-services.com/',
});On the public package that rejects with E_UNSUPPORTED_EDITION. The base URL is
stripped from the public native builds on both platforms deliberately, so this
is a build-time edition rather than a runtime switch. Turning it on means
zallySdkEdition=internal on Android and ZALLY_INTERNAL_EDITION=1 on iOS, and
both platforms must be switched. See
docs/internal-edition.md.
Releasing
Releasing is two dispatches with a hold point between them. See docs/release-process.md.
License
Apache-2.0
