zally_flutter_sdk 1.2.0
zally_flutter_sdk: ^1.2.0 copied to clipboard
Continuous Authentication for Flutter. Wraps the native zally iOS and Android SDKs behind one Dart API.
zally Flutter SDK #
Continuous Authentication for Flutter. One Dart API over the native zally iOS and Android SDKs.
zally learns how a person holds and touches their device, then keeps verifying that it is still them. This package carries the Dart API and the two native bridges. The native SDKs themselves are fetched at build time, from Maven Central on Android and from a public GitHub release on iOS.
Prefer to own the integration yourself, without a package dependency? The same capability ships as a drag-and-drop bridge at zally-flutter-sdk-bridge.
Requirements #
- Flutter 3.41.0 or later. That release carries the fix for CVE-2026-27704, a path traversal in the pub client, and is the floor at which Swift Package Manager support is dependable for plugins.
- Android: minSdk 23, compileSdk 36, Java 17, Kotlin 2.2.21 or later.
- iOS: deployment target 17.0, Xcode 16 or later.
- An identity provider that signs a JWT for the signed-in user, set up under Authentication configuration in the zally platform. See The client token.
Install #
flutter pub add zally_flutter_sdk
While this package is in beta, pub add will not resolve to it by default.
Ask for the prerelease explicitly:
flutter pub add zally_flutter_sdk:^1.0.0-beta.1
Android #
Set the app's floors in android/app/build.gradle.kts if they are lower than the
plugin needs:
android {
compileSdk = 36
defaultConfig { minSdk = 23 }
}
Nothing else. The native SDK resolves from Maven Central, which the Flutter app
template already declares, and it merges its own sensor permissions and ProGuard
rules. Add android.permission.INTERNET if your app does not already have it.
Optionally declare your app UUID in android/app/src/main/AndroidManifest.xml,
inside <application>:
<meta-data android:name="com.zally.fluttersdk.APP_UUID" android:value="your-app-uuid" />
This is recommended. See Android touch capture timing.
iOS #
Set the deployment target to 17.0 in Xcode, or in ios/Podfile:
platform :ios, '17.0'
Swift Package Manager, the default since Flutter 3.44, needs nothing else. The native xcframework resolves from a public GitHub release.
CocoaPods apps need one more line, because the native pod is not on the
CocoaPods trunk registry and CocoaPods cannot otherwise find it. In ios/Podfile,
inside your target 'Runner' block:
pod 'zally-sdk', '3.2.0',
:podspec => 'https://raw.githubusercontent.com/zally-ltd/zally-ios-sdk-package/3.2.0/zally-sdk.podspec'
Pin the tag path as shown. Pointing at main lets a native release change what
your build resolves without you changing anything. The version must match the one
this package pins; a mismatch fails pod install rather than reaching a build.
Then add a motion usage description to ios/Runner/Info.plist:
<key>NSMotionUsageDescription</key>
<string>This app uses motion sensors for continuous authentication.</string>
Use #
import 'package:zally_flutter_sdk/zally_flutter_sdk.dart';
final zally = ZallySdk.instance;
Future<void> onSignedIn() async {
try {
await zally.initialize(
appUUID: 'your-app-uuid',
getClientToken: auth.fetchZallyToken, // a function, not a token
);
await zally.start();
} on ZallyException catch (e) {
// e.code is E_INIT, E_AUTH or E_START. e.authReason is set for E_AUTH.
}
}
final subscription = zally.userStateStream.listen((UserState state) {
if (state.isImposter) {
// step up authentication
}
});
Future<void> onSignedOut() => zally.stop();
Call initialize() after the user signs in. Call stop() on sign out.
API #
| Member | Returns | Throws |
|---|---|---|
initialize({appUUID, getClientToken, baseUrl}) |
Future<void> |
E_MISSING_APP_UUID, E_INVALID_BASE_URL, E_INIT, E_AUTH, E_UNSUPPORTED_EDITION, E_UNSUPPORTED_PLATFORM |
start() |
Future<void> |
E_START |
stop() |
Future<void> |
E_STOP |
isInitialized() |
Future<bool> |
|
isRunning() |
Future<bool> |
|
getUserState() |
Future<UserState?> |
|
userStateStream |
Stream<UserState> |
ZallyException carries code, message, and for E_AUTH an authReason of
invalidToken, networkUnavailable, tokenExchangeFailed, notInitialized or
internal.
UserState carries trainingState (unknown, untrained, training,
trained), zIdStatus (user, imposter, inactive, unknown), the raw
native strings, and the booleans isUser, isImposter, isOutOfTraining,
isInactive.
baseUrl points the SDK at a non production zally stack. It works only on the
internal edition of this package, which zally's own apps use; on the published
package it throws E_UNSUPPORTED_EDITION. The public native SDKs do not carry
the API it needs.
initialize() is safe to call more than once. Concurrent calls share one native
call. A later call, after a re-login or a hot restart, swaps in the new token
provider and re-runs authorization against the already initialized native SDK.
Things to know #
The client token #
getClientToken returns a JWT that your own identity provider signs for the
signed-in user. zally does not issue it. zally checks it against the JWKS URL,
issuer and audience set under Authentication configuration in the zally
platform, then exchanges it for zally tokens that the native SDK manages itself.
If a token your app already holds passes these checks, you can return that one.
| Field | What zally checks |
|---|---|
| signature | RS256, ES256 or PS256, with a key from your JWKS URL. The header carries kid and alg |
iss |
Equals your expected issuer |
aud |
Includes your expected audience |
sub |
A stable ID for the user. A new sub is a new person, and their training starts over |
iat |
Within the last hour. An older token is rejected even if exp has not passed |
exp |
Not yet passed |
The native SDK calls getClientToken once during initialize(), for the first
exchange. It does not call it on every request, because it refreshes its zally
tokens without you. It calls it again only when zally's refresh token expires or
is rejected, which can be hours later and in the background. By then a token
fetched once at sign in is more than an hour old, so pass a function that fetches
a current token on each call.
// Wrong. Every call returns the same token. Once it is an hour old,
// re-authentication fails.
final token = await auth.fetchZallyToken();
await zally.initialize(appUUID: id, getClientToken: () async => token);
// Right. Each call fetches a token issued within the last hour.
await zally.initialize(appUUID: id, getClientToken: auth.fetchZallyToken);
This package does not fetch a token up front. It waits for the native SDK to
ask, then calls your getClientToken and returns the answer.
If your provider returns null, an empty string, throws, or takes longer than 25
seconds, the plugin answers the SDK with no token and prints a [zally] warning.
The SDK reports E_AUTH with reason invalidToken on the initial exchange, or
stops quietly on a background refresh. Watch for that warning in your logs.
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.
isImposter is true before there is a verdict #
On both platforms isImposter is true while zIdStatus is unknown, which is
the state right after start() and during training. Read it as "not proven to be
the user". Use zIdStatus == ZIdStatus.imposter for an explicit imposter verdict.
Android touch capture timing #
The Android SDK attaches touch capture when an Activity resumes. A Dart
initialize() call runs after your first screen is already on screen, so without
the com.zally.fluttersdk.APP_UUID manifest entry, touch capture begins the next
time the app comes to the foreground. Sensor streaming is unaffected either way.
With the manifest entry the plugin initializes the SDK before the first resume and
capture attaches immediately on start().
If the manifest value and the appUUID passed to initialize() disagree, the
plugin logs a warning and keeps the manifest value.
Keystroke dynamics are not available in Flutter #
The native SDKs observe platform text fields to measure typing rhythm. Flutter draws its own text fields, so there is nothing native to observe, on either platform. Touch, gesture and motion signals are unaffected. This is a known platform limitation, not a defect in this package.
Hot restart #
Plugin registration lives in the Flutter engine and survives a Dart hot restart.
Dart main() runs again and calls initialize() again. Both native bridges treat
that as a no-op on an already initialized SDK and re-run authorization with the
new provider.
Android Kotlin Gradle Plugin warning #
Building an app that uses this package prints a Flutter warning that the plugin
applies the Kotlin Gradle Plugin. That is deliberate and safe today. Flutter's
current app template still sets android.builtInKotlin=false, so a plugin without
the Kotlin Gradle Plugin would fail to compile in apps generated today. This
package migrates when Flutter flips that default.
Native SDK versions #
| Platform | Pinned | Source |
|---|---|---|
| Android | com.zally:zally-sdk:3.3.0 |
Maven Central |
| iOS | zally-ios-sdk-package 3.2.0 |
GitHub release, via Swift Package Manager or the podspec URL above |
Moving a pin is a release of this package, so one package version always resolves one native version on both platforms.
Example #
cd example
flutter run \
--dart-define=ZALLY_APP_UUID=... \
--dart-define=ZALLY_TOKEN_URL=https://your-backend/zally-token
The token URL is fetched with GET each time the SDK asks for a token. The body is
the JWT, or JSON with a top-level token field. Pass -PzallyAppUuid=... to
Gradle to exercise the early initialization path. The screen shows every call,
every token request with a timestamp, and the live user state.
License #
Apache-2.0. The native zally SDKs carry their own licenses.