
Flutter Integration
mubeen_flutter Documentation
This guide covers mubeen_flutter, the Flutter plugin for Mubeen device intelligence.
The plugin is a thin bridge. It contains no fingerprinting logic of its own: it calls the native Android and iOS SDKs, which do the collection. Everything you use comes from one import:
import 'package:mubeen_flutter/mubeen_flutter.dart';
Overview
mubeen_flutter gives each device a persistent identifier and sends device fingerprints to
Mubeen for risk analysis, from a single Dart API on both platforms:
- Persistent Device ID — one identifier per device. On iOS it survives a reinstall; on Android it does through Auto Backup, which covers it by default and needs one line if your app defines its own backup rules.
- Fingerprint submission —
sendFingerprint(), with collection off the main thread. - Risk Signals — root, tamper and emulator detection on Android; jailbreak checks on iOS.
- Honest platform differences — what one platform cannot do is reported, never faked.
You do not add deviceintel-lib or MubeenDeviceSDK to your app yourself; the plugin
brings both. On Android, Gradle resolves deviceintel-lib 1.7.3 from Maven Central as a
dependency of the plugin. On iOS, the SDK binary is committed inside the plugin for
CocoaPods, and Swift Package Manager downloads the same checksum-verified release from
GitHub. Your app declares one package.
Requirements
| Requirement | Version |
|---|---|
| Flutter | 3.44.0+ |
| Dart | 3.12+ |
| Android | 7.0+ (API 24) |
| iOS | 15.0+ |
| Xcode | 16+ |
Xcode 16 is required even though the native iOS SDK's own guide lists Xcode 15: the SDK binary bundled with the plugin needs a newer Swift compiler than Xcode 15 ships.
Below 3.44 the package will not resolve. If your app is on an older Flutter, contact us before you start so we can discuss the options with you.
Installation
Add the plugin as a git dependency pinned to a release tag. No GitHub account is needed.
dependencies:
mubeen_flutter:
git:
url: https://github.com/mubeen-ai/mubeen_flutter_dist.git
ref: v0.1.4
Then:
flutter pub get
Platform minimums your app must meet
The plugin cannot lower these for you, so check them before your first build.
Android. Your app's minSdk must be 24 or higher. A lower value fails the build with
a manifest merger error naming the plugin.
defaultConfig {
minSdk = 24
}
iOS. Your app's deployment target must be 15.0 or higher. In Xcode, select the Runner target, open General, and set Minimum Deployments to iOS 15.0.
If your app uses CocoaPods, which is the case when you have turned Swift Package Manager off,
also set the platform line in the Podfile. Keep the rest of the file as Flutter generated it,
including use_frameworks!.
platform :ios, '15.0'
If pod install complains that the plugin requires a higher minimum deployment target, that
line is missing. The native SDK binary ships inside the plugin, so pod install downloads
nothing extra.
Beyond those minimums there is nothing else to wire up, with one optional exception covered under Android Auto Backup.
Quick Start
1. Configure
Call configure once, during app startup, and await it before anything else.
import 'package:flutter/widgets.dart';
import 'package:mubeen_flutter/mubeen_flutter.dart';
Future<void> main() async {
// Subscribe BEFORE configure: errors raised during startup are not replayed.
Mubeen.instance.errors.listen((MubeenError error) {
debugPrint('mubeen: ${error.code} ${error.message}');
});
await Mubeen.instance.configure(
const MubeenConfig(
apiKey: 'public_your_key',
endpoint: 'https://fp.sa.mubeen.ai',
),
);
runApp(const MyApp());
}
A key issued in one region returns 401 invalid credentials against another region's
endpoint, even though the key is perfectly valid where it was issued. On iOS,
sendFingerprint() throws MubeenAuthError. On Android nothing reports it: fingerprints
simply never arrive. Use the endpoint for your region from your onboarding, for example
https://fp.sa.mubeen.ai or https://fp.de.mubeen.ai.
2. Read the Device ID
final String deviceId = await Mubeen.instance.getDeviceId();
Reading the ID never uploads anything.
3. Send a Fingerprint
await Mubeen.instance.sendFingerprint();
Platform capabilities
This is the part that differs from a single-platform SDK. The two native SDKs do not offer the same features, so the plugin exposes everything each one can do and tells you which is which, rather than reducing the API to what both share.
| Android | iOS | |
|---|---|---|
configure, getDeviceId, sendFingerprint, resetDeviceId | yes | yes |
linkage and custom data sent | no (accepted, not sent) | yes |
getFingerprint() (returns the raw payload) | yes | no |
pendingUploadCount() | yes | no |
Calling an unsupported method (getFingerprint() on iOS, pendingUploadCount() on iOS)
throws MubeenUnsupportedError with a message naming the reason. It never returns a
plausible-looking zero.
linkage and custom are the one exception, so that the same code runs on both platforms:
pass them everywhere. On Android, sendFingerprint() accepts them, uploads the fingerprint
without them, and does not throw. To read whether they were actually sent, check
capabilities.linkage.
The current Android SDK's payload has no field for linkage or custom, so on Android they
are not sent. The first time this happens, one MubeenUnsupportedError warning arrives on
Mubeen.instance.errors; it is not repeated. When a plugin release adds Android support,
updating the ref is the only change: your code stays the same.
Configuration
const MubeenConfig(
apiKey: 'public_your_key',
endpoint: 'https://fp.sa.mubeen.ai',
logLevel: MubeenLogLevel.warning,
allowedCustomKeys: <String>{'checkout_step'},
android: MubeenAndroidOptions(),
ios: MubeenIosOptions(),
);
| Field | Type | Default | Notes |
|---|---|---|---|
apiKey | String | required | A public - JS client side key |
endpoint | String? | https://fp.sa.mubeen.ai | Same region as the key |
logLevel | MubeenLogLevel | warning | none to debug |
allowedCustomKeys | Set<String> | empty | Keys allowed in custom |
android | MubeenAndroidOptions | all off | Optional collectors |
ios | MubeenIosOptions | jailbreak on | Optional collectors |
endpoint must match the region your key was issued in, and null means the default.
An empty string is an error, not a request for the default. It must be a plain base URL:
configure() rejects an endpoint that contains a query string, a fragment, credentials
(user:password@), whitespace or a backslash, with an ArgumentError. It must be an
absolute https URL with a host; http is accepted only for localhost, 127.0.0.1, ::1
and 10.0.2.2, because both platforms block cleartext in release builds. A trailing slash is
harmless; the plugin strips it.
android carries collectLocation, locationMaxAge, collectAdvancedSignals and
collectAdvertisingId. ios carries environment, enableJailbreakChecks and
enableBehavioralSignals. Each platform ignores the other's group.
collectAdvertisingId needs the play-services-ads-identifier dependency in your app
and, for apps targeting API 33 or higher, the AD_ID permission. If either is missing,
configure() still succeeds and a MubeenUnsupportedError about that one collector arrives
on Mubeen.instance.errors. It is a warning: everything else keeps working, so do not
disable the SDK because of it.
A private - server side key is accepted by iOS and rejected by Android. Ship the wrong
type and you get a working iOS build while every Android upload is rejected with a 401
that your app never sees: no error on the Future, nothing on the error stream. Android
fingerprints simply never arrive, which reads like an Android bug rather than a key mistake.
MubeenLogLevel.debug prints your API keyAt debug, the Android SDK writes the API key and the full request body to logcat, in
release builds too. Use it while integrating, not in what you ship.
Configure once. The native SDKs are process-wide, and the plugin applies your
configuration once per process. Calling configure() again with identical values is a no-op.
With different values, including a different allowedCustomKeys, it throws
MubeenAlreadyConfiguredError rather than pretending the new values took effect. During
development this also means a Flutter hot restart keeps the previous configuration: to
change the key or endpoint, fully restart the app.
Device ID
final String deviceId = await Mubeen.instance.getDeviceId();
final String fresh = await Mubeen.instance.resetDeviceId();
resetDeviceId() discards the current identifier and mints a new one. Use it when a user
signs out of a shared device, not routinely: a device that changes identity constantly is
indistinguishable from a device that is trying to. On Android it also discards uploads still
waiting to retry.
On iOS the identifier is stored in the Keychain and survives a reinstall. On Android, surviving a reinstall depends on your app's Auto Backup rules. See below.
Sending Fingerprints
await Mubeen.instance.sendFingerprint();
// Same code on both platforms. On Android, linkage and custom are
// accepted but not sent yet (see Platform capabilities).
await Mubeen.instance.sendFingerprint(
linkage: const MubeenLinkage(
accountId: 'acct_123',
email: '[email protected]',
phone: '+966551234567',
),
custom: <String, String>{'checkout_step': 'payment-review'},
);
Linkage values are hashed on the device. The raw email or phone never leaves it.
When to send
Send once when the app is opened, then at moments that matter for risk: sign-in, sign-up, checkout, payment method change, password reset. Sending on every screen wastes battery and adds nothing, and on Android the SDK's own rate limiter (5 per minute) discards the excess.
Response handling, and what the Future actually means
What a completed await tells you depends on the platform.
Android: the Future completes on handoff, within milliseconds. Collection and upload
run afterwards, off the main thread. If the server rejects the upload (a 401, for example),
the SDK re-queues it and neither the Future nor Mubeen.instance.errors hears about it.
pendingUploadCount() is the backstop.
iOS: the Future completes after collection and the first upload attempt.
- A
2xxresponse completes it normally. - A
404or405throwsMubeenInvalidConfigError: the endpoint does not serve the fingerprint route, so check the URL, not the key. - Any other
4xxexcept429(for example400,401or403) throwsMubeenAuthError. - While Mubeen has switched submissions off remotely, it throws
MubeenDisabledError. - A
429or a5xxcompletes it normally. The SDK queues the payload and retries it, and each429or5xxresponse is reported onMubeen.instance.errors. - With no network (offline, DNS, TLS or a timeout), it completes normally and the payload is queued for retry, but no error is reported anywhere.
- A queued payload that later gets a terminal
4xxon retry is dropped without any report.
Typical timings on real hardware: on Android, collection took 0.5 to 0.7 seconds on
current phones, and a repeat collection on the same device was faster. On iOS,
sendFingerprint() completed in about 150 ms, upload included. Slow or emulated Android
devices were measured at 10 to 25 seconds.
configure() does not wait on the network and returns in milliseconds, so it does not slow
app startup. On iOS the SDK starts a background fetch of its remote configuration, which
configure() does not wait for.
The custom map
custom carries non-identifying extras. It is checked in Dart before anything is sent, in
release builds as well as debug, and a failure throws ArgumentError:
- Every key must be registered in
allowedCustomKeys. The linkage namesaccount_id,payment_token_id,emailandphoneare refused as keys, and also inallowedCustomKeys. - A value that contains an email address or an international phone number is rejected.
- At most 30 keys, and at most 4096 bytes as JSON.
Personal identifiers belong in linkage, which hashes them. The second rule is a safety net
for mistakes, not a PII detector: national-format phone numbers, government IDs, timestamps
and order IDs such as ORD-00123456789 or INV_0098765432 all pass. Only a 00 directly
after identifier punctuation (-, _, ., ,, (, ) or a Unicode dash) is read as a
zero-padded ID. A 00 or + number that stands as its own token is rejected, including at the
start of the value and in shapes like tel:00966… or phone=00966…; one glued to a letter or
digit, such as INV00966501234567, is not.
Error Handling
Failures reach you on exactly two paths.
Thrown through the awaited Future when the call itself fails:
try {
await Mubeen.instance.sendFingerprint();
} on MubeenUnsupportedError {
// not available on this platform
} on MubeenError catch (e) {
debugPrint('mubeen: ${e.code} ${e.message}');
} on ArgumentError {
// invalid input, e.g. a rejected custom map
}
Delivered on Mubeen.instance.errors when something fails outside a call. Subscribe
before configure(). What arrives there differs by platform:
- iOS: an upload answered with a
429or a5xx, which the SDK queues for retry. - Android: a transport failure (offline, DNS, TLS, timeout) as
MubeenNetworkErrorwith codenetwork, usually twice per failed call because the SDK re-sends immediately; a fault during collection, a failure to store the device ID, or a missingINTERNETpermission (the plugin declares it, so this only happens if your manifest removes it) asMubeenUnknownError; thecollectAdvertisingIdwarning described under Configuration; and a one-time warning whenlinkageorcustomwere not sent.
A rejected upload is not delivered on the stream on either platform. On iOS a rejected
first attempt throws from sendFingerprint() (a rejected retry is dropped silently, as above);
on Android it is re-queued silently (see the caution below).
The only thing thrown synchronously is the capabilities getter, if you read it before
configure() has completed. Every method call reports failure through its Future, so
unawaited(...), .catchError and FutureBuilder all behave.
| Error | Meaning |
|---|---|
MubeenNotConfiguredError | configure() has not completed. |
MubeenAlreadyConfiguredError | Configured already, with different values. Restart the app. |
MubeenInvalidConfigError | Configuration rejected, or (iOS) the endpoint answered 404/405. |
MubeenUnsupportedError | Not available on this platform. |
MubeenAuthError | Upload rejected. iOS only. |
MubeenNetworkError | Upload failed transiently and was queued. |
MubeenDisabledError | Submissions switched off remotely. iOS only. |
MubeenUnknownError | Anything unrecognised. |
ArgumentError | Invalid input, including PII in custom. |
MubeenUnsupportedError comes from calling a feature this platform lacks (check
capabilities first). On Android it also arrives on the stream as a warning, in two cases:
the collectAdvertisingId warning, and once when linkage or custom were accepted but not
sent. Neither means the SDK stopped working.
MubeenInvalidConfigError comes from the native SDK rejecting the configuration, or, on
iOS, from sendFingerprint() when the endpoint answers 404 or 405: a wrong URL, not a
wrong key.
MubeenAuthError is thrown by sendFingerprint() on iOS when the upload gets a 4xx
other than 404, 405 or 429 (typically 400, 401 or 403). The Android SDK never
reports an HTTP status, so you will not see this error there.
MubeenNetworkError arrives on the stream when an upload fails transiently and is queued
for retry: on iOS for a 429 or 5xx response, where code tells the two apart
(rate_limited or network); on Android for a transport failure (offline, DNS, TLS,
timeout), code network, usually twice per call. Being offline is not reported on iOS.
MubeenDisabledError is iOS only: Mubeen has switched submissions off remotely. It is not
a key problem and retrying will not help, so catch it and carry on with your flow.
The current Android SDK logs and re-queues rejected uploads without notifying the app,
so neither the Future nor the error stream sees them. pendingUploadCount() is the
backstop: it counts uploads that already failed and are waiting to retry.
Android Auto Backup
The Android SDK stores the device ID in a SharedPreferences file
(MB_device_intel_secure_prefs) and relies on Android's Auto Backup to carry it across a
reinstall. The plugin cannot wire this up for you: declaring backup rules in the plugin's own
manifest would conflict with any your app already declares and break the manifest merge.
If your app already defines backup rules, add the <include> line into your existing files.
Whole files shown for clarity:
<full-backup-content>
<include domain="sharedpref" path="MB_device_intel_secure_prefs.xml" />
</full-backup-content>
<data-extraction-rules>
<cloud-backup>
<include domain="sharedpref" path="MB_device_intel_secure_prefs.xml" />
</cloud-backup>
</data-extraction-rules>
Do not point your android:fullBackupContent or android:dataExtractionRules attribute at
our file directly. These attributes are include-only, so doing that would restrict your
app's entire backup to our preferences and exclude your own data.
If your app has no backup rules at all, change nothing: the platform default already backs up shared preferences.
Auto Backup restores the ID on reinstall, for the same account, with backup enabled. It does not survive Clear Data, which always mints a new ID.
A cloud restore or a device-to-device transfer (API 31+) can also carry the ID onto a new
phone, so treat the device ID on Android as backup-scoped, not strictly device-bound. Use <exclude>
under <device-transfer> if you want each physical device to get its own ID.
Privacy and store compliance
You declare the data, because it is collected under your app's identity.
- Device identifiers, always.
- Email, phone number and user ID, if you use
linkage, as linked data (plus Payment Info if you passpaymentTokenId). They are hashed on the device, and hashing does not remove the declaration requirement. - Usage data, if you turn on
enableBehavioralSignals(iOS). - Each Android opt-in collector you enable (location, advertising ID, advanced signals) adds its own Google Play Data Safety entry.
The bundled privacy manifests do not cover linkage or behavioral signals, so Xcode's aggregated privacy report will not fill these in for you.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Build fails with a manifest merger error naming the plugin | Your app's minSdk is below 24 | Raise minSdk to 24 |
pod install says the plugin needs a higher deployment target | The Podfile platform line is missing | Set platform :ios, '15.0' |
iOS throws MubeenAuthError with a key you know is valid | The key and the endpoint are from different regions | Use the endpoint for the region the key was issued in |
| Android fingerprints never arrive, no error on Android, iOS works | A private - server side key | Use a public - JS client side key |
Android: MubeenUnsupportedError on the stream after configure() | collectAdvertisingId without its dependency or AD_ID | Add them, or turn the option off |
MubeenAlreadyConfiguredError during development | Flutter hot restart keeps the native SDK configured | Fully restart the app, not hot restart |
MubeenNotConfiguredError on a call you expected to work | configure() was not awaited before it | Await configure() during startup |
MubeenUnsupportedError | The feature does not exist on this platform | Check capabilities first; see Platform capabilities |
sendFingerprint() returns fine on Android but nothing arrives | On Android the Future reports handoff, not delivery | Check pendingUploadCount(), and the key type and region |
Known limitations
| Limitation | Detail |
|---|---|
| Android cannot attach linkage or custom | Both are accepted and validated, but not sent: the SDK's payload has no field for either yet. |
| A rejected upload can look like success on Android | See the caution above. |
| Offline queues differ | Android's retry queue is in memory and can be lost when the process dies; iOS persists it. |
| No certificate pinning | A device with a user-installed CA can intercept traffic. |
debug log level prints the API key | In release builds too. |
Repeated sendFingerprint() calls | Each runs its own collection on Android; there is no coalescing. |
Related Documentation
- Android Integration — the native SDK this plugin wraps
- iOS Integration — the native SDK this plugin wraps
- Server-Side Integration — the risk decision API
- Architecture — how the pieces fit together