Introduce origin migration handling with outdated route guards, redirect logic, and migration-related error views.
- Add `originMigration.js` for handling allowed origins, standalone contexts, and redirect routes. - Create `OutdatedGateway.vue` and `OutdatedInstallation.vue` error views with timer-based redirection and user instructions. - Update router to include `/outdated-installation` and `/outdated-gateway` paths. - Implement unit tests for migration utility methods and route guards. - Remove debug `console.log` statements across multiple components.
This commit is contained in:
@@ -52,7 +52,6 @@ const loadPendingBookings = () => {
|
||||
}
|
||||
).then(response => {
|
||||
pendingBookings.value = response.data.data;
|
||||
console.log("Pending bookings:", pendingBookings.value);
|
||||
}).catch(error => {
|
||||
console.error("Error:", error);
|
||||
});
|
||||
@@ -742,4 +741,4 @@ const isDropdownContentLoaded = (scan) => {
|
||||
.input.has-placeholder-italic::placeholder {
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -121,12 +121,10 @@ const handleLPRResult = () => {
|
||||
const parseImage = async (image: string) => {
|
||||
// Check if the time since the last successful parse is enough
|
||||
if (!camera.hasDelayAfterSuccessPassed()) {
|
||||
console.log("Skipping parsing due to delay after success.");
|
||||
return;
|
||||
}
|
||||
if (lastParsedImage.value === image) {
|
||||
// If the image is the same as the last parsed one, skip parsing
|
||||
console.log("Skipping parsing for the same image.");
|
||||
return;
|
||||
}
|
||||
lastParsedImage.value = image; // Update the last parsed image
|
||||
|
||||
+1
-2
@@ -11,7 +11,6 @@ const { coords, locatedAt, error, resume, pause } = useGeolocation({
|
||||
timeout: 27000,
|
||||
});
|
||||
const onUpdate = (newCoords: { latitude: number | null; longitude: number | null }) => {
|
||||
console.log("Geolocation updated:", newCoords);
|
||||
if (newCoords.latitude && newCoords.longitude) {
|
||||
locations.set({
|
||||
coords: {
|
||||
@@ -48,4 +47,4 @@ watch(coords, (newCoords) => {
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
+3
-1
@@ -1,7 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import {defineProps, onMounted, computed, ref} from "vue";
|
||||
import { reset_all_values, customer_name, nextStep, searchAndSelectCustomer, isCustomerSelected, order_id, customer_id, step, reg_1, reg_2, reg_3, reference, order_notes, loadCustomerAttributes, hasAttribute, uploadAttachment, isCreatingOrder } from "@/components/shop/POSDepartmentProcess.vue";
|
||||
import { reset_all_values, customer_name, nextStep, searchAndSelectCustomer, isCustomerSelected, order_id, customer_id, step, reg_1, reg_2, reg_3, reference, order_notes, loadCustomerAttributes, hasAttribute, uploadAttachment } from "@/components/shop/POSDepartmentProcess.vue";
|
||||
import * as POSDepartmentProcess from "@/components/shop/POSDepartmentProcess.vue";
|
||||
import { errors } from "@/components/request/HandleGlobalError.vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import GenericButton from "@/components/viewport/page/templates/generic/graphics/GenericButton.vue";
|
||||
@@ -13,6 +14,7 @@ import {views} from "@/components/displays/department/pos/steps/mobile/objects/P
|
||||
|
||||
const { t } = useI18n();
|
||||
const selectCustomerString = `${SessionUser.objects.global.language.select} ${SessionUser.objects.global.language.customer.toLowerCase()}`;
|
||||
const isCreatingOrder = computed(() => POSDepartmentProcess.isCreatingOrder?.value ?? false);
|
||||
|
||||
const props = defineProps({
|
||||
// If true, the button will have a white background. Default is true.
|
||||
|
||||
+1
-4
@@ -213,12 +213,10 @@ const getPopup = () => {
|
||||
}
|
||||
// Function to set the popup
|
||||
const setPopup = (newPopup: PosPopup | null) => {
|
||||
console.log('Setting popup:', newPopup);
|
||||
popup.value = newPopup;
|
||||
}
|
||||
// Function to clear the popup
|
||||
const clearPopup = () => {
|
||||
console.log('Clearing popup');
|
||||
popup.value = null;
|
||||
}
|
||||
// Default list of popups
|
||||
@@ -231,7 +229,6 @@ const addPopup = (popup: PosPopup) => {
|
||||
return;
|
||||
}
|
||||
popupsList.value[popup.id] = {value: popup};
|
||||
console.log(`Added popup with id: ${popup.id}`);
|
||||
}
|
||||
// Function to add default popups to the list
|
||||
const addDefaultPopups = () => {
|
||||
@@ -2068,4 +2065,4 @@ export {
|
||||
getCameraImageCaptureDelay as getCameraCaptureDelay,
|
||||
};
|
||||
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -26,7 +26,6 @@ const loadPendingBookings = () => {
|
||||
}
|
||||
).then(response => {
|
||||
pendingBookings.value = response.data.data;
|
||||
console.log("Pending bookings:", pendingBookings.value);
|
||||
}).catch(error => {
|
||||
console.error("Error:", error);
|
||||
});
|
||||
@@ -558,4 +557,4 @@ const getCurrentIconColor = () => {
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -16,7 +16,6 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const slots = useSlots();
|
||||
console.log(slots);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -47,4 +46,4 @@ console.log(slots);
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -12,7 +12,6 @@ export const getDepartments = async () => {
|
||||
const request = await authenticatedRequest('/departments','get');
|
||||
departments.value = request.data.data;
|
||||
isLoading.value = false;
|
||||
console.log(departments.value);
|
||||
};
|
||||
|
||||
// Check if a department exists by ID
|
||||
@@ -53,7 +52,6 @@ export const getDepartmentsGuest = async (queryParams = {}) => {
|
||||
const request = await unauthenticatedRequest('/guest/departments' + (queryString ? `?${queryString}` : ''),'get');
|
||||
departments.value = request.data.data;
|
||||
isLoading.value = false;
|
||||
console.log(departments.value);
|
||||
return departments.value;
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -952,7 +952,6 @@ export const loadPendingBookings = () => {
|
||||
}
|
||||
).then(response => {
|
||||
pendingBookings.value = response.data.data;
|
||||
console.log("Pending bookings:", pendingBookings.value);
|
||||
}).catch(error => {
|
||||
console.error("Error:", error);
|
||||
});
|
||||
|
||||
@@ -13,8 +13,6 @@ import { isHidden } from "@/components/viewport/page/headers/ViewportHeaderSetti
|
||||
import ConnectivityIssue from "@/views/errors/ConnectivityIssue.vue";
|
||||
const router = useRouter();
|
||||
|
||||
console.log("Viewport component loaded");
|
||||
|
||||
const headerHeight = ref(60); // Default header height in pixels
|
||||
const headerElement = ref<HTMLElement | null>(null);
|
||||
const footerHeight = ref(0); // Default footer height in pixels
|
||||
@@ -23,7 +21,6 @@ const footerElement = ref<HTMLElement | null>(null);
|
||||
const updateHeaderHeight = () => {
|
||||
if (headerElement.value) {
|
||||
headerHeight.value = headerElement.value.offsetHeight;
|
||||
console.log("Header height updated:", headerHeight.value);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -162,4 +159,4 @@ const isFooterInContent = computed(() => {
|
||||
.transform-color-black {
|
||||
filter: invert(100%) grayscale(100%) brightness(0) contrast(100%);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -128,7 +128,6 @@ onMounted(() => {
|
||||
// Emit a picture every 10 seconds
|
||||
if (!isCameraMounted.value) {
|
||||
isCameraMounted.value = true;
|
||||
console.log("Camera mounted")
|
||||
setInterval(() => {
|
||||
if (isCameraActive.value) {
|
||||
getFrame();
|
||||
@@ -140,7 +139,6 @@ onMounted(() => {
|
||||
|
||||
onUnmounted(() => {
|
||||
stopCamera();
|
||||
console.log('Camera unmounted');
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
|
||||
@@ -3929,6 +3929,29 @@
|
||||
"retry": "Prøv igen",
|
||||
"footer": "Hvis problemet fortsætter, kontakt venligst support."
|
||||
},
|
||||
"outdated": {
|
||||
"installation": {
|
||||
"title": "Forældet installation",
|
||||
"subtitle": "Denne installerede app er forældet og skal migreres.",
|
||||
"description": "Truck Wash er flyttet til en ny installation på truckwash.io. Åbn den nye side og installer appen igen på din enhed.",
|
||||
"android_title": "Android-migrering",
|
||||
"android_step_1": "Åbn truckwash.io i Chrome.",
|
||||
"android_step_2": "Åbn browsermenuen og vælg \"Installer app\".",
|
||||
"android_step_3": "Brug den nye installerede app og fjern denne forældede app.",
|
||||
"ios_title": "iPhone-migrering",
|
||||
"ios_step_1": "Åbn truckwash.io i Safari.",
|
||||
"ios_step_2": "Tryk på Del, og vælg \"Føj til hjemmeskærm\".",
|
||||
"ios_step_3": "Brug den nye hjemmeskærmsapp og fjern denne forældede app.",
|
||||
"open_now": "Åbn truckwash.io nu"
|
||||
},
|
||||
"gateway": {
|
||||
"title": "Forældet gateway",
|
||||
"subtitle": "Dette link er ved at blive udfaset.",
|
||||
"description": "Du bliver omdirigeret til den samme sti på truckwash.io.",
|
||||
"redirecting_in": "Omdirigerer om {seconds} sekunder...",
|
||||
"redirect_now": "Gå nu"
|
||||
}
|
||||
},
|
||||
"global_search": {
|
||||
"title": "Avanceret systemsøgning",
|
||||
"launcher": {
|
||||
|
||||
@@ -4034,6 +4034,29 @@
|
||||
"retry": "Wiederholen",
|
||||
"footer": "Wenn das Problem weiterhin besteht, kontaktieren Sie bitte den Support."
|
||||
},
|
||||
"outdated": {
|
||||
"installation": {
|
||||
"title": "Veraltete Installation",
|
||||
"subtitle": "Diese installierte App ist veraltet und muss migriert werden.",
|
||||
"description": "Truck Wash wurde auf eine neue Installation unter truckwash.io umgestellt. Ãffnen Sie die neue Seite und installieren Sie die App erneut auf Ihrem Gerät.",
|
||||
"android_title": "Android-Migration",
|
||||
"android_step_1": "Ãffnen Sie truckwash.io in Chrome.",
|
||||
"android_step_2": "Ãffnen Sie das Browsermenü und wählen Sie \"App installieren\".",
|
||||
"android_step_3": "Verwenden Sie die neu installierte App und entfernen Sie diese veraltete App.",
|
||||
"ios_title": "iPhone-Migration",
|
||||
"ios_step_1": "Ãffnen Sie truckwash.io in Safari.",
|
||||
"ios_step_2": "Tippen Sie auf \"Teilen\" und dann auf \"Zum Home-Bildschirm\".",
|
||||
"ios_step_3": "Verwenden Sie die neue Home-Bildschirm-App und entfernen Sie diese veraltete App.",
|
||||
"open_now": "truckwash.io jetzt öffnen"
|
||||
},
|
||||
"gateway": {
|
||||
"title": "Veraltetes Gateway",
|
||||
"subtitle": "Dieser Link wird schrittweise eingestellt.",
|
||||
"description": "Sie werden auf denselben Pfad bei truckwash.io weitergeleitet.",
|
||||
"redirecting_in": "Weiterleitung in {seconds} Sekunden...",
|
||||
"redirect_now": "Jetzt weiter"
|
||||
}
|
||||
},
|
||||
"passkeys": {
|
||||
"title": "Passkeys",
|
||||
"description": "Verwenden Sie Passkeys für eine sichere Anmeldung mit Biometrie wie Face ID, Touch ID oder der PIN Ihres Geräts.",
|
||||
|
||||
@@ -4250,5 +4250,28 @@
|
||||
"retry_now": "Trying to reconnect...",
|
||||
"retry": "Retry",
|
||||
"footer": "If the issue persists, please contact support."
|
||||
},
|
||||
"outdated": {
|
||||
"installation": {
|
||||
"title": "Outdated installation",
|
||||
"subtitle": "This installed app is outdated and must be migrated.",
|
||||
"description": "Truck Wash has moved to a new installation at truckwash.io. Please open the new site and install it again on your device.",
|
||||
"android_title": "Android migration",
|
||||
"android_step_1": "Open truckwash.io in Chrome.",
|
||||
"android_step_2": "Open the browser menu and choose \"Install app\".",
|
||||
"android_step_3": "Use the newly installed app and remove this outdated one.",
|
||||
"ios_title": "iPhone migration",
|
||||
"ios_step_1": "Open truckwash.io in Safari.",
|
||||
"ios_step_2": "Tap Share and choose \"Add to Home Screen\".",
|
||||
"ios_step_3": "Use the new home-screen app and remove this outdated one.",
|
||||
"open_now": "Open truckwash.io now"
|
||||
},
|
||||
"gateway": {
|
||||
"title": "Outdated gateway",
|
||||
"subtitle": "This link is being phased out.",
|
||||
"description": "You will be redirected to the same path on truckwash.io.",
|
||||
"redirecting_in": "Redirecting in {seconds} seconds...",
|
||||
"redirect_now": "Go now"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4014,6 +4014,29 @@
|
||||
"retry": "Prøv på nytt",
|
||||
"footer": "Hvis problemet vedvarer, kontakt support."
|
||||
},
|
||||
"outdated": {
|
||||
"installation": {
|
||||
"title": "Utdatert installasjon",
|
||||
"subtitle": "Denne installerte appen er utdatert og må migreres.",
|
||||
"description": "Truck Wash er flyttet til en ny installasjon pÃ¥ truckwash.io. Ã
pne den nye siden og installer appen på nytt på enheten din.",
|
||||
"android_title": "Android-migrering",
|
||||
"android_step_1": "Ã
pne truckwash.io i Chrome.",
|
||||
"android_step_2": "Ã
pne nettlesermenyen og velg \"Installer app\".",
|
||||
"android_step_3": "Bruk den nye installerte appen og fjern denne utdaterte appen.",
|
||||
"ios_title": "iPhone-migrering",
|
||||
"ios_step_1": "Ã
pne truckwash.io i Safari.",
|
||||
"ios_step_2": "Trykk Del og velg \"Legg til på hjemskjermen\".",
|
||||
"ios_step_3": "Bruk den nye hjemskjerm-appen og fjern denne utdaterte appen.",
|
||||
"open_now": "Ã
pne truckwash.io nå"
|
||||
},
|
||||
"gateway": {
|
||||
"title": "Utdatert gateway",
|
||||
"subtitle": "Denne lenken fases ut.",
|
||||
"description": "Du blir omdirigert til samme sti på truckwash.io.",
|
||||
"redirecting_in": "Omdirigerer om {seconds} sekunder...",
|
||||
"redirect_now": "Gå nå"
|
||||
}
|
||||
},
|
||||
"2fa": {
|
||||
"title": "Tofaktorautentisering",
|
||||
"description": "Legg til et ekstra sikkerhetslag på kontoen din ved å kreve en verifiseringskode ved innlogging.",
|
||||
|
||||
@@ -4014,6 +4014,29 @@
|
||||
"retry": "Försök igen",
|
||||
"footer": "Om problemet kvarstår, kontakta supporten."
|
||||
},
|
||||
"outdated": {
|
||||
"installation": {
|
||||
"title": "Föråldrad installation",
|
||||
"subtitle": "Den här installerade appen är föråldrad och måste migreras.",
|
||||
"description": "Truck Wash har flyttats till en ny installation på truckwash.io. Öppna den nya sidan och installera appen igen på din enhet.",
|
||||
"android_title": "Android-migrering",
|
||||
"android_step_1": "Öppna truckwash.io i Chrome.",
|
||||
"android_step_2": "Öppna webbläsarmenyn och välj \"Installera app\".",
|
||||
"android_step_3": "Använd den nyinstallerade appen och ta bort den här föråldrade appen.",
|
||||
"ios_title": "iPhone-migrering",
|
||||
"ios_step_1": "Öppna truckwash.io i Safari.",
|
||||
"ios_step_2": "Tryck på Dela och välj \"Lägg till på hemskärmen\".",
|
||||
"ios_step_3": "Använd den nya hemskärmsappen och ta bort den här föråldrade appen.",
|
||||
"open_now": "Öppna truckwash.io nu"
|
||||
},
|
||||
"gateway": {
|
||||
"title": "Föråldrad gateway",
|
||||
"subtitle": "Den här länken håller på att fasas ut.",
|
||||
"description": "Du omdirigeras till samma sökväg på truckwash.io.",
|
||||
"redirecting_in": "Omdirigerar om {seconds} sekunder...",
|
||||
"redirect_now": "Gå nu"
|
||||
}
|
||||
},
|
||||
"2fa": {
|
||||
"title": "Tvåfaktorsautentisering",
|
||||
"description": "Lägg till ett extra säkerhetslager på ditt konto genom att kräva en verifieringskod vid inloggning.",
|
||||
|
||||
+59
@@ -18,6 +18,65 @@ import { initializeAutoTableExports } from '@/services/AutoTableExportService.js
|
||||
import { API_URL, IS_DEV, POS_STEP_1_VERSION } from './config';
|
||||
// export { API_URL, IS_DEV, POS_STEP_1_VERSION };
|
||||
|
||||
const VITE_BUILD_DATE = import.meta.env.VITE_BUILD_DATE || '';
|
||||
const VITE_COMMIT_HASH = import.meta.env.VITE_COMMIT_HASH || '';
|
||||
const LAST_VERSION_CHECK_STORAGE_KEY = 'lastVersionCheck';
|
||||
|
||||
const TRUCK_WASH_ASCII = [
|
||||
' _____ _ _ _ _ \n' +
|
||||
'|_ _| | | | | | | | | \n' +
|
||||
' | |_ __ _ _ ___| | __ | | | | __ _ ___| |__ \n' +
|
||||
' | | \'__| | | |/ __| |/ / | |/\\| |/ _` / __| \'_ \\ \n' +
|
||||
' | | | | |_| | (__| < \\ /\\ / (_| \\__ \\ | | |\n' +
|
||||
' \\_/_| \\__,_|\\___|_|\\_\\ \\/ \\/ \\__,_|___/_| |_|'
|
||||
].join('\n');
|
||||
|
||||
const pad = (value) => String(value).padStart(2, '0');
|
||||
|
||||
const formatDateTime = (value) => {
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return 'Unknown';
|
||||
}
|
||||
const day = pad(date.getDate());
|
||||
const month = pad(date.getMonth() + 1);
|
||||
const year = date.getFullYear();
|
||||
const hours = pad(date.getHours());
|
||||
const minutes = pad(date.getMinutes());
|
||||
return `${day}-${month}-${year} ${hours}:${minutes}`;
|
||||
};
|
||||
|
||||
const formatTimeAgo = (timestamp) => {
|
||||
const elapsed = Date.now() - timestamp;
|
||||
if (elapsed < 60_000) return 'just now';
|
||||
if (elapsed < 3_600_000) return `${Math.floor(elapsed / 60_000)}m ago`;
|
||||
if (elapsed < 86_400_000) return `${Math.floor(elapsed / 3_600_000)}h ago`;
|
||||
return `${Math.floor(elapsed / 86_400_000)}d ago`;
|
||||
};
|
||||
|
||||
const formatCommit = (hash) => {
|
||||
if (!hash) return '#unknown';
|
||||
return `#${hash.substring(0, 7)}`;
|
||||
};
|
||||
|
||||
const logBuildBanner = () => {
|
||||
const lastVersionCheckRaw = Number.parseInt(localStorage.getItem(LAST_VERSION_CHECK_STORAGE_KEY) || '', 10);
|
||||
const hasLastVersionCheck = Number.isFinite(lastVersionCheckRaw) && lastVersionCheckRaw > 0;
|
||||
const lastVersionCheckDisplay = hasLastVersionCheck
|
||||
? `${formatTimeAgo(lastVersionCheckRaw)} @ ${formatDateTime(lastVersionCheckRaw)}`
|
||||
: 'never @ Unknown';
|
||||
|
||||
console.log([
|
||||
TRUCK_WASH_ASCII,
|
||||
'',
|
||||
`Build: ${formatCommit(VITE_COMMIT_HASH)} @ ${formatDateTime(VITE_BUILD_DATE)} (${IS_DEV ? 'development' : 'production'})`,
|
||||
`Last version check: ${lastVersionCheckDisplay}`,
|
||||
`Remote API: ${API_URL}`,
|
||||
].join('\n'));
|
||||
};
|
||||
|
||||
logBuildBanner();
|
||||
|
||||
// Service worker update handling
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||||
|
||||
+19
-1
@@ -1,3 +1,9 @@
|
||||
import { ALLOWED_ORIGINS } from '@/config';
|
||||
import {
|
||||
isStandaloneContext,
|
||||
resolveOriginMigrationRoute,
|
||||
} from '@/middleware/originMigration';
|
||||
|
||||
function middlewarePipeline(context, middleware, index) {
|
||||
const nextMiddleware = middleware[index];
|
||||
|
||||
@@ -13,6 +19,18 @@ function middlewarePipeline(context, middleware, index) {
|
||||
|
||||
export default function applyMiddleware(router) {
|
||||
router.beforeEach((to, from, next) => {
|
||||
const browserWindow = typeof window !== 'undefined' ? window : undefined;
|
||||
const originMigrationRoute = resolveOriginMigrationRoute({
|
||||
to,
|
||||
currentOrigin: browserWindow?.location?.origin || '',
|
||||
allowedOrigins: ALLOWED_ORIGINS,
|
||||
standalone: isStandaloneContext(browserWindow),
|
||||
});
|
||||
|
||||
if (originMigrationRoute) {
|
||||
return next(originMigrationRoute);
|
||||
}
|
||||
|
||||
if (!to.meta.middleware) {
|
||||
return next();
|
||||
}
|
||||
@@ -30,4 +48,4 @@ export default function applyMiddleware(router) {
|
||||
|
||||
return middleware[0]({ ...context, next: nextMiddleware });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
export const OUTDATED_INSTALLATION_ROUTE_NAME = 'outdated-installation';
|
||||
export const OUTDATED_GATEWAY_ROUTE_NAME = 'outdated-gateway';
|
||||
|
||||
const EXEMPT_ROUTE_NAMES = new Set([
|
||||
OUTDATED_INSTALLATION_ROUTE_NAME,
|
||||
OUTDATED_GATEWAY_ROUTE_NAME,
|
||||
]);
|
||||
|
||||
const normalizeAllowedOrigins = (allowedOrigins = []) => {
|
||||
return new Set(
|
||||
allowedOrigins
|
||||
.map((origin) => {
|
||||
try {
|
||||
return new URL(origin).origin;
|
||||
} catch (_error) {
|
||||
return origin;
|
||||
}
|
||||
})
|
||||
.filter(Boolean),
|
||||
);
|
||||
};
|
||||
|
||||
export const isOriginAllowed = (currentOrigin, allowedOrigins = []) => {
|
||||
if (!currentOrigin) return true;
|
||||
const allowedOriginSet = normalizeAllowedOrigins(allowedOrigins);
|
||||
return allowedOriginSet.has(currentOrigin);
|
||||
};
|
||||
|
||||
export const isStandaloneContext = (win) => {
|
||||
const resolvedWindow = win || (typeof window !== 'undefined' ? window : undefined);
|
||||
if (!resolvedWindow) return false;
|
||||
const iosStandalone = resolvedWindow.navigator?.standalone === true;
|
||||
const mediaStandalone = typeof resolvedWindow.matchMedia === 'function'
|
||||
&& resolvedWindow.matchMedia('(display-mode: standalone)').matches;
|
||||
return iosStandalone || mediaStandalone;
|
||||
};
|
||||
|
||||
export const shouldBypassOriginGate = (route) => {
|
||||
return EXEMPT_ROUTE_NAMES.has(route?.name);
|
||||
};
|
||||
|
||||
export const resolveOriginMigrationRoute = ({
|
||||
to,
|
||||
currentOrigin,
|
||||
allowedOrigins,
|
||||
standalone,
|
||||
}) => {
|
||||
if (shouldBypassOriginGate(to)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isOriginAllowed(currentOrigin, allowedOrigins)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (standalone) {
|
||||
return { name: OUTDATED_INSTALLATION_ROUTE_NAME };
|
||||
}
|
||||
|
||||
return {
|
||||
name: OUTDATED_GATEWAY_ROUTE_NAME,
|
||||
query: {
|
||||
redirect: to?.fullPath || '/',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const sanitizeRedirectPath = (redirectPath) => {
|
||||
if (typeof redirectPath !== 'string') return '/';
|
||||
if (!redirectPath.startsWith('/')) return '/';
|
||||
return redirectPath;
|
||||
};
|
||||
|
||||
export const buildMigrationUrl = (migrationOrigin, redirectPath) => {
|
||||
const safePath = sanitizeRedirectPath(redirectPath);
|
||||
try {
|
||||
return new URL(safePath, migrationOrigin).toString();
|
||||
} catch (_error) {
|
||||
return migrationOrigin;
|
||||
}
|
||||
};
|
||||
|
||||
export const startRedirectCountdown = ({
|
||||
seconds = 5,
|
||||
onTick,
|
||||
onRedirect,
|
||||
setIntervalFn = setInterval,
|
||||
clearIntervalFn = clearInterval,
|
||||
setTimeoutFn = setTimeout,
|
||||
clearTimeoutFn = clearTimeout,
|
||||
}) => {
|
||||
let remainingSeconds = seconds;
|
||||
const intervalId = setIntervalFn(() => {
|
||||
remainingSeconds = Math.max(remainingSeconds - 1, 0);
|
||||
if (typeof onTick === 'function') {
|
||||
onTick(remainingSeconds);
|
||||
}
|
||||
if (remainingSeconds === 0) {
|
||||
clearIntervalFn(intervalId);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
const timeoutId = setTimeoutFn(() => {
|
||||
if (typeof onRedirect === 'function') {
|
||||
onRedirect();
|
||||
}
|
||||
}, seconds * 1000);
|
||||
|
||||
return () => {
|
||||
clearIntervalFn(intervalId);
|
||||
clearTimeoutFn(timeoutId);
|
||||
};
|
||||
};
|
||||
@@ -49,6 +49,8 @@ import MyMaterials from "@/views/dashboards/userDashboard/materials/MyMaterials.
|
||||
/** Errors */
|
||||
import NotFound from './views/errors/NotFound.vue';
|
||||
import ConnectivityIssue from './views/errors/ConnectivityIssue.vue';
|
||||
import OutdatedInstallation from "@/views/errors/OutdatedInstallation.vue";
|
||||
import OutdatedGateway from "@/views/errors/OutdatedGateway.vue";
|
||||
import MyOrders from "@/views/dashboards/userDashboard/orders/MyOrders.vue";
|
||||
import NewVehicle from "@/views/dashboards/userDashboard/vehicles/NewVehicle.vue";
|
||||
import MyBookings from "@/views/dashboards/userDashboard/bookings/MyBookings.vue";
|
||||
@@ -945,6 +947,18 @@ export const router = createRouter({
|
||||
path: '/guest/book/wash',
|
||||
component: GuestBookExteriorWash,
|
||||
},
|
||||
{
|
||||
name: 'outdated-installation',
|
||||
path: '/outdated-installation',
|
||||
component: OutdatedInstallation,
|
||||
meta: { template: 'clear-main', titleKey: 'outdated.installation.title' }
|
||||
},
|
||||
{
|
||||
name: 'outdated-gateway',
|
||||
path: '/outdated-gateway',
|
||||
component: OutdatedGateway,
|
||||
meta: { template: 'clear-main', titleKey: 'outdated.gateway.title' }
|
||||
},
|
||||
{
|
||||
name: 'connectivity-issue',
|
||||
path: '/connectivity-issue',
|
||||
|
||||
@@ -11,9 +11,8 @@ const check_connection = () => {
|
||||
reset_timer();
|
||||
try {
|
||||
SessionUser.request('/ping')
|
||||
.then((response: any) => {
|
||||
.then(() => {
|
||||
connectionStatus.value = 'ok';
|
||||
console.warn('Connection OK', response);
|
||||
})
|
||||
.catch((error: any) => {
|
||||
// Keep the user on the error page if the connection check fails
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
<script setup>
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { MIGRATION_ORIGIN } from '@/config';
|
||||
import {
|
||||
buildMigrationUrl,
|
||||
sanitizeRedirectPath,
|
||||
startRedirectCountdown,
|
||||
} from '@/middleware/originMigration';
|
||||
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
const secondsRemaining = ref(5);
|
||||
let stopRedirectCountdown = null;
|
||||
|
||||
const redirectPath = computed(() => {
|
||||
const redirectQuery = route.query?.redirect;
|
||||
const candidate = Array.isArray(redirectQuery) ? redirectQuery[0] : redirectQuery;
|
||||
return sanitizeRedirectPath(candidate);
|
||||
});
|
||||
|
||||
const destinationUrl = computed(() => {
|
||||
return buildMigrationUrl(MIGRATION_ORIGIN, redirectPath.value);
|
||||
});
|
||||
|
||||
const clearTimers = () => {
|
||||
if (typeof stopRedirectCountdown === 'function') {
|
||||
stopRedirectCountdown();
|
||||
stopRedirectCountdown = null;
|
||||
}
|
||||
};
|
||||
|
||||
const redirectNow = () => {
|
||||
window.location.replace(destinationUrl.value);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
stopRedirectCountdown = startRedirectCountdown({
|
||||
seconds: 5,
|
||||
onTick: (remaining) => {
|
||||
secondsRemaining.value = remaining;
|
||||
},
|
||||
onRedirect: redirectNow,
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearTimers();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="hero is-light is-fullheight">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-11-mobile is-9-tablet is-7-desktop">
|
||||
<div class="box migration-card">
|
||||
<h1 class="title">{{ t('outdated.gateway.title') }}</h1>
|
||||
<p class="subtitle">{{ t('outdated.gateway.subtitle') }}</p>
|
||||
|
||||
<p class="mb-3">{{ t('outdated.gateway.description') }}</p>
|
||||
<p class="mb-2">{{ t('outdated.gateway.redirecting_in', { seconds: secondsRemaining }) }}</p>
|
||||
<p class="is-size-7 has-text-grey mb-5">{{ destinationUrl }}</p>
|
||||
|
||||
<button class="button is-primary is-medium" @click="redirectNow">
|
||||
{{ t('outdated.gateway.redirect_now') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.migration-card {
|
||||
border-top: 5px solid #13324c;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { MIGRATION_ORIGIN } from '@/config';
|
||||
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="hero is-light is-fullheight">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-11-mobile is-9-tablet is-7-desktop">
|
||||
<div class="box migration-card">
|
||||
<h1 class="title">{{ t('outdated.installation.title') }}</h1>
|
||||
<p class="subtitle">{{ t('outdated.installation.subtitle') }}</p>
|
||||
|
||||
<p class="mb-4">{{ t('outdated.installation.description') }}</p>
|
||||
|
||||
<h2 class="is-size-5 has-text-weight-semibold mb-2">{{ t('outdated.installation.android_title') }}</h2>
|
||||
<ol class="mb-5">
|
||||
<li>{{ t('outdated.installation.android_step_1') }}</li>
|
||||
<li>{{ t('outdated.installation.android_step_2') }}</li>
|
||||
<li>{{ t('outdated.installation.android_step_3') }}</li>
|
||||
</ol>
|
||||
|
||||
<h2 class="is-size-5 has-text-weight-semibold mb-2">{{ t('outdated.installation.ios_title') }}</h2>
|
||||
<ol class="mb-5">
|
||||
<li>{{ t('outdated.installation.ios_step_1') }}</li>
|
||||
<li>{{ t('outdated.installation.ios_step_2') }}</li>
|
||||
<li>{{ t('outdated.installation.ios_step_3') }}</li>
|
||||
</ol>
|
||||
|
||||
<a class="button is-primary is-medium" :href="MIGRATION_ORIGIN">
|
||||
{{ t('outdated.installation.open_now') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.migration-card {
|
||||
border-top: 5px solid #13324c;
|
||||
}
|
||||
|
||||
ol {
|
||||
margin-left: 1.25rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,101 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
OUTDATED_GATEWAY_ROUTE_NAME,
|
||||
OUTDATED_INSTALLATION_ROUTE_NAME,
|
||||
buildMigrationUrl,
|
||||
resolveOriginMigrationRoute,
|
||||
sanitizeRedirectPath,
|
||||
startRedirectCountdown,
|
||||
} from "@/middleware/originMigration";
|
||||
|
||||
describe("origin migration route guard", () => {
|
||||
const allowedOrigins = ["https://truckwash.io", "https://www.truckwash.io"];
|
||||
const route = { name: "landing", fullPath: "/user/bookings?tab=open#latest" };
|
||||
|
||||
it("allows navigation on allowed origins", () => {
|
||||
const target = resolveOriginMigrationRoute({
|
||||
to: route,
|
||||
currentOrigin: "https://truckwash.io",
|
||||
allowedOrigins,
|
||||
standalone: false,
|
||||
});
|
||||
|
||||
expect(target).toBeNull();
|
||||
});
|
||||
|
||||
it("routes to outdated installation for disallowed standalone context", () => {
|
||||
const target = resolveOriginMigrationRoute({
|
||||
to: route,
|
||||
currentOrigin: "https://legacy.truckwash.io",
|
||||
allowedOrigins,
|
||||
standalone: true,
|
||||
});
|
||||
|
||||
expect(target).toEqual({ name: OUTDATED_INSTALLATION_ROUTE_NAME });
|
||||
});
|
||||
|
||||
it("routes to outdated gateway for disallowed browser context", () => {
|
||||
const target = resolveOriginMigrationRoute({
|
||||
to: route,
|
||||
currentOrigin: "https://legacy.truckwash.io",
|
||||
allowedOrigins,
|
||||
standalone: false,
|
||||
});
|
||||
|
||||
expect(target).toEqual({
|
||||
name: OUTDATED_GATEWAY_ROUTE_NAME,
|
||||
query: { redirect: "/user/bookings?tab=open#latest" },
|
||||
});
|
||||
});
|
||||
|
||||
it("bypasses the gate on dedicated outdated routes", () => {
|
||||
const target = resolveOriginMigrationRoute({
|
||||
to: { name: OUTDATED_GATEWAY_ROUTE_NAME, fullPath: "/outdated-gateway" },
|
||||
currentOrigin: "https://legacy.truckwash.io",
|
||||
allowedOrigins,
|
||||
standalone: false,
|
||||
});
|
||||
|
||||
expect(target).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("origin migration redirect url", () => {
|
||||
it("preserves path, query, and hash on migration origin", () => {
|
||||
const redirectUrl = buildMigrationUrl(
|
||||
"https://truckwash.io",
|
||||
"/user/bookings?tab=open#latest",
|
||||
);
|
||||
|
||||
expect(redirectUrl).toBe("https://truckwash.io/user/bookings?tab=open#latest");
|
||||
});
|
||||
|
||||
it("sanitizes invalid redirect paths to root", () => {
|
||||
expect(sanitizeRedirectPath("https://malicious.example")).toBe("/");
|
||||
});
|
||||
});
|
||||
|
||||
describe("gateway redirect countdown", () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("ticks every second and redirects after five seconds", () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const onRedirect = vi.fn();
|
||||
const tickValues = [];
|
||||
const stop = startRedirectCountdown({
|
||||
seconds: 5,
|
||||
onTick: (seconds) => tickValues.push(seconds),
|
||||
onRedirect,
|
||||
});
|
||||
|
||||
vi.advanceTimersByTime(5000);
|
||||
|
||||
expect(tickValues).toEqual([4, 3, 2, 1, 0]);
|
||||
expect(onRedirect).toHaveBeenCalledTimes(1);
|
||||
|
||||
stop();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user