to select ↑↓ to navigate
mForm Docs

mForm Docs

Open in ChatGPT
Ask ChatGPT about this page
Open in Claude
Ask Claude about this page

2.0.0-beta.2

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 in eval:!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_filters expression no longer breaks the fieldname lookup.
  • A missing docstatus now defaults to 0, matching how Frappe treats a new document, instead of comparing as null and 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.

Last updated 4 weeks ago
Was this helpful?
Thanks!