feat: implement multi-device sync hardening with periodic auto-refresh and table-open synchronization
This commit is contained in:
parent
881c4af73a
commit
93ac66fa5b
@ -17,7 +17,7 @@ Features
|
||||
3. Portrait Display Mode: Single-pane responsive layout for mobile POS terminals.
|
||||
""",
|
||||
'author': "Suherdy Yacob",
|
||||
'depends': ['point_of_sale'],
|
||||
'depends': ['point_of_sale', 'pos_restaurant'],
|
||||
'data': [
|
||||
'views/res_config_settings_views.xml',
|
||||
],
|
||||
@ -33,6 +33,8 @@ Features
|
||||
'pos_ui_optimization/static/src/app/screens/product_screen/order_summary_patch.xml',
|
||||
# Safe multi-device Release table guard
|
||||
'pos_ui_optimization/static/src/app/screens/product_screen/order_summary/safe_release_patch.js',
|
||||
# Multi-device sync hardening (table-open sync, 30s auto-refresh, visibility refresh)
|
||||
'pos_ui_optimization/static/src/app/screens/floor_screen/floor_screen_sync_patch.js',
|
||||
# Payment screen patches
|
||||
'pos_ui_optimization/static/src/app/screens/payment_screen/payment_screen_patch.xml',
|
||||
# Ticket screen patches
|
||||
|
||||
212
static/src/app/screens/floor_screen/floor_screen_sync_patch.js
Normal file
212
static/src/app/screens/floor_screen/floor_screen_sync_patch.js
Normal file
@ -0,0 +1,212 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
/**
|
||||
* pos_ui_optimization — Multi-Device Sync Hardening
|
||||
* ==================================================
|
||||
*
|
||||
* This patch adds three complementary sync strategies that work on top of
|
||||
* Odoo's existing WebSocket-based DevicesSynchronisation to ensure order
|
||||
* data stays fresh on all devices in a multi-tablet restaurant setup.
|
||||
*
|
||||
* === Android System WebView Compatibility Notes ===
|
||||
*
|
||||
* This file is intentionally written to be compatible with Android System
|
||||
* WebView versions used by the Odoo Android app (Chrome/WebView 80+):
|
||||
*
|
||||
* requestIdleCallback:
|
||||
* ✅ Supported in Android WebView 47+ (all modern devices).
|
||||
* We have a setTimeout fallback for any older environment.
|
||||
*
|
||||
* visibilitychange:
|
||||
* ⚠️ Reliable in Chrome browser, but in Android WebView the event only
|
||||
* fires when the WebView loses DOM focus — NOT when the entire Android
|
||||
* app is backgrounded by the OS, unless the native app explicitly calls
|
||||
* WebView.onPause()/onResume(). The Odoo Android app does implement this
|
||||
* correctly, so visibilitychange works. As a safety net, we also register
|
||||
* a focus/blur fallback that works regardless of native app lifecycle.
|
||||
*
|
||||
* Optional chaining (?.) and nullish coalescing (??):
|
||||
* ✅ Supported in Chrome 80+ / Android WebView from early 2020.
|
||||
* All active Odoo Android installs run Chrome 80+. No changes needed.
|
||||
*
|
||||
* Numeric separators (30_000):
|
||||
* ⚠️ Not supported in WebView < 75. Replaced with plain 30000.
|
||||
*
|
||||
* Strategy 1 — Sync-on-table-open:
|
||||
* Pulls fresh data from the server before the FloorScreen opens a table so
|
||||
* cashiers always see up-to-date orders (not stale local state).
|
||||
*
|
||||
* Strategy 2 — Periodic auto-refresh (30 s):
|
||||
* A background timer fires every 30 seconds ONLY when safe:
|
||||
* • The device is on the FloorScreen (never during order editing).
|
||||
* • No input is focused (user is not typing a quantity/note).
|
||||
* • No concurrent refresh is already running.
|
||||
* • Uses requestIdleCallback so the browser picks a CPU-idle moment,
|
||||
* preventing dropped frames or UI stutter.
|
||||
*
|
||||
* Strategy 3 — Visibility + focus/blur refresh:
|
||||
* Triggers a refresh when:
|
||||
* • document.visibilitychange fires (Chrome browser, Odoo Android app).
|
||||
* • window 'focus' fires (fallback for Android WebView when the OS
|
||||
* resumes the app but visibilitychange did not fire).
|
||||
*/
|
||||
|
||||
import { FloorScreen } from "@pos_restaurant/app/screens/floor_screen/floor_screen";
|
||||
import { PosStore } from "@point_of_sale/app/services/pos_store";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { onMounted, onWillUnmount } from "@odoo/owl";
|
||||
|
||||
// Auto-refresh interval (avoid numeric separators for WebView < 75 compat)
|
||||
const AUTO_REFRESH_INTERVAL_MS = 30000;
|
||||
|
||||
// ─── PosStore patch ───────────────────────────────────────────────────────────
|
||||
patch(PosStore.prototype, {
|
||||
/**
|
||||
* Start all background sync strategies. Called from FloorScreen.onMounted.
|
||||
* Guards against double-initialisation (e.g. if FloorScreen unmounts/remounts).
|
||||
*/
|
||||
setupSyncRefresh() {
|
||||
if (this._syncRefreshInitialized) {
|
||||
return;
|
||||
}
|
||||
this._syncRefreshInitialized = true;
|
||||
this._refreshInProgress = false;
|
||||
|
||||
// Strategy 2: periodic timer
|
||||
this._autoRefreshInterval = setInterval(() => {
|
||||
this._tryAutoRefresh();
|
||||
}, AUTO_REFRESH_INTERVAL_MS);
|
||||
|
||||
// Strategy 3a: Page Visibility API
|
||||
// Works in Chrome browser and in the Odoo Android app (which calls
|
||||
// WebView.onPause/onResume bridging visibility state correctly).
|
||||
this._onVisibilityChange = () => {
|
||||
if (document.visibilityState === "visible") {
|
||||
this._tryAutoRefresh();
|
||||
}
|
||||
};
|
||||
document.addEventListener("visibilitychange", this._onVisibilityChange);
|
||||
|
||||
// Strategy 3b: window focus fallback for Android WebView environments
|
||||
// where visibilitychange may not fire when the OS resumes the app.
|
||||
// Deduplicated with _refreshInProgress so it never double-triggers.
|
||||
this._onWindowFocus = () => {
|
||||
// Add a short delay: Android sometimes fires focus before the
|
||||
// WebView has fully re-rendered after coming back to foreground.
|
||||
setTimeout(() => {
|
||||
this._tryAutoRefresh();
|
||||
}, 500);
|
||||
};
|
||||
window.addEventListener("focus", this._onWindowFocus);
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove all timers and event listeners. Called from FloorScreen.onWillUnmount.
|
||||
*/
|
||||
destroySyncRefresh() {
|
||||
if (this._autoRefreshInterval) {
|
||||
clearInterval(this._autoRefreshInterval);
|
||||
this._autoRefreshInterval = null;
|
||||
}
|
||||
if (this._onVisibilityChange) {
|
||||
document.removeEventListener("visibilitychange", this._onVisibilityChange);
|
||||
this._onVisibilityChange = null;
|
||||
}
|
||||
if (this._onWindowFocus) {
|
||||
window.removeEventListener("focus", this._onWindowFocus);
|
||||
this._onWindowFocus = null;
|
||||
}
|
||||
this._syncRefreshInitialized = false;
|
||||
this._refreshInProgress = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Central guard method — decides whether it is safe to run readDataFromServer()
|
||||
* and schedules it via requestIdleCallback (or setTimeout(0) as fallback).
|
||||
*
|
||||
* Guards:
|
||||
* 1. No concurrent refresh already running.
|
||||
* 2. Skip if the user is actively typing in an input/textarea.
|
||||
* 3. Only run while on the FloorScreen (never during order editing).
|
||||
*/
|
||||
_tryAutoRefresh() {
|
||||
// Guard 1
|
||||
if (this._refreshInProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Guard 2: skip while the user is typing
|
||||
const activeEl = document.activeElement;
|
||||
const activeTag = activeEl ? activeEl.tagName : "";
|
||||
if (activeTag === "INPUT" || activeTag === "TEXTAREA") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Guard 3: only on FloorScreen
|
||||
const mainScreen = this.mainScreen;
|
||||
const component = mainScreen ? mainScreen.component : null;
|
||||
const screenName = component ? component.name : null;
|
||||
if (screenName && screenName !== "FloorScreen") {
|
||||
return;
|
||||
}
|
||||
|
||||
const doRefresh = async () => {
|
||||
this._refreshInProgress = true;
|
||||
try {
|
||||
if (this.deviceSync) {
|
||||
await this.deviceSync.readDataFromServer();
|
||||
}
|
||||
} catch (_e) {
|
||||
// Non-blocking — if server is unreachable, just skip silently
|
||||
} finally {
|
||||
this._refreshInProgress = false;
|
||||
}
|
||||
};
|
||||
|
||||
// requestIdleCallback: lets the browser pick a CPU-idle moment between
|
||||
// animation frames, preventing dropped frames or visible stutter.
|
||||
// Supported in Android WebView 47+ (all modern devices).
|
||||
// Falls back to setTimeout(fn, 0) which at minimum yields the call
|
||||
// stack and runs after the current paint cycle.
|
||||
if (typeof requestIdleCallback === "function") {
|
||||
requestIdleCallback(() => doRefresh(), { timeout: 5000 });
|
||||
} else {
|
||||
setTimeout(() => doRefresh(), 0);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// ─── FloorScreen patch ────────────────────────────────────────────────────────
|
||||
patch(FloorScreen.prototype, {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
|
||||
onMounted(() => {
|
||||
this.pos.setupSyncRefresh();
|
||||
});
|
||||
|
||||
onWillUnmount(() => {
|
||||
this.pos.destroySyncRefresh();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Strategy 1: Sync-on-table-open.
|
||||
*
|
||||
* Pulls the latest order state from the server before navigating into a
|
||||
* table's order view. Non-blocking on failure — the cashier can always
|
||||
* open the table even if the server is temporarily unreachable.
|
||||
*/
|
||||
async onClickTable(table, ev) {
|
||||
if (!this.pos.isEditMode && !this.pos.isOrderTransferMode && !table.parent_id) {
|
||||
try {
|
||||
if (this.pos.deviceSync) {
|
||||
await this.pos.deviceSync.readDataFromServer();
|
||||
}
|
||||
} catch (_e) {
|
||||
// Non-blocking: proceed with local data if server unreachable
|
||||
}
|
||||
}
|
||||
return super.onClickTable(table, ev);
|
||||
},
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user