Shared UI, separate runtimes
packages/app-ui and packages/app-core are the single customer UI and contract implementation. apps/demo-runtime supplies simulated capabilities to the WordPress showcase. apps/mobile supplies actual device adapters and fails closed for unsupported operations.
Why
Maintaining two similar-looking apps by hand doubles the correction effort and allows presenter behaviour to drift away from the shipped app. Build-time separation retains fast browser demos without allowing fake transaction success into a native release.
Trade-off
- Shared legacy JavaScript/global state still needs typed modular conversion before the native release gate can pass.
- A demo may show a confirmation for teaching; the native app must obtain authoritative server evidence.
- The build cannot use a run-time
DEMO_MODEswitch to choose between financial simulations and production actions.
Boundaries to test
Shared files have no imports from the demo or WordPress presenter. Mobile bundles exclude demo data and sample payment successes. Each runtime imports the same UI/core; independent adapters implement their own service ports.
Ownership of the screen implementation
packages/app-ui owns the rendered customer experience. packages/app-core owns the service contracts that make those screens independent of a particular runtime. This choice was made because there are five venue styles but not five wholly different sets of customer tasks. A table-ordering defect should be fixed once rather than patched across independent Web, Android and iOS implementations. Reusing screens also keeps visual review meaningful: a bug seen in the WordPress presenter is likely in a shared source file.
Adapters are essential, not optional complexity
The browser presenter uses apps/demo-runtime to supply sample menu items, simulated logins, fictional payment results and browser-friendly QR links. The mobile app uses apps/mobile and must call real native plugins or server APIs. The two runtimes are separate entry points so demo behaviour cannot be enabled in the APK with a loose runtime flag. The adapter introduces additional code and tests, but pays for itself by giving one UI a truthful response appropriate to its environment.
A screen should request an abstract action, such as scanQr, authenticate or submitOrder, then handle pending, completed, cancelled and error results. It should not import WordPress presenter chrome or an Android plugin implementation itself. The contract should make impossible states difficult to express: unknown payment settlement is not success, camera cancellation is not scanner failure, and network connectivity is not proof of server reachability.
Existing technical debt
Some shared screens remain in older global-style JavaScript files, including legacy/AppScreens.js, legacy/TableOrdering.js and legacy/VenueScreens.js. The integration has service boundaries but is not fully typed or modular. A refactor should proceed by isolating a screen and its state transitions, migrating it to a typed component/controller, retaining exact visual behaviour and running the same presenter and device-contract tests. Rewriting the visual design during the refactor would make it harder to distinguish regression from intentional change.
Integration flow example
For Scan Table, the customer presses the shared control. The current runtime calls its scanQr capability. The presenter can return a predefined safe URL with an explicit demo label. The Android runtime invokes Capacitor Barcode Scanner and returns the scanned content or cancellation. The shared destination router validates the URL and then changes venue/table context. The backend still has authority over any later order. The QR value itself does not authenticate or authorize the user.
Verification and release boundary
Run separate dependency-graph checks for the demo and mobile entry points. Ensure the native compiled bundle excludes the presenter iframe, sample payment successes, demo-only assets and mock responses. Compile both entry points and run browser flows to detect UI regressions; then test the real APK on a phone to prove native callbacks. If either runtime cannot satisfy a new contract, do not hide the missing operation with a simulated response. Document it as unsupported and block the affected release tier.