deepidsdk_flutter 2.2.0 copy "deepidsdk_flutter: ^2.2.0" to clipboard
deepidsdk_flutter: ^2.2.0 copied to clipboard

Flutter plugin for the DeepID SDK (Android + iOS). Device enrollment, SIM binding, on-demand posture attestation, and device fraud detection.

2.2.0 #

New: on-demand posture attestation. verifySecurityPolicy() re-measures the device immediately before a critical operation and returns a server-issued reference your backend redeems.

final result = await DeepId.verifySecurityPolicy(
  action: 'payment.initiate',
  reference: orderId,
);

switch (result) {
  case Allowed(:final attestationId):
    await myBackend.pay(orderId, attestation: attestationId); // backend verifies
  case Blocked(:final displayMessage):
    showError(displayMessage);
  case Challenge(:final attestationId):
    await startStepUp(attestationId);
  case Unavailable():
    showError('Could not verify device security.');            // do NOT proceed
}

The problem it solves: enrollment establishes posture once, at app start, and everything after that trusts a measurement that may be days old. The attack is specific — the user logs in on a clean device, passes every check, and then the attacker attaches a hooking framework. Nothing in the previous design re-measured before the operation that actually matters.

⚠️ What this returns is advisory. It is not the security boundary. On a compromised device, a branch that reads Allowed and proceeds is a branch the attacker patches. Allowed.attestationId is the part that cannot be forged: send it to your backend and redeem it against POST /api/biz/attestations/verify before honouring the operation. An integration that acts only on the local return value has bought nothing.

AttestationResult is a sealed hierarchy, so switch is exhaustive and Unavailable — the case most likely to be skipped — cannot be omitted silently. It is also a normal result rather than a thrown exception, deliberately: an exception lands in the catch you wrote for connectivity, and that is how fail-open paths get written by accident. For any operation NPCI considers critical, treat Unavailable exactly as Blocked.

Also new:

  • prewarmAttestation(action:) — fetches the challenge on screen entry so the attestation at button press costs one round trip instead of two. Only the challenge is pre-fetched; posture is always measured at call time.
  • isAttestationSupported — the native binaries are dropped in out-of-band and can be older than this plugin. Requires deepidsdk 2.2.0+ (Android) and DeepIdSDK.xcframework 2.2.0+ (iOS).

Enforcement differs by platform, and it is not symmetric. Android presents a blocking alert for block and a dismissible one for warn. iOS has no policy-enforcement layer, so it reports policyAction and does nothing. Neither platform terminates the process for close. If your response to a compromised device matters, implement it from Blocked rather than relying on the configured action.

New: the /api/sdk/sim-binding/init response is delivered to Flutter mid-flow. startSimBinding() takes an optional onSimBindingInit callback that fires as soon as the init call resolves — before the verification SMS is sent, and long before the returned Future completes.

final result = await DeepId.startSimBinding(
  phoneNumber: '+919876543210',
  onSimBindingInit: (init) {
    if (init.ok != true) print('Init rejected (${init.clientId}): ${init.error}');
  },
);

The new SimBindingInitResponse carries all eight server fields: ok, clientId, bindingHash, smsTargetMobile, smsContent, instructions, error, message.

clientId (e.g. sim_binding_hRXbkIMrSsQpUmwciUpm) is the backend's identifier for the attempt. It is present whenever the server answered — including most rejections — and unlike bindingHash it carries no secret, so it is the field to log, surface on a support screen, or quote to DeepID when reporting a failed binding.

Called for every outcome, so there is one branch to handle: the server's response verbatim when there is one (accepted or rejected), and a synthesized ok: false carrying the reason in error when the call never reached the server. Previously a rejected init surfaced only as a flattened string on the thrown DeepIdException, with the server's own error / message / instructions discarded — on iOS at two separate layers.

Delivered once per init attempt, not once per binding flow. A flow normally makes exactly one, but both platforms can start a second — Android after a retry or an invalidated token, iOS if the user double-taps confirm — and each attempt gets its own clientId and bindingHash. The callback is dropped when the Future settles so it cannot fire into a later flow.

⚠️ bindingHash and smsContent are the live verification token the SDK is about to send from the bound SIM. Exposing them is deliberate, so callers can correlate a binding attempt server-side — it is not an oversight, and it does mean the host app can read the token. Do not log or persist them, and do not send the SMS yourself; use clientId when all you need is something to correlate on. SimBindingInitResponse.toString() redacts both; toMap() does not.

Requires updated native binaries — the Dart API cannot surface what the prebuilt SDKs do not expose. Both were rebuilt for this release: deepidsdk.aar (new SimBindingCallback.onSimBindingInit, clientId on SimBindingInitResponse) and DeepIdSDK.xcframework (new DeepIdSDKManager.onSimBindingInit, clientId on DeepIdSDKInitResponse, which is now public). The Gradle AAR coordinate moved to 1.2.2 so the new bytes are actually picked up instead of a cached module.

Internal: the iOS plugin now retains its FlutterMethodChannel. It previously let the channel go out of scope in register(with:), which was fine while traffic was one-way but leaves no route back to Dart.

Reliability: the wrapper's error contract is now airtight. Every failure crossing the plugin boundary surfaces as a DeepIdException with a typed DeepIdErrorCode — a raw MissingPluginException, or a TypeError from a malformed native payload, can no longer sail past an on DeepIdException handler. The host app always finds out what happened:

  • New codes — if you switch exhaustively over DeepIdErrorCode, add branches for these:
    • loggedOut — a pending onEnrollment cancelled by logout(). Previously reported as unknown, indistinguishable from a real failure.
    • unsupportedPlatform — the method channel has no native handler (web/desktop targets, or unmocked widget tests). Previously a raw MissingPluginException.
    • malformedResponse — the native layer answered on the success path with a payload this build could not read; usually a plugin/native version mismatch. Previously a raw TypeError, or worse, an EnrollmentResult carrying empty identifiers documented as guaranteed non-empty.
  • DeepIdException.nativeCode preserves the raw platform code, so a native code this build cannot map (unknown) is still identifiable in logs.
  • Enrollment failures are never dropped. With no onError handler, onEnrollment routes the failure through FlutterError.reportError (console + installed crash reporting) instead of swallowing it.
  • Concurrent onEnrollment listeners share one native wait. Previously a second waitForEnrollment replaced the first's pending result on the native side, and the first caller's callback simply never fired.
  • startSimBinding validates the success payload: a result the wrapper cannot parse — or one claiming success: false on the success channel — throws typed instead of returning a "success" the host has to second-guess.
  • verifySecurityPolicy now cannot throw: parse failures and MissingPluginException degrade to Unavailable(internalError) like every other no-verdict path, and AttestationResult.fromMap is hardened to match its documented never-throws contract.
  • A host onSimBindingInit callback that throws no longer dies inside the method-channel handler; it is reported through FlutterError, attributed, and the binding flow continues. Likewise, an exception thrown by the host's onEnrollment onSuccess surfaces as the host's own uncaught error rather than being re-reported as a fake SDK enrollment failure.
  • initialize rejects an empty appKey / appSecret immediately with the typed code, before any native round trip.
  • logout() and the isInitialized / isAttestationSupported getters treat a missing native side as their documented no-op answers (false) instead of leaking an exception.

2.1.0 #

Device binding is stricter in this release, to meet the UPI rules on failing a binding when the customer leaves the app mid-flow and on confirming the verification SMS was really sent. Bindings that previously succeeded quietly can now fail visibly — see the behaviour notes at the end of this entry.

New (Android): the verification SMS is confirmed as sent before verification starts. The send was previously fire-and-forget, so an SMS that never left the device — mobile radio off, no service, carrier send limit reached — still moved the flow on to polling and surfaced 45 seconds later as a generic timeout. The SDK now waits for the platform's send result and fails immediately with the real reason (e.g. "The mobile radio is off"), typically within a second.

New (Android): leaving the app mid-binding rejects the binding immediately. Going Home, opening Recents, or switching apps while a binding is in flight now fails it on the spot rather than after a grace period, and drops the local binding token. Deliberate departures only: notifications — including the carrier's SMS-charge notice — incoming calls, and system permission dialogs do not trigger it. Screen lock and system-initiated backgrounding are still handled by the existing grace-period watcher.

New (Android): the verification sheet no longer appears in the Recents thumbnail, since it shows the customer's number while a token is live.

New (iOS): cancelling the Messages composer now fails the binding. It previously dismissed the sheet and left the flow waiting. Binding is also declined if control takes longer than five seconds to return from the composer.

Fix (iOS): the URL-scheme fallback rejected every binding it handled. On the path where the in-app composer is unavailable and the SDK opens Messages directly, the app backgrounding itself tripped the abandonment watcher about five seconds after hand-off, failing bindings the customer had completed correctly. That hand-off is now an expected trip and verification resumes when the app returns.

New API: DeepIdErrorCode.bindingAbandoned is reported when a binding fails because the customer left the app, as distinct from DeepIdErrorCode.userCancelled for a deliberate dismissal. Treat it as a failed binding and offer a retry. If you switch exhaustively over DeepIdErrorCode you will need to add a branch for it.

Fix (Android): logout() could silently fail to log the device out. If enrollment or SDK init was still finishing when you called it — a few seconds after onEnrollment fires, or any time before it — the native SDK wrote the old session back to storage after logout had cleared it. The next initialize() then resumed the session logout was supposed to end instead of enrolling fresh, and the enrollment callback delivered the pre-logout deepId / sessionId. On Android that stale deepId was also persisted as the authoritative one, so it kept coming back on every later launch and enrollment.

Fix (Android): repeated login/logout cycles degraded toward an ANR. The native SDK's continuous security monitoring and device polling outlived logout() for the life of the process, and the next initialize() started a second set on top of them. Each cycle added another main-thread security scan on the same interval.

Requires updated native binaries — both platforms. Most of the above lives in the native SDKs rather than this plugin: the SMS send confirmation and the logout ordering fix are in deepidsdk.aar, and the composer cancel, five-second hand-off rule and URL-scheme fix are in DeepIdSDK.xcframework. Replace both binaries in your pub cache as part of this upgrade (see Prerequisites).

Upgrading the plugin without replacing the binaries is the failure case to avoid: the build still succeeds and the flow still works, but you silently keep the old permissive behaviour — no SMS send confirmation, no composer cancel handling — while appearing to be on 2.1.0. The plugin does fence off a retired SDK instance on its own side, so an older AAR can no longer feed post-logout state back into EnrollmentResult, and the Android leave-the-app rejection ships in the plugin itself and works regardless.

Behaviour notes for integrators. Failure paths that used to pass silently now surface as errors, so expect a higher visible failure rate rather than a change in what actually works:

  • A customer who switches away mid-binding gets bindingAbandoned instead of a binding that continued in the background.
  • A verification SMS the device could not send now fails fast with the carrier reason instead of timing out after 45 seconds.
  • Verification SMS content longer than a single 160-character message now fails visibly at send time instead of being silently truncated or dropped.

bindingAbandoned is the only API addition; no existing signatures changed.

2.0.0 #

Breaking: device intelligence is now computed entirely by the DeepID backend on both platforms. The SDK still collects device facts and sends them, but no longer derives scores or verdicts on-device, and iOS ships a smaller binary.

  • deviceIntelligence in EnrollmentResult (and from getFreshDeviceIntelligence()) is now whatever the backend returns, and is null until your backend starts sending the field. If your code reads specific keys such as device_score, re-check that logic — the shape has changed.
  • getFreshDeviceIntelligence() now always makes a network round trip on iOS, matching Android.
  • SIM binding's verification timeout is now 45 seconds on both platforms (previously 15s on Android, 20s on iOS).
  • SIM binding now fails if the app is backgrounded for more than 5 seconds while the flow is in progress — a new possible cause of a simBindingFailed error.
  • Fixed an iOS bug where the SIM binding confirmation screen could show a blank SIM name on single-SIM devices.
  • Breaking: DeepId.logout() now returns Future<bool> (true if an active session was cleared, false if there was nothing to log out of) instead of Future<void>. Existing calls that don't use the return value are unaffected.

Upgrading:

flutter clean
rm -rf ~/.pub-cache/hosted/pub.dev/deepidsdk_flutter-*
cd ios && rm -rf Pods Podfile.lock && pod install

Android just needs a rebuild with the new deepidsdk.aar.

1.0.5 #

  • Feature: added DeepId.logout() — clears the current enrolled session (deepId, sessionId, DeepId credentials, SIM binding state, and any locally persisted identifier) on both Dart and native sides. After logout(), calling initialize() again performs a fresh enrollment and fires the onEnrollment callback with a new deepId / sessionId. Safe to call when no session is active; a pending onEnrollment is rejected with the message Enrollment cancelled by logout(). Example app demonstrates the flow with a new "Logout" button under "Initialize & Enroll".
  • Fix (Android): EnrollmentResult.deepId is now stable across process restarts, matching the contract documented on the field. On the second-and-subsequent launches with an existing enrollment, the plugin was delivering a different identifier than the one returned on the first fresh enrollment. The plugin now persists the correct identifier from the first enrollment and returns it on every subsequent restore.
  • Fix (Android + iOS): DeepId.getFreshDeviceIntelligence() now works on every launch, not only the first fresh enrollment. Previously, on a second-and-subsequent launch with an existing enrollment, the native device-intelligence collector was never re-initialized in the new process, so the call resolved with null ("Fresh device intelligence is unavailable"). The underlying SDKs now lazily re-create the collector on demand, the first time getFreshDeviceIntelligence() is invoked after a process start.
  • Migration: If you tested against 1.0.4 or earlier and observed the wrong deepId on subsequent launches, clear the app's storage once after upgrading (Android Settings → Apps → Storage → Clear Data, or uninstall + reinstall). Devices that never ran an affected build are not impacted. The fresh device intelligence fix requires no migration.
  • Requires updated native binaries — replace deepidsdk.aar and the iOS xcframeworks under ~/.pub-cache/hosted/pub.dev/deepidsdk_flutter-1.0.5/ with the matching artifacts from DeepID.

1.0.4 #

  • Fix (build-blocking): corrected minimum iOS target to 15.0 (was 13.0) — podspec, example Podfile, and Xcode project updated.
  • Fix (build-blocking): corrected minimum Android API to 29 (was 21) — manifest-merger fails for hosts below API 29.
  • Fix (documentation): Added troubleshooting steps for the iOS release build.

1.0.3 #

  • iOS: refreshed DeepIdSDK.xcframework with an updated device-identifier source for the enrollment and SIM binding callbacks. The deepId field name, type, and shape are unchanged — existing integrations require no code changes, though the returned identifier value may differ after re-enrollment.
  • iOS: internal native plugin-bridge updates to match the refreshed binary.
  • No public Dart API changes.

1.0.2 #

  • iOS: added ShieldPtr.xcframework as a required vendored framework alongside DeepIdSDK.xcframework. Both must be placed under ios/Frameworks/ before running pod install.
  • Documentation: updated Prerequisites, "Verify before continuing", iOS setup, and Troubleshooting sections to reflect the second xcframework requirement.

1.0.1 #

  • Documentation: clarified that native SDK binaries must be placed inside the pub cache directory (~/.pub-cache/hosted/pub.dev/deepidsdk_flutter-<version>/), not a local plugin checkout.
  • Documentation: corrected enrollmentTimeout default from 30 s to 150 s in the API reference and error-handling sections.
  • Documentation: corrected iOS mobile field description — server value is returned first, phoneNumber parameter is the fallback.
  • Documentation: renamed Android binary from deepidsdk-release.aar to deepidsdk.aar throughout.

1.0.0 #

Initial release of deepidsdk_flutter.

Features #

  • DeepId.initialize(appKey:, appSecret:, onEnrollment:, onEnrollmentError:, enrollmentTimeout:) — initializes the native SDK and kicks off background device enrollment. Returns once the SDK is constructed (does not wait for enrollment to complete). Pass onEnrollment here or call DeepId.onEnrollment() separately to be notified when deepId + sessionId are ready.
  • DeepId.onEnrollment(onSuccess:, onError:, timeout:) — callback-driven enrollment listener. Fires exactly once when both identifiers are available, or delivers a DeepIdException on timeout or failure.
  • DeepId.startSimBinding({phoneNumber}) — presents the native SIM binding sheet (Android: Jetpack Compose Activity; iOS: SwiftUI page sheet). Awaits user confirmation and carrier verification, then returns a SimBindingResult.
  • DeepId.isInitialized — async getter; true after a successful initialize() call.
  • DeepId.deepId / DeepId.sessionId — synchronous accessors populated after the enrollment callback fires.

Types #

  • EnrollmentResult — carries deepId and sessionId from a completed enrollment.
  • SimBindingResult — carries success, deepId, sessionId, mobile, and message from a completed SIM binding flow.
  • DeepIdException — typed exception with a DeepIdErrorCode and a human-readable message. Thrown by initialize() and startSimBinding(), and passed to onEnrollmentError.
  • DeepIdErrorCode — exhaustive enum covering all failure modes: notInitialized, invalidAppKey, invalidAppSecret, initFailed, enrollmentTimeout, enrollmentNotComplete, simBindingFailed, userCancelled, and more.

Platform support #

Platform Minimum version
Android API 21 (Android 5.0)
iOS 13.0

Notes #

  • The native SDK binaries (Android AAR and iOS xcframework) are distributed separately by DeepID and are not bundled in this package. See the README.md Prerequisites section for placement instructions.
  • On Android, READ_PHONE_STATE and SEND_SMS are dangerous permissions that must be granted at runtime before calling startSimBinding().
  • On iOS, add NSMotionUsageDescription to your Info.plist before submitting to the App Store.
0
likes
0
points
147
downloads

Documentation

Documentation

Publisher

verified publishersurepass.io

Weekly Downloads

Flutter plugin for the DeepID SDK (Android + iOS). Device enrollment, SIM binding, on-demand posture attestation, and device fraud detection.

Homepage
Repository (GitHub)
View/report issues

Topics

#security #fraud-detection #attestation #sim-binding #authentication

License

unknown (license)

Dependencies

flutter

More

Packages that depend on deepidsdk_flutter

Packages that implement deepidsdk_flutter