deepidsdk_flutter 2.4.0
deepidsdk_flutter: ^2.4.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.4.0 #
Breaking for integrators — you no longer place native binaries anywhere. Both SDKs are now downloaded by your build and pinned to the versions this plugin was built against. iOS needs no setup at all. Android needs a
read:packagestoken in~/.gradle/gradle.properties(gpr.user/gpr.key) instead of a file — README → Installation has the setup; contact DeepID for a token. Delete anydeepidsdk.aarorDeepIdSDK.xcframeworkyou previously placed in the pub cache.
Changed (iOS): the xcframework is downloaded and checksum-verified during
pod install. The podspec fetches it from the public deepidsdk-ios-binary
repo, pinned by version and SHA-256 in the plugin's natives.json, and caches it
in ~/Library/Caches/com.deepidsdk.ios/ so it downloads once per version per
machine rather than once per project. No credentials are involved: the framework
is inert without the appKey / appSecret the backend verifies.
This closes the last silent-drift path in the plugin. An out-of-band xcframework had no version pin anywhere in the package — an older one still compiled, it just lacked that release's iOS fixes, with no warning at build or runtime. A mismatched pair is now unrepresentable on both platforms.
Changed: native SDK versions moved to natives.json. One file at the package
root pins both platforms; ios/deepidsdk_flutter.podspec reads it with Ruby's
JSON and android/build.gradle with Groovy's JsonSlurper, so a version bump is
a single edit rather than two that can disagree.
Changed (Android): the SDK is resolved from GitHub Packages instead of a
vendored AAR. android/build.gradle now declares
com.deepidsdk:deepidsdk-android as an ordinary Maven dependency, pinned to the
version this plugin was built against. android/libs/ and the generated
android/local-maven/ tree are gone, along with the copy-at-configuration-time
step that populated them.
This makes the plugin↔SDK pairing checkable. Previously the AAR was a file with
no version in its name, so nothing in git recorded which SDK a given plugin
revision expected, and a mismatch surfaced as an unresolved-reference error at a
customer's build. The version is now pinned in a tracked file and upgrades with
the plugin. Override it for a pre-release check with -PdeepidSdkVersion=x.y.z
or DEEPID_SDK_VERSION.
Fixed (Android): the plugin no longer restates the SDK's dependency versions.
Room, sqlite, okhttp, gson, security-crypto, play-integrity, play-services-appset,
appcompat, material and core-ktx were declared in android/build.gradle and had
to be kept in lockstep with the SDK's own build by hand — the hand-written POM
the old local-Maven step generated declared no dependencies at all. A stale entry
did not fail the build; it threw AbstractMethodError at runtime once Room's
generated _Impl signatures shifted. The published POM now supplies all of them
at the versions the SDK was compiled against, and the plugin declares only what
its own Kotlin compiles against.
Added (Android): configuration-time credential check. A build with no usable
credentials stops immediately with a message naming the setup step, instead of
failing later with a bare 401 from the registry. GITHUB_USERNAME /
GITHUB_TOKEN and GH_USERNAME / GH_TOKEN are accepted as well, as Gradle
properties or environment variables, for CI. Apps that set
RepositoriesMode.FAIL_ON_PROJECT_REPOS must declare DeepID's repository in
their own settings.gradle; the README carries the block.
2.3.0 #
Requires the native binaries built for 2.3.0. The Android bridge calls into a SIM-binding API that older AARs do not have, so pairing this release with an older
deepidsdk.aarfails the build with an unresolved-reference error naming a DeepID class. An olderDeepIdSDK.xcframeworkstill compiles, but every iOS fix below is simply absent from it. Replace both binaries when you upgrade.
Fix (Android): leaving the app mid-binding now rejects the binding on every
device. Rejection previously rode entirely on onUserLeaveHint, which several
OEM launcher and gesture-navigation stacks (observed on ColorOS) never deliver
for Recents or a quick app switch — a customer could hop through another app and
come back with the binding still alive. The hint remains the instant tier; a
process-level lifecycle backstop now rejects whenever the app actually leaves
the foreground, however it got there. Same-process interruptions — permission
dialogs, the notification shade, OEM SMS-confirmation dialogs, incoming-call
banners, and the SMS-charge notification the UPI rules exempt from "toggling" —
cannot trip it. Entering split-screen, where neither signal fires, now rejects
on entry.
Fix (iOS): switching away mid-binding now rejects the binding immediately on
every device. The previous five-second grace was a scheduled work item, which
froze the moment iOS suspended the process; whether the rejection ever fired
depended on how long each device kept the backgrounded app runnable, and then on
a race against the cancel-on-return path. The rejection now runs synchronously
inside the didEnterBackground handler, before suspension can interfere. The
deliberate trip to the Messages app on the URL-scheme fallback remains exempt,
and the five-second composer-handoff rule is unchanged — those two are the NPCI
behaviours; the grace period never was one. A late composer callback or a
foreground return can no longer revive a flow that already ended.
Changed (iOS): a background rejection is now reported as
DeepIdErrorCode.bindingAbandoned, matching Android. It previously surfaced
as userCancelled, which misfiled an abandonment as a deliberate dismissal.
Fix (iOS): a binding rejected while the SMS composer was open left the composer standing. The verification SMS is composed in a system sheet inside the app, so leaving the app mid-compose now rejects the binding — but the composer stayed on screen, putting the customer back in front of a live compose sheet still holding the verification token for a binding the SDK had already refused. One tap on Send and the backend would see a token the host was told to treat as abandoned. Any terminal result now takes the composer down with it, matching Android, where rejecting finishes the whole Activity.
Fix (iOS): SIM binding text was invisible on devices in Dark Mode. Screen
titles, the confirmation description and the countdown timer were drawn in
.primary — an adaptive colour that resolves to white — on a sheet whose
background is a fixed light card, so on a device set to Dark Mode they
disappeared while the hardcoded-dark body text stayed readable. The sheet
reported as "UI elements not showing" was fully rendered and functional; it
was white-on-white. The themed text colours are now fixed, and the SDK pins
its own view subtree to the light colour scheme so untinted system elements
resolve against the card rather than the device appearance. Nothing changes in
Light Mode. A host that themes cardBackground dark must theme the text
colours to match.
Fix (iOS): the confirm step could render "Make sure is selected as a default
SIM…" with a hole where the SIM name belongs, whenever CoreTelephony reported
no provider (common on eSIM-only devices) and no phoneNumber was passed. That
case now uses a complete standalone sentence.
New (Android): the UPI 'SMS sent check', with an onSmsSentCheck callback.
After the platform's sentIntent confirms the verification SMS left the radio,
the SDK now reads the device's sent SMS box and validates a record addressed to
the verification number carrying the verification content — the guideline's
assurance that a binding cannot be granted on a token that was never sent from
the device the app is installed on. When the check runs and finds no matching
record, the binding fails. When it cannot run — READ_SMS not granted, an
unreadable provider — the flow proceeds, because an unreadable sent box is not
evidence the SMS was not sent; the result says why so hosts can enforce a
stricter policy themselves. Either way the outcome is delivered mid-flow to the
new optional onSmsSentCheck parameter of startSimBinding() as an
[SmsSentCheckResult]. The AAR manifest now declares READ_SMS (same
permission group as SEND_SMS, so the existing SMS consent dialog covers it);
iOS never delivers this callback since iOS offers no read access to the SMS
store. AAR coordinate bumped to com.deepidsdk:deepidsdk:1.3.0.
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
Allowedand proceeds is a branch the attacker patches.Allowed.attestationIdis the part that cannot be forged: send it to your backend and redeem it againstPOST /api/biz/attestations/verifybefore 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. Requiresdeepidsdk2.2.0+ (Android) andDeepIdSDK.xcframework2.2.0+ (iOS).
Enforcement differs by platform, and it is not symmetric. Android presents a blocking alert for
blockand a dismissible one forwarn. iOS has no policy-enforcement layer, so it reportspolicyActionand does nothing. Neither platform terminates the process forclose. If your response to a compromised device matters, implement it fromBlockedrather 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.
⚠️
bindingHashandsmsContentare 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; useclientIdwhen 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
switchexhaustively overDeepIdErrorCode, add branches for these:loggedOut— a pendingonEnrollmentcancelled bylogout(). Previously reported asunknown, indistinguishable from a real failure.unsupportedPlatform— the method channel has no native handler (web/desktop targets, or unmocked widget tests). Previously a rawMissingPluginException.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 rawTypeError, or worse, anEnrollmentResultcarrying empty identifiers documented as guaranteed non-empty.
DeepIdException.nativeCodepreserves 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
onErrorhandler,onEnrollmentroutes the failure throughFlutterError.reportError(console + installed crash reporting) instead of swallowing it. - Concurrent
onEnrollmentlisteners share one native wait. Previously a secondwaitForEnrollmentreplaced the first's pending result on the native side, and the first caller's callback simply never fired. startSimBindingvalidates the success payload: a result the wrapper cannot parse — or one claimingsuccess: falseon the success channel — throws typed instead of returning a "success" the host has to second-guess.verifySecurityPolicynow cannot throw: parse failures andMissingPluginExceptiondegrade toUnavailable(internalError)like every other no-verdict path, andAttestationResult.fromMapis hardened to match its documented never-throws contract.- A host
onSimBindingInitcallback that throws no longer dies inside the method-channel handler; it is reported throughFlutterError, attributed, and the binding flow continues. Likewise, an exception thrown by the host'sonEnrollmentonSuccesssurfaces as the host's own uncaught error rather than being re-reported as a fake SDK enrollment failure. initializerejects an emptyappKey/appSecretimmediately with the typed code, before any native round trip.logout()and theisInitialized/isAttestationSupportedgetters 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
bindingAbandonedinstead 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.
deviceIntelligenceinEnrollmentResult(and fromgetFreshDeviceIntelligence()) is now whatever the backend returns, and isnulluntil your backend starts sending the field. If your code reads specific keys such asdevice_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
simBindingFailederror. - Fixed an iOS bug where the SIM binding confirmation screen could show a blank SIM name on single-SIM devices.
- Breaking:
DeepId.logout()now returnsFuture<bool>(trueif an active session was cleared,falseif there was nothing to log out of) instead ofFuture<void>. Existing calls that don't use the return value are unaffected.
Upgrading:
flutter clean
rm -rf ~/.pub-cache/hosted/pub.flutter-io.cn/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. Afterlogout(), callinginitialize()again performs a fresh enrollment and fires theonEnrollmentcallback with a newdeepId/sessionId. Safe to call when no session is active; a pendingonEnrollmentis rejected with the messageEnrollment cancelled by logout(). Example app demonstrates the flow with a new "Logout" button under "Initialize & Enroll". - Fix (Android):
EnrollmentResult.deepIdis 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 withnull("Fresh device intelligence is unavailable"). The underlying SDKs now lazily re-create the collector on demand, the first timegetFreshDeviceIntelligence()is invoked after a process start. - Migration: If you tested against 1.0.4 or earlier and observed the wrong
deepIdon 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.aarand the iOS xcframeworks under~/.pub-cache/hosted/pub.flutter-io.cn/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.xcframeworkwith an updated device-identifier source for the enrollment and SIM binding callbacks. ThedeepIdfield 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.xcframeworkas a required vendored framework alongsideDeepIdSDK.xcframework. Both must be placed underios/Frameworks/before runningpod 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.flutter-io.cn/deepidsdk_flutter-<version>/), not a local plugin checkout. - Documentation: corrected
enrollmentTimeoutdefault from 30 s to 150 s in the API reference and error-handling sections. - Documentation: corrected iOS
mobilefield description — server value is returned first,phoneNumberparameter is the fallback. - Documentation: renamed Android binary from
deepidsdk-release.aartodeepidsdk.aarthroughout.
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). PassonEnrollmenthere or callDeepId.onEnrollment()separately to be notified whendeepId+sessionIdare ready.DeepId.onEnrollment(onSuccess:, onError:, timeout:)— callback-driven enrollment listener. Fires exactly once when both identifiers are available, or delivers aDeepIdExceptionon 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 aSimBindingResult.DeepId.isInitialized— async getter;trueafter a successfulinitialize()call.DeepId.deepId/DeepId.sessionId— synchronous accessors populated after the enrollment callback fires.
Types #
EnrollmentResult— carriesdeepIdandsessionIdfrom a completed enrollment.SimBindingResult— carriessuccess,deepId,sessionId,mobile, andmessagefrom a completed SIM binding flow.DeepIdException— typed exception with aDeepIdErrorCodeand a human-readablemessage. Thrown byinitialize()andstartSimBinding(), and passed toonEnrollmentError.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.mdPrerequisites section for placement instructions. - On Android,
READ_PHONE_STATEandSEND_SMSare dangerous permissions that must be granted at runtime before callingstartSimBinding(). - On iOS, add
NSMotionUsageDescriptionto yourInfo.plistbefore submitting to the App Store.