Changelog
All notable changes to the Mobile Locker JavaScript SDK are documented here.
The format follows Keep a Changelog. This project adheres to Semantic Versioning.
[Unreleased]
Section titled “[Unreleased]”isWindows()(MLJS-33) — detect the Mobile Locker Windows app viaIS_MOBILE_LOCKER_WINDOWS_APPor themobilelocker-windowsuser-agent prefix. Alsotruefor the existing Electron Windows shell (isElectron()). Exported on the public API next toisIOS/isAndroid/isElectron.isApp()treats Windows as a native app environment.
[2.0.1] — 2026-08-12
Section titled “[2.0.1] — 2026-08-12”- Scanner open calls no longer use the 30s
apiClientdefault timeout (MLJS-32 / MLI-1863).scanner.scanBusinessCardandscanner.scanBadgepass per-requesttimeout: 0(no limit) so native capture + OCR can finish without the presentation rejecting early. Other SDK traffic keeps the global 30s default. - Scanner open calls no longer use
withRetry. A retry after a network blip would open a second native scanner session; interactive host bridges fire once.
[2.0.0] — Unreleased
Section titled “[2.0.0] — Unreleased”Highlights
Section titled “Highlights”Major release focused on safe list access for large contacts/CRM tables, clearer storage APIs, and stronger typing/errors.
List host contract (MLI-1718):
GET …?limit={1…5000}&cursor={token?}→ { data, meta: { cursor: { next: string | null, count: number } } }Walks stop when meta.cursor.next === null. Prefer page-by-page processing (or SOQL for filtered CRM). Do not load unbounded full tables into memory.
Breaking changes in this release: MLJS-24, MLJS-25, MLJS-26, MLJS-27.
-
Page<T>/PageMeta/PageCursor— shared cursor-envelope types for list APIs. -
Contacts cursor paging (MLJS-27 / MLI-1718).
contacts.getPage(limit, cursor?)→Page<UserContact>contacts.eachPage(pageSize, handler)— advances only viameta.cursor.next
-
CRM list paging (MLJS-26 / MLI-1718) — same envelope for offline CRM tables. Prefer filtered SOQL via
crm.querywhen possible.Entity One page Full walk Accounts crm.getAccountsPage(limit, cursor?)crm.eachAccountsPage(pageSize, handler)Addresses crm.getAddressesPage(limit, cursor?)crm.eachAddressesPage(pageSize, handler)Contacts crm.getContactsPage(limit, cursor?)crm.eachContactsPage(pageSize, handler)Leads crm.getLeadsPage(limit, cursor?)crm.eachLeadsPage(pageSize, handler)Users crm.getUsersPage(limit, cursor?)crm.eachUsersPage(pageSize, handler)limit/pageSizemust be integers 1…5000 (elseInvalidArgument). Host:GET /mobilelocker/api/{user-contacts|crm/{entity}}?limit=&cursor=. -
storage.getAllAcrossPresentations()(MLJS-25) — correctly named replacement for the misnamed full-user list API (see Removed). -
Typed CRM models (MLJS-25) —
CRMAccount,CRMAddress,CRMContact,CRMLead,CRMUserreplaceunknownon CRM list/get helpers (shapes match iOStoJSON()). -
isAndroid()(MLJS-25) — exported on the public API next toisIOS/isElectron. -
General error codes (MLJS-25) —
InvalidArgument,UnsupportedEnvironment,NotFound. -
Domain errors now extend
MobileLockerErrorsoinstanceof MobileLockerErrormatches CRM / database / HTTP failures. -
Shared error mappers:
mapToMobileLockerError,mapToCRMError,mapToDatabaseError. -
Package
exportsnow includes atypescondition pointing atdist/index.d.ts. -
storage.get(name)prefersGET /user/user-storage-entries/item?name=(single-key host route; iOS MLJS-25) and falls back to listing current-presentation entries on older hosts.
Changed
Section titled “Changed”- Validation and platform-gate failures use
InvalidArgument/UnsupportedEnvironmentinstead ofServerError. - Presentation not-found paths use
NotFound. SDKLogDomainincludeshttp,network,permissions, andlocalforage.
Removed
Section titled “Removed”-
contacts.getAll()(MLJS-24 / MLI-1708) — breaking. Loading the entire address book in one call is unsafe for production users with 100k+ contacts (memory spike, presentation stall). -
contacts.getChunked(minID, limit)(MLJS-27) — breaking. Superseded bycontacts.getPage/eachPageagainst the MLI-1718 cursor envelope. No publicminquery param.// Beforeconst contacts = await mobilelocker.contacts.getAll()// or: await mobilelocker.contacts.getChunked(minID, 500)// After — process pages; do not push(...chunk) into one arrayawait mobilelocker.contacts.eachPage(500, (chunk) => {for (const contact of chunk) {// handle one contact}})// Single pageconst page = await mobilelocker.contacts.getPage(500)const next = page.meta.cursor.next? await mobilelocker.contacts.getPage(500, page.meta.cursor.next): nullHost list routes use the cursor envelope only (no unbound full-dump or
min/afterbare-array shapes). -
crm.getAccounts()/getAddresses()/getContacts()/getLeads()/getUsers()(MLJS-26 / MLI-1718) — breaking. Full-table CRM list loads are unsafe for large offline sets (same class of memory risk as the old contacts dump). Single-id getters (getAccount, etc.) andcrm.queryare unchanged.// Beforeconst accounts = await mobilelocker.crm.getAccounts()// After — prefer SOQL when filteringconst { rows } = await mobilelocker.crm.query('SELECT Id, Name FROM Account WHERE Name = :name',{ name: 'Acme' },)// After — offline walk; process pages; do not rebuild one arrayawait mobilelocker.crm.eachAccountsPage(500, (chunk) => {for (const account of chunk) {// handle one account}})// Single page when you already know limit + optional cursorconst page = await mobilelocker.crm.getAccountsPage(500)const next = page.meta.cursor.next? await mobilelocker.crm.getAccountsPage(500, page.meta.cursor.next): null -
storage.getAllForPresentation()(MLJS-25) — breaking rename. That method hit the unrestricted user storage list (all presentations), not “for the current presentation.” Use:storage.getAll()— current presentationstorage.getAllAcrossPresentations()— all presentations for the userstorage.getForPresentation(id)— one presentation by id
-
StorageEntrycamelCase aliases (MLJS-25) — breaking.teamID,userID,presentationID,createdAt,updatedAtremoved. Use snake_case only:team_id,user_id,presentation_id,created_at,updated_at.
[1.1.0] — 2026-06-01
Section titled “[1.1.0] — 2026-06-01”mobilelocker.localforage— a localForage-compatible key-value store backed by native app storage on iOS 5.3.0+ (Android and Windows when supported). Drop-in replacement for nativelocalforagethat is immune to the port-collision data loss problem in WKWebView. On iOS 5.3.0+ reads and writes go through the/mobilelocker/api/localstorageroutes introduced in MLI-1392; on all other environments (CDN, Electron, older iOS, local development) localForage falls back automatically to IndexedDB. All value types supported by localForage are supported, includingArrayBuffer,Blob, and typed arrays (binary types are base64-encoded for transport, with ~33% size overhead on the native path). The globallocalforageinstance is untouched — migration is opt-in. On first use, any existing data written by nativelocalforage(IndexedDB) is automatically migrated to the native store so presentations switching tomobilelocker.localforagedo not lose previously saved data.MobileLockerLocalForageTypeScript type — the interface exposed bymobilelocker.localforage, mirroring the localForage data API (getItem,setItem,removeItem,clear,length,key,keys,iterate).storage._migrate()— migrates existinglocalStorageentries into the iOS SQLite-backed store. Called automatically on first storage access when running on iOS; safe to call manually for early initialization or testing.
Changed
Section titled “Changed”- On Mobile Locker iOS 5.3.0+,
window.localStorageis transparently replaced by a database-backed storage engine. AlllocalStoragereads and writes in existing presentations are automatically routed through the same SQLite store that backsmobilelocker.localforage, making them immune to the port-collision data loss problem (MLI-1387/MLI-1388). No code changes are required — the swap happens at the native layer before any page JavaScript runs. AnylocalStorageentries written by older app versions are migrated to the new store automatically on first load and removed from nativelocalStorageon success. storage.save()on Mobile Locker app 5.3.0+ now POSTs directly to the native SQLite route (POST /mobilelocker/api/user/user-storage-entries) instead of routing through the capturedata analytics path. This fixes an issue where entries were lost or silently shared between presentations when multiple high-ID presentations were served from the same port (65535). On Mobile Locker app 5.2.1 and earlier, the capturedata path is used automatically as a fallback.storage.save()on CDN and Electron retains the capturedata analytics path but now retriesget()up to 3 times with 500ms between each attempt. Previously the immediate read-back could return stale data ornullbefore the backend had finished processing the event.storage.delete()on Mobile Locker app 5.3.0+ calls the capturedata analytics path (for backend audit trail) and immediately deletes the local SQLite record (DELETE /mobilelocker/api/user/user-storage-entries?name=X), so deleted entries no longer reappear in subsequentgetAll()calls before the next backend sync. On Mobile Locker app 5.2.1 and earlier, only the capturedata path fires.storage._migrate()only runs on Mobile Locker app 5.3.0+. On earlier versions it is a no-op, preserving existing localStorage behaviour.StorageEntryfields now use snake_case as the canonical wire format, matching the Laravel backend and iOStoJSON()output:team_id,user_id,presentation_id,created_at,updated_at.- All server responses in the
storagedomain are mapped through a new internal_fromServer()function.
Deprecated
Section titled “Deprecated”StorageEntry.teamID— useteam_idStorageEntry.userID— useuser_idStorageEntry.presentationID— usepresentation_idStorageEntry.createdAt— usecreated_atStorageEntry.updatedAt— useupdated_at
Both camelCase and snake_case keys are present on all StorageEntry objects returned by the server during this transitional period. The camelCase aliases will be removed in a future minor release.
[1.0.1] — prior release
Section titled “[1.0.1] — prior release”See git history for changes prior to 1.1.0.