diff --git a/apps/user-app-v2/src/components/ui/PageBar.vue b/apps/user-app-v2/src/components/ui/PageBar.vue index e18c58e..af5d9ea 100644 --- a/apps/user-app-v2/src/components/ui/PageBar.vue +++ b/apps/user-app-v2/src/components/ui/PageBar.vue @@ -30,13 +30,24 @@ diff --git a/apps/user-app-v2/src/main.ts b/apps/user-app-v2/src/main.ts index 10237f9..4f9397e 100644 --- a/apps/user-app-v2/src/main.ts +++ b/apps/user-app-v2/src/main.ts @@ -42,7 +42,10 @@ app.use(router); setUnauthorizedHandler(() => { useSessionStore().logout(); if (router.currentRoute.value.name !== 'login') { - void router.push({ name: 'login' }); + // `replace`: the page whose request just failed is behind a dead session, + // so leaving it in history only gives the person a back button that returns + // them to it and bounces straight back here. + void router.replace({ name: 'login' }); } }); // Ported from legacy main.ts:32-46 — Vector.svg is both the while-loading and diff --git a/apps/user-app-v2/src/router/index.ts b/apps/user-app-v2/src/router/index.ts index fb19467..8e577a6 100644 --- a/apps/user-app-v2/src/router/index.ts +++ b/apps/user-app-v2/src/router/index.ts @@ -187,6 +187,11 @@ router.afterEach((to) => { const nav = useNavStore(); const transition = useTransitionStore(); + // Remembers where in the browser's history this app instance started, which + // is what PageBar's back button compares against before choosing between a + // real history-back and its fallback route. + nav.noteFirstNavigation(); + transition.setTransitionFlag(false); transition.setTransitionEnded(false); setTimeout(() => { diff --git a/apps/user-app-v2/src/stores/navStore.ts b/apps/user-app-v2/src/stores/navStore.ts index cdb3331..c28a407 100644 --- a/apps/user-app-v2/src/stores/navStore.ts +++ b/apps/user-app-v2/src/stores/navStore.ts @@ -18,6 +18,18 @@ interface NavState { showNav: boolean // legacy passes `undefined` to clear the transition (router/index.ts:270) pageTransition: string | undefined + /** + * The history index this app started at, or null before the first + * navigation. + * + * Answers "is there anywhere inside this app to go back to?", which + * `history.length` cannot: it also counts entries from before the app was + * opened, so it says yes for a link opened in a fresh tab and the back button + * would leave the app. Comparing the CURRENT history position against the one + * we started at is direction-proof — it stays correct however many times the + * person goes back and forward, where a plain counter would only ever grow. + */ + entryIndex: number | null } export const useNavStore = defineStore('nav', { @@ -32,8 +44,34 @@ export const useNavStore = defineStore('nav', { activeLink: 1, showNav: true, pageTransition: 'right', + entryIndex: null, }), + getters: { + /** + * True when `router.back()` lands somewhere inside this app rather than on + * whatever the person was looking at before they opened it. + */ + canGoBack: (state) => { + if (state.entryIndex === null) return false + const current = window.history.state?.position + // `position` is written by vue-router on every entry it creates. If it is + // missing (a hand-written history entry, an old browser), assume we must + // not leave the app and let the caller fall back. + return typeof current === 'number' ? current > state.entryIndex : false + }, + }, actions: { + /** + * Records where in history this app instance began. Called once, on the + * first navigation — every later entry sits above it, so anything above is + * ours to go back through. + */ + noteFirstNavigation() { + if (this.entryIndex !== null) return + const position = window.history.state?.position + this.entryIndex = typeof position === 'number' ? position : 0 + }, + setNavVisibility(data: boolean) { this.showNav = data }, diff --git a/apps/user-app-v2/src/stores/queryCache.ts b/apps/user-app-v2/src/stores/queryCache.ts index b7693a7..7643b90 100644 --- a/apps/user-app-v2/src/stores/queryCache.ts +++ b/apps/user-app-v2/src/stores/queryCache.ts @@ -9,8 +9,25 @@ // - `force` → always asks the server (pull-to-refresh, the // profile tab's balance, reaching the end of a list) // -// Entries never expire on a timer: data changes when the user asks for it or -// when a write here makes it wrong, and those writes call `invalidate`. +// Entries never expire on a timer, deliberately (owner ruling 2026-10-12: «هربار +// با رفتن به اون صفحه اون مقادیر مورد نیاز رو درخواست بزنه … هم سربار برای سرور +// نداره، هم درخواست الکی نمیخوره، هم دیتاها به‌روز میمونه»). A TTL guesses how +// long data stays true; entering a screen is the moment it actually matters. +// +// So the policy is: +// +// - a screen that shows server-owned data re-reads it with `force` when it is +// opened (`onMounted`, plus `onActivated` for the kept-alive views), while +// `peek` paints the previous copy instantly so the refresh is invisible; +// - the cached copy exists for that first paint and to collapse concurrent +// callers into one request, NOT to avoid asking again; +// - the catalogue is the exception: it is the whole product list in one +// response, changes when an admin edits it, and is already invalidated by +// pull-to-refresh and by placing an order. +// +// `force` is what makes this work for anything changed OUTSIDE the app — an +// order's status, a ledger entry, the gold price. Nothing here can know those +// changed, so not asking means never seeing them. // // Entries live in a plain Map, not reactive state: the catalogue is large and // making it deeply reactive would cost more than the request it saves. diff --git a/apps/user-app-v2/src/views/HomeView.vue b/apps/user-app-v2/src/views/HomeView.vue index 73a58c2..75d8f5f 100644 --- a/apps/user-app-v2/src/views/HomeView.vue +++ b/apps/user-app-v2/src/views/HomeView.vue @@ -80,7 +80,7 @@