Restoring Capacitor Mobile App Compatibility with a sessionStorage Shim
Restoring Capacitor Mobile App Compatibility with a sessionStorage Shim
Release: v0.1.89
Background
The myProp portal ships as both a web app and a native mobile app via Capacitor. The original portal was built with Vue.js, and its Vuex store persisted discovered user accounts to sessionStorage under the key user_accounts every time a user's accounts were fetched. The Capacitor mobile app relied on this specific contract: on startup or page refresh, it read sessionStorage['user_accounts'] to rehydrate the account list without making a network call, giving users an instant, flicker-free experience.
When the portal was rebuilt in Next.js with tRPC and React Query, this behaviour was not carried over. tRPC and React Query handle caching internally and do not write to sessionStorage. The result was that the Capacitor mobile app could no longer find the data it expected, breaking session rehydration.
v0.1.89 restores this contract with a targeted compatibility shim.
How the shim works
The fix is composed of two new files and two small modifications.
useSessionStorageSync hook
src/hooks/use-session-storage-sync.ts is the core of the shim. It is a custom React hook that:
- Accepts the result of a successful
accountDiscovery.discovertRPC query. - Maps each normalised
PortalAccountobject back to the legacyAgentOSAccountRecordshape the Capacitor app expects. - Serialises the array to JSON and writes it to
sessionStorage['user_accounts']. - Skips the write if the serialised value has not changed since the last write (deduplication via a
useRef). - Silently catches any
sessionStorageerrors — quota exceeded, private browsing, SSR — so the shim never disrupts the web experience.
The same file also exports clearSessionStorageAccounts(), a standalone utility that removes the user_accounts key.
// Simplified hook signature
useSessionStorageSync({
companyShortName: string, // Agency slug for reverse-mapping company fields
accounts: PortalAccount[] | undefined,
isSuccess: boolean,
});
SessionStorageSync component
src/components/session-storage-sync.tsx is an invisible React component that:
- Subscribes to the
accountDiscovery.discovertRPC query withstaleTime: 60_000, matching the value used by the accounts list page. - Passes the query result into
useSessionStorageSync. - Renders
null— it has no visible output.
Because it shares the same query key and staleTime as the accounts list page, no additional network requests are made. React Query returns the cached result.
// session-storage-sync.tsx renders nothing
export function SessionStorageSync() {
// ... subscribes to tRPC, calls useSessionStorageSync
return null;
}
Mounting in the company layout
<SessionStorageSync /> is mounted inside CompanyLayoutClient, which wraps all company-namespaced pages. This means the sync runs automatically on every page within the portal — no per-page wiring required.
Clearing on sign-out
CompanyShell now calls clearSessionStorageAccounts() before invoking signOut(). This mirrors the original Vue app's clearAccountsData Vuex action, which removed the user_accounts key on logout to prevent stale data from being rehydrated on the next app launch.
sessionStorage data format
The shim writes a JSON array. Each element matches the original AgentOS API response shape:
[
{
"OID": "abc123",
"Name": "Jane Smith",
"EmailAddress": "jane@example.com",
"AccountType": "LettingsLandlord",
"CompanyShortName": "myagency",
"CompanyMarketingName": "My Agency Ltd",
"CompanyLogo": "https://cdn.example.com/logo.png",
"IsActive": true,
"BranchID": "branch-001"
}
]
AccountType reverse mapping
The Next.js app uses normalised portal type strings internally. The shim maps these back to the AgentOS AccountType values the Capacitor app expects:
| Portal type | AgentOS AccountType |
|---|---|
landlord | LettingsLandlord |
tenant | LettingsTenant |
buyer | SalesApplicantBuyer |
vendor | SalesVendor |
contractor | MaintenanceContractor |
If the original AgentOS field values are available in the account's metadata object (preserved during account discovery), those are used directly rather than the reverse-mapped values.
Design decisions
| Decision | Rationale |
|---|---|
| Share React Query cache | Avoids a duplicate network request. The component uses the same query key and staleTime as the accounts list page. |
Deduplicate writes with useRef | Prevents repeated sessionStorage.setItem calls on re-renders where account data hasn't changed. |
| Silent error handling | Storage failures (private browsing, quota limits, SSR) must never break the web portal. The Capacitor app already handles missing storage data gracefully. |
| Metadata passthrough | Preserves fidelity of the original AgentOS field values where available, rather than relying solely on reverse-mapped approximations. |
| Clear on sign-out | Matches the original app's logout behaviour exactly, preventing ghost sessions in the Capacitor app. |