deepidsdk_flutter 1.0.4
deepidsdk_flutter: ^1.0.4 copied to clipboard
Flutter plugin for the DeepID SDK (Android + iOS). Provides SIM binding, device enrollment, and fraud detection capabilities.
deepidsdk_flutter #
Flutter plugin that bridges the DeepID SDK on Android and iOS.
Provides device enrollment (SIM-based fingerprinting) and an interactive SIM
binding flow — a two-step fraud-prevention mechanism that cryptographically
links a user's phone number to their device.
Note — SDK binaries are not bundled in this package.
The native Android AAR and iOS xcframework are distributed separately by DeepID. You will receive them via email or by some other means. Place them at the exact paths shown in Prerequisites before runningflutter pub getor building your app.
Table of contents #
- Prerequisites
- How it works
- Requirements
- Installation
- Android setup
- iOS setup
- Integration walkthrough
- API reference
- Error handling
- Platform differences
- Troubleshooting
Prerequisites #
Before adding this plugin to your app you must obtain the native SDK binaries from DeepID (contact deepidsdk.com if you haven't received them yet) and place them inside the plugin's directory in your pub cache.
The pub cache path for this plugin is:
~/.pub-cache/hosted/pub.flutter-io.cn/deepidsdk_flutter-<version>/
Replace <version> with the version you added to your pubspec.yaml
(e.g. deepidsdk_flutter-1.0.0).
Android #
~/.pub-cache/hosted/pub.flutter-io.cn/deepidsdk_flutter-<version>/
└── android/
└── libs/
└── deepidsdk.aar ← place the AAR here
The Gradle build script auto-publishes this AAR into a local Maven repository at configuration time. If the file is missing the build will fail immediately with a descriptive error message pointing to this path.
iOS #
~/.pub-cache/hosted/pub.flutter-io.cn/deepidsdk_flutter-<version>/
└── ios/
└── Frameworks/
├── DeepIdSDK.xcframework/ ← place the xcframework here
│ ├── Info.plist
│ ├── ios-arm64/
│ └── ios-arm64_x86_64-simulator/
└── ShieldPtr.xcframework/ ← place the xcframework here
├── Info.plist
├── ios-arm64/
└── ios-arm64_x86_64-simulator/
CocoaPods picks up both xcframeworks via the plugin's podspec. If either
directory is absent, pod install will fail with "vendored framework not found".
Verify before continuing #
After placing both files, your pub cache directory should look like this:
~/.pub-cache/hosted/pub.flutter-io.cn/deepidsdk_flutter-<version>/
├── android/
│ └── libs/
│ └── deepidsdk.aar ✓
├── ios/
│ └── Frameworks/
│ ├── DeepIdSDK.xcframework/ ✓
│ └── ShieldPtr.xcframework/ ✓
├── lib/
└── pubspec.yaml
Only once both binaries are in place should you proceed with the steps below.
Requirements #
| Platform | Minimum version |
|---|---|
| Android | API 29 (Android 10) |
| iOS | 15.0 |
| Flutter | 3.10.0 |
| Dart SDK | ≥ 3.0.0 |
Installation #
Step 1 — Place the SDK binaries as described in Prerequisites.
Step 2 — Add the plugin to your pubspec.yaml:
dependencies:
deepidsdk_flutter: ^1.0.4
Step 3 — If your SIM binding flow requires runtime permissions on Android
(it does — see below), also add permission_handler:
dependencies:
permission_handler: ^11.0.0
Step 4 — Fetch dependencies:
flutter pub get
Android setup #
Permissions #
deepidsdk.aar declares the permissions below in its own manifest, and the
Android manifest-merger merges them into your host app at build time. You do
not need to add any of them to your own AndroidManifest.xml, but you should
know they will appear in your final APK because some of them are protected
and may trigger Play Console reviews.
Standard (install-time, normal protection):
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
Dangerous (require runtime grant before startSimBinding()):
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
<uses-permission android:name="android.permission.SEND_SMS" />
Protected / policy-restricted (merged silently — see notes below):
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
The SDK also declares one feature flag:
<uses-feature android:name="android.hardware.telephony" android:required="false" />
Requesting the dangerous permissions at runtime #
Request READ_PHONE_STATE + SEND_SMS immediately before launching the SIM
binding flow:
import 'package:permission_handler/permission_handler.dart';
Future<bool> _requestSimBindingPermissions() async {
final statuses = await [Permission.phone, Permission.sms].request();
return statuses[Permission.phone]!.isGranted &&
statuses[Permission.sms]!.isGranted;
}
Without both grants the SDK cannot read the SIM list or send the silent
verification SMS — startSimBinding() will fail or return an error.
build.gradle — minSdk #
The bundled deepidsdk.aar declares minSdkVersion="29". Your app module's
android/app/build.gradle must set minSdk to at least 29 (Android 10) —
the Android manifest-merger will fail with
uses-sdk:minSdkVersion N cannot be smaller than version 29 declared in library for any lower value.
android {
defaultConfig {
minSdk 29
}
}
Activity (auto-registered) #
The plugin registers SimBindingActivity (a transparent Compose host) in its
own manifest. It is never launched directly; the plugin starts it internally
when startSimBinding() is called. No changes to your manifest are needed.
iOS setup #
CocoaPods #
The plugin uses CocoaPods to link DeepIdSDK.xcframework and
ShieldPtr.xcframework. You must have placed both xcframeworks under
ios/Frameworks/ (see Prerequisites) before running pod
install.
cd ios && pod install
The podspec pulls in the required system frameworks automatically:
MessageUI and CoreTelephony.
Minimum iOS target: Your app target must be set to iOS 15.0 or later — the
DeepIdSDK.framework is built with MinimumOSVersion = 15.0, so pod install
will fail on lower targets.
In Xcode: Target → General → Minimum Deployments → iOS 15.0, or in
ios/Podfile:
platform :ios, '15.0'
Info.plist — usage descriptions #
DeepIdSDK.framework links against LocalAuthentication, AVFoundation,
CoreLocation, and CoreMotion (verified via otool -L). Even when the SDK
calls only metadata/availability APIs on these frameworks and triggers no
runtime permission prompt, App Review's static analyzer will reject a binary
that imports them without the corresponding NS*UsageDescription keys.
Add the following four keys to your app's ios/Runner/Info.plist:
<!-- Biometric hardware probe — LAContext.canEvaluatePolicy(.biometrics) -->
<key>NSFaceIDUsageDescription</key>
<string>DeepID Protect checks for biometric hardware availability during enrollment.</string>
<!-- Camera-hardware enumeration — AVCaptureDevice.default(...) -->
<key>NSCameraUsageDescription</key>
<string>DeepID Protect queries camera hardware capabilities to build a device fingerprint during enrollment. No image or video is captured.</string>
<!-- Location-services availability — CLLocationManager.locationServicesEnabled() -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>DeepID Protect checks location-service availability as a device-integrity signal during enrollment.</string>
<!-- Motion-sensor availability — CMMotionManager().isAccelerometerAvailable etc. -->
<key>NSMotionUsageDescription</key>
<string>DeepID Protect inspects motion-sensor availability as a device-integrity signal during enrollment. No sensor data is recorded.</string>
No runtime permission prompts appear during enrollment or SIM binding — these
are all metadata/availability calls. NSContactsUsageDescription,
NSPhotoLibraryUsageDescription, NSMicrophoneUsageDescription, and
NSUserTrackingUsageDescription are not needed and should not be added
(the SDK does not link Contacts, Photos, AdSupport, or AppTrackingTransparency).
PrivacyInfo.xcprivacy #
DeepIdSDK.framework ships with its own PrivacyInfo.xcprivacy declaring
the required-reason APIs it uses (SystemBootTime, UserDefaults,
DiskSpace, FileTimestamp) and the data categories it collects
(DeviceID, PhoneNumber, linked, non-tracking). No action is required in
your app — your app's own privacy manifest (if any) is separate and lives at
ios/Runner/PrivacyInfo.xcprivacy.
Integration walkthrough #
1. Initialize the SDK (once, at app startup) #
Call DeepId.initialize() as early as possible — ideally in your root widget's
initState. The call returns immediately after constructing the SDK; enrollment
runs in the background.
import 'package:deepidsdk_flutter/deepidsdk_flutter.dart';
@override
void initState() {
super.initState();
_initDeepId();
}
Future<void> _initDeepId() async {
try {
await DeepId.initialize(
appKey: 'YOUR_APP_KEY',
appSecret: 'YOUR_APP_SECRET',
onEnrollment: (EnrollmentResult result) {
// Fires once when deepId + sessionId are ready.
// Only now is it safe to show the SIM binding CTA.
setState(() {
_deepId = result.deepId;
_sessionId = result.sessionId;
_enrollmentReady = true;
});
},
onEnrollmentError: (DeepIdException error) {
// Enrollment failed — show an error state.
setState(() => _enrollmentError = error.message);
},
enrollmentTimeout: const Duration(seconds: 30),
);
} on DeepIdException catch (e) {
// initialize() itself failed — bad credentials or SDK construction error.
setState(() => _enrollmentError = e.message);
}
}
initialize() throws DeepIdException only if the credentials are missing or
the SDK cannot be constructed. Network/server errors during the background
enrollment are delivered via onEnrollmentError, not as exceptions from
initialize().
2. Gate the SIM binding CTA on enrollment #
The sim binding cannot start without the deepid enrollment, so first initialize the DeepIdSDK which enrolls the DeepId automatically and then proceed with the sim binding.
3. Request Android permissions (Android only) #
Request READ_PHONE_STATE + SEND_SMS immediately before launching the flow,
not at app startup — the system associates the rationale dialog with the action
that requires the permission:
Future<void> _startSimBinding() async {
if (Platform.isAndroid) {
final statuses = await [Permission.phone, Permission.sms].request();
final granted = statuses[Permission.phone]!.isGranted &&
statuses[Permission.sms]!.isGranted;
if (!granted) {
// Show permission rationale to the user and return.
return;
}
}
try {
final result = await DeepId.startSimBinding(
phoneNumber: '+911234567890', // optional
);
print('Bound: ${result.deepId} / ${result.mobile}');
} on DeepIdException catch (e) {
if (e.code == DeepIdErrorCode.userCancelled) {
// User dismissed the sheet — treat as a no-op, not an error.
} else {
// Real failure — show error message.
print('SIM binding failed: ${e.message}');
}
}
}
4. Phone number parameter #
startSimBinding({String? phoneNumber}) accepts an optional E.164-formatted
number:
- Provided: Pre-fills the number field in the native sheet. The user still confirms before verification is sent.
- Omitted: The native UI displays all available SIMs; the user selects one and enters their number.
API reference #
DeepId class #
All methods are static. The class cannot be instantiated.
DeepId.initialize()
static Future<void> initialize({
required String appKey,
required String appSecret,
void Function(EnrollmentResult)? onEnrollment,
void Function(DeepIdException)? onEnrollmentError,
Duration enrollmentTimeout = const Duration(seconds: 150),
})
Constructs and starts the native SDK. Returns as soon as the SDK object is created — does not block until enrollment finishes.
| Parameter | Description |
|---|---|
appKey |
Your DeepID app key. Must not be empty. |
appSecret |
Your DeepID app secret. Must not be empty. |
onEnrollment |
Fires once when the server returns both deepId and sessionId. |
onEnrollmentError |
Fires if enrollment times out or fails. Optional. |
enrollmentTimeout |
How long to wait for enrollment. Defaults to 150 s. |
Throws DeepIdException if credentials are invalid or SDK construction fails.
If you call initialize() a second time (e.g., hot restart), the existing SDK
instance is reused — no duplicate initialization occurs.
DeepId.onEnrollment()
static void onEnrollment({
required void Function(EnrollmentResult result) onSuccess,
void Function(DeepIdException error)? onError,
Duration timeout = const Duration(seconds: 30),
})
Registers an enrollment callback independently of initialize(). Useful when
you need to attach a callback in a different part of the widget tree than where
you called initialize().
- If enrollment has already completed,
onSuccessfires on the next event-loop tick. - If enrollment has not completed,
onSuccessfires the moment the native SDK delivers the result. - If
timeoutexpires before enrollment completes,onErrorreceivesDeepIdErrorCode.enrollmentTimeout.
DeepId.startSimBinding()
static Future<SimBindingResult> startSimBinding({
String? phoneNumber,
})
Presents the native SIM binding UI. Awaits user interaction and verification.
Returns a SimBindingResult on success. Throws DeepIdException on any
failure including user cancellation.
Preconditions:
initialize()must have been called.onEnrollmentmust have fired — i.e., bothdeepIdandsessionIdmust be available.- On Android:
READ_PHONE_STATEandSEND_SMSmust be granted.
DeepId.isInitialized
static Future<bool> get isInitialized
Returns true if initialize() has been successfully called. Does not
indicate that enrollment is complete — use the onEnrollment callback for that.
DeepId.deepId / DeepId.sessionId
static String? get deepId
static String? get sessionId
Synchronous accessors for the last enrollment result. Both return null until
onEnrollment has fired at least once in this process. Safe to call from any
isolate.
EnrollmentResult #
Delivered to the onEnrollment / onSuccess callback.
| Field | Type | Description |
|---|---|---|
deepId |
String |
Unique device fingerprint token. Non-empty. |
sessionId |
String |
Current enrollment session identifier. Non-empty. |
SimBindingResult #
Returned by a successful startSimBinding() call.
| Field | Type | Description |
|---|---|---|
success |
bool |
Always true when the object is returned (errors throw instead). |
deepId |
String |
Device token at the time of binding. |
sessionId |
String |
Session identifier at the time of binding. |
mobile |
String |
Verified phone number. See Platform differences. |
message |
String |
Human-readable confirmation string from the SDK. |
DeepIdException #
Thrown by initialize() and startSimBinding(), and passed to error
callbacks.
| Property | Type | Description |
|---|---|---|
code |
DeepIdErrorCode |
Typed error code. |
message |
String |
Human-readable description. |
DeepIdErrorCode #
| Code | When it occurs |
|---|---|
notInitialized |
startSimBinding() called before initialize(). |
invalidAppKey |
appKey was blank when calling initialize(). |
invalidAppSecret |
appSecret was blank when calling initialize(). |
noContext |
Android: no Activity context available during initialize(). Call after the app is fully running. |
noViewController |
iOS: no root UIViewController available to present the sheet. |
initFailed |
SDK construction failed (network or server error during setup). |
enrollmentFailed |
Backend rejected the enrollment request. |
enrollmentTimeout |
Enrollment did not complete within the specified timeout. |
enrollmentNotComplete |
startSimBinding() called before onEnrollment has fired. |
simBindingFailed |
Verification flow failed on the carrier or server side. |
alreadyInProgress |
startSimBinding() called while another SIM binding flow is still running. |
userCancelled |
User dismissed the native SIM binding sheet without completing. |
unknown |
Catch-all for unexpected native errors. |
Error handling #
Distinguish cancellation from failure #
User cancellation is a normal UX path and should not be treated as an error:
try {
final result = await DeepId.startSimBinding();
// success path
} on DeepIdException catch (e) {
switch (e.code) {
case DeepIdErrorCode.userCancelled:
// User tapped back / dismissed — no error to show
break;
case DeepIdErrorCode.enrollmentNotComplete:
// Logic bug — show internal error or restart enrollment
break;
default:
_showErrorBanner(e.message);
}
}
Enrollment timeout #
The default timeout is 150 seconds. Increase it on slow networks if needed, or lower it to surface failures faster:
await DeepId.initialize(
appKey: key,
appSecret: secret,
enrollmentTimeout: const Duration(seconds: 60),
onEnrollmentError: (e) {
if (e.code == DeepIdErrorCode.enrollmentTimeout) {
// Offer a "Retry" button that calls initialize() again
}
},
);
Retry after failure #
initialize() is idempotent — calling it again reuses the existing native SDK
instance if it was already constructed successfully. If the initial SDK
construction failed, calling initialize() again creates a fresh instance.
Platform differences #
mobile field in SimBindingResult #
| Platform | Value |
|---|---|
| Android | Phone number returned by the carrier after successful verification — guaranteed to be the real verified number. |
| iOS | The verified phone number from the server response if the backend returns one; otherwise the phoneNumber argument passed to startSimBinding(). Pass phoneNumber explicitly to ensure the field is populated even if the server omits it. |
Design your backend to treat the mobile value as informational on iOS, and
rely on deepId + sessionId as the authoritative binding proof.
Permissions #
| Permission | Android | iOS |
|---|---|---|
| Phone / SIM access | READ_PHONE_STATE, READ_PHONE_NUMBERS (runtime, dangerous) |
No prompt — IDFV and CoreTelephony are available without permission |
| SMS sending | SEND_SMS (runtime, dangerous) |
No prompt — MessageUI framework used |
| Motion / sensors | None | NSMotionUsageDescription in Info.plist (no prompt at runtime; CMMotionActivityManager prompt only if used) |
| Network | INTERNET, ACCESS_NETWORK_STATE, ACCESS_WIFI_STATE (install-time, normal) |
No prompt |
SIM binding UI #
| Android | iOS | |
|---|---|---|
| Presentation | Full Activity launched via startActivityForResult |
UIHostingController presented as a .pageSheet |
| SIM selection | Jetpack Compose UI inside a transparent Activity |
SwiftUI DeepIdSDKEntryView |
| Dismissal detection | onActivityResult with RESULT_CANCELED |
UIAdaptivePresentationControllerDelegate + viewDidDisappear guard |
Troubleshooting #
Framework 'DeepIdSDK' not found or Framework 'ShieldPtr' not found (iOS linker) #
Most commonly caused by one or both xcframeworks not being placed at the expected paths. Verify both files exist:
~/.pub-cache/hosted/pub.flutter-io.cn/deepidsdk_flutter-<version>/ios/Frameworks/DeepIdSDK.xcframework
~/.pub-cache/hosted/pub.flutter-io.cn/deepidsdk_flutter-<version>/ios/Frameworks/ShieldPtr.xcframework
(See Prerequisites.) Then clean the pod cache and reinstall:
cd ios
rm -rf Pods .symlinks Podfile.lock
pod install
The xcframework container name must also match the inner .framework name —
the pod expects DeepIdSDK.xcframework containing DeepIdSDK.framework, and
ShieldPtr.xcframework containing ShieldPtr.framework.
If you received a differently named xcframework, rename the outer directory to
match before running pod install.
Gradle error: "DeepID Protect SDK AAR is missing or empty" (Android) #
The build script requires the AAR at
~/.pub-cache/hosted/pub.flutter-io.cn/deepidsdk_flutter-<version>/android/libs/deepidsdk.aar.
This file is not included in the pub.flutter-io.cn package — you must obtain it from
DeepID (see Prerequisites) and place it at that exact path
before running flutter build or flutter run.
.swiftinterface error: 'X' is not a member type of class 'DeepIdSDK.X' #
This is a Swift module/class name collision that occurs when a public class
shares its name with the containing module. The DeepIdSDK xcframework avoids
this by naming its public client class DeepIDSDK (distinct from the module
DeepIdSDK). If you see this error, verify that you are using an xcframework
built with BUILD_LIBRARY_FOR_DISTRIBUTION=YES and that the binary has not been
renamed.
App exits silently on initialize() (iOS device with an attached debugger) #
Symptom. On a real iPhone, the app disappears within ~1 second of
DeepId.initialize() being called. flutter run typically reports Lost connection to device. (or just "Application finished"). No Dart exception is
thrown, no native crash report is generated, and nothing useful appears in
the device console. The same build works fine on the iOS Simulator and on
App Store / TestFlight / ad-hoc installs.
Cause. The release-built DeepIdSDK.xcframework includes an anti-debug
guard that calls exit(-1) whenever it sees a debugger attached to the
process (the kernel P_TRACED flag is set). This is fraud-prevention working
as designed — it blocks LLDB, Frida-with-debugger, and other ptrace-based
dynamic analysis on production devices. The catch is that flutter run to a
tethered device attaches LLDB for log forwarding, which trips the same guard.
End users who install your app via the App Store, TestFlight, MDM, or an ad-hoc IPA are not affected — their devices launch the app with no debugger attached, and the SDK runs normally.
Workarounds for development on hardware, in order of how close they are
to a normal flutter run loop:
-
Install once, then launch manually from the home screen. This is the simplest path and gives the same runtime conditions as a TestFlight build:
flutter build ios --release xcrun devicectl device install app \ --device <device-id> \ build/ios/iphoneos/Runner.appThen tap the app icon on the device. Hot reload is not available with this approach; for device logs, use
idevicesyslog, or Xcode → Window → Devices and Simulators → Open Console. -
Distribute via TestFlight or an ad-hoc IPA. Closest to production — no debugger ever attaches, and you exercise the same install path your end users will.
-
Request a debug build of the xcframework from DeepID. The debug variant does not install the anti-debug guard, so
flutter runto a tethered device works normally with hot reload. Use the debug variant during active development and swap in the release variant before shipping. The two variants are produced by the SDK build script with and without the--releaseflag.
Enrollment times out on first run (Android) #
On first install the SDK enrolls the device with the DeepID backend. On slow
networks this can exceed 30 seconds. Increase enrollmentTimeout and prompt
the user to check their network connection if enrollment fails.
NO_CONTEXT error on Android #
initialize() must be called after the Flutter engine is attached to an
Activity. Calling it in a background isolate or before the first frame is
rendered will fail with this error. Place the call in initState() of your
root widget or inside WidgetsBinding.instance.addPostFrameCallback.
ALREADY_IN_PROGRESS error #
Only one SIM binding flow can run at a time. If the user navigates away and
back, the original Future from the first startSimBinding() call is still
pending. Disable the "Bind SIM" button while startSimBinding() is in flight:
bool _simBindingInFlight = false;
// In your button:
onPressed: (_enrollmentReady && !_simBindingInFlight) ? _startSimBinding : null,
// In _startSimBinding:
setState(() => _simBindingInFlight = true);
try {
final result = await DeepId.startSimBinding();
// ...
} finally {
setState(() => _simBindingInFlight = false);
}