Prerelease · Published 12 August 2026 · Second prerelease on the 2.0 line
pub.dev still resolves 1.2.0 by default. Ask for this build by name:
dependencies:
frappe_mobile_sdk: ^2.0.0-beta.2
This supersedes 2.0.0-beta. If you're moving straight from 1.x, read that page first for the offline-first foundation, then come back here for what changed since.
iOS build system
The plugin's Swift source now lives under Swift Package Manager as well as CocoaPods. Package.swift sits alongside the existing podspec and declares no dependencies of its own, so it doesn't impose a newer Flutter floor on your app than the SDK already needs. Both build systems work; pick whichever your app already uses.
The version floor is now measured, not guessed
pubspec.yaml now says sdk: ">=3.9.0 <4.0.0" and flutter: ">=3.35.0". The floor comes from an actual API the SDK calls, DropdownButtonFormField(initialValue:), which only exists from Flutter 3.35. This was verified against Flutter 3.35.7, 3.38.10, and 3.41.5. The previous >=1.10.0 passed pub.dev's publish check but claimed support for Flutter versions that would not build.
Dependency ranges widened, not bumped
geolocator, package_info_plus, app_links, and flutter_form_builder all got wider ranges, not higher floors. Every widened range is a strict superset of what beta shipped, so an app on an older Flutter still resolves the same versions it did before. Nothing is forced to upgrade.
Session health, out loud
SessionHealth (healthy / degraded / expired) is now public, exposed as sdk.sessionHealth. The SDK does not act on this value itself, it only tells you what happened. Prompt for re-login on expired. Leave degraded alone; it clears itself once a refresh succeeds. See Authentication.
Dead sessions stop retrying forever
Frappe answers an unredeemable refresh token with 417, not 401. Beta treated 417 as a transient failure, so a dead session retried on every request indefinitely and eventually tripped the server's rate limiter, with no path back to login. Fixed: a 417 on the refresh call now clears the token and publishes expired.
A related fix on the OAuth side: Frappe answers a dead OAuth grant with 400, which the SDK's classifier didn't recognize either, so an OAuth session could stay "healthy" forever while carrying a token the server would never accept. Both leaks are closed now, in opposite directions from a fix that shipped earlier in the same release cycle. One earlier pass wiped tokens too eagerly, on any error, including a dropped connection. This one stops it wiping on transport failures while still wiping on a genuinely dead grant.
Refresh backoff, and it actually recovers
Failed refreshes now back off on a ladder: 30 seconds, 2 minutes, 5 minutes, 15 minutes, repeating at the top. A 429 jumps straight to the top rung. Transport failures (you were offline) do not climb the ladder. Being offline costs the server's rate-limit budget nothing, so it shouldn't cost you a longer wait once you're back.
Sync correctness
Two fixes here are both about a page of results that comes back empty:
- A malformed or truncated bulk-fetch response used to be read the same as "every document on this page was denied." Now it throws instead, so the page is retried rather than treated as a permission decision. The difference matters because the earlier behavior could silently skip documents that were never actually denied.
- A fully-filtered incremental page used to freeze that doctype's sync watermark permanently, because the pull cursor had no way to step past a block of denied rows. It now advances past the block when it safely can, so sync keeps making progress instead of getting stuck at one page forever.
Form logic
Several depends_on expression types that silently misbehaved now work correctly:
- A leading
!(as ineval:!doc.__islocal) never evaluated. The common "lock this field once saved" pattern now actually locks. .some()over a child table now evaluates instead of always returning false.- A trailing semicolon in a
link_filtersexpression no longer breaks the fieldname lookup. - A missing
docstatusnow defaults to0, matching how Frappe treats a new document, instead of comparing asnulland failing every draft-only rule. - Mandatory and read-only rules that the evaluator can't parse now fail open (visible/editable) instead of fail closed. An unparseable rule can no longer make a field permanently mandatory with no way to satisfy it.
The Rating scale fix
Frappe stores a Rating as a 0..1 fraction (three stars out of five is 0.6), not a star count. The widget was writing a plain integer star count, so a rating saved through the SDK was on a different scale from every other Frappe client, and a rating saved elsewhere rendered as zero stars in the app. Fixed to store and read the fraction.
One case is genuinely ambiguous on upgrade: a stored value of exactly 1 could mean "one star" under the old scheme or "full marks" under Frappe's. Nothing in the value tells you which, so it now reads as full marks. This only affects installs that saved ratings through the SDK before this release, so decide whether to migrate those rows before you ship.
A doctype can now disappear from the workspace mid-session
If the server permanently refuses a doctype (403, or a non-gateway 5xx, not a timeout, not a 502/503/504), the SDK stops asking for it until the next login. It drops out of the app's workspace, then comes back on relaunch or on a call to resyncMobileConfiguration(). This is deliberate: it stops a doctype the user can't read from being requested over and over. But if your app renders its workspace from a cached list, the list can shrink within a session.
What's verified how
Most of this release is covered by an automated test suite exercising real request/response shapes. Three things are verified by tests and source review only, not on a live device or a live server, and are worth exercising yourself before you ship: the token-refresh re-entrancy fix, the schema audit-column backfill, and the iOS SPM build path. The CHANGELOG has the full detail on all three, plus everything else that shipped in this release.
Upgrade notes
If you're already on beta:
dependencies:
frappe_mobile_sdk: ^2.0.0-beta.2
No new server requirement beyond what beta already needed (Control >= 1.0.0). If you're carrying stored Rating values from an install that used the SDK before this release, read the Rating section above before you ship.
If you're upgrading from 1.x directly, start with the SDK 2.0 guide and the rollout order it describes: server first, flag second, app third.