diff --git a/android/app/build.gradle b/android/app/build.gradle index 2ec8475..49393e2 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.maison.mona" minSdkVersion 26 targetSdkVersion 35 - versionCode 801 - versionName "8.0.1" + versionCode 803 + versionName "8.0.3" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index f857615..a053e1b 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -348,12 +348,12 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 801; + CURRENT_PROJECT_VERSION = 803; DEVELOPMENT_TEAM = 843UJ9V2XK; INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 8.0.1; + MARKETING_VERSION = 8.0.3; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = com.mona.starter; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -369,12 +369,12 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 801; + CURRENT_PROJECT_VERSION = 803; DEVELOPMENT_TEAM = 843UJ9V2XK; INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 8.0.1; + MARKETING_VERSION = 8.0.3; PRODUCT_BUNDLE_IDENTIFIER = "ca.umontreal.mona-ios"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; diff --git a/src/components/DiscoveryDetails.vue b/src/components/DiscoveryDetails.vue index 518a2ab..806d23e 100644 --- a/src/components/DiscoveryDetails.vue +++ b/src/components/DiscoveryDetails.vue @@ -1,4 +1,5 @@ @@ -1041,4 +1057,15 @@ a { color: black; font-weight: normal; } + +.loading-placeholder { + display: flex; + align-items: center; + justify-content: center; + height: 60vh; +} +.loading-text { + font-size: 3.8vw; + color: #666; +} \ No newline at end of file diff --git a/src/components/LogoutContainer.vue b/src/components/LogoutContainer.vue index 33a4324..ead0226 100644 --- a/src/components/LogoutContainer.vue +++ b/src/components/LogoutContainer.vue @@ -57,8 +57,8 @@ export default { document.querySelector("ion-back-button").click(); }, - disconnectUser() { - UserData.resetPreferences(false); // `false` to keep hasSeenTutorial to true + async disconnectUser() { + await UserData.clearLocalDataAndReset(false); // wipe local DBs and cache, keep tutorial flag // Go to main page this.$router.replace("/register") diff --git a/src/internal/databases/Database.ts b/src/internal/databases/Database.ts index 9af5fb5..eea574a 100644 --- a/src/internal/databases/Database.ts +++ b/src/internal/databases/Database.ts @@ -29,12 +29,38 @@ export abstract class Database { if (typeof content.data === "string") { const parsed = JSON.parse(content.data); + + // Validate and construct elements; if any element is malformed, + // consider the local DB file corrupt and fetch from server instead. + const validated: Discovery[] = []; + for (const element of parsed) { - // for (const element of parsed.data) { - this.data.push(this.createSingleElement(element)); + try { + const candidate = this.createSingleElement(element); + + // Basic validation: must have numeric id and a callable getTitle + if ( + !candidate || + typeof candidate.id !== "number" || + typeof candidate.getTitle !== "function" + ) { + throw new Error("Invalid discovery object"); + } + + validated.push(candidate); + } catch (e) { + console.warn(`${this.type} db: detected corrupt element while parsing local file (${e}). Will rebuild from server.`); + // Delete the corrupted local file and populate from server + try { + await Filesystem.deleteFile({ path: this.path, directory: Directory.Data }); + } catch (_ignored) {} + + return await this.populateFromServer(); + } } - } + this.data = validated; + } console.log(`${this.type} db: successfully populated (locally).`); } catch (err) { @@ -126,4 +152,9 @@ export abstract class Database { return this.data.slice(a, b); } + + // Reset in-memory data for this database (useful when clearing local files) + public static resetData() { + this.data = []; + } } diff --git a/src/internal/databases/UserData.ts b/src/internal/databases/UserData.ts index a97c8ed..d4edfc8 100644 --- a/src/internal/databases/UserData.ts +++ b/src/internal/databases/UserData.ts @@ -145,6 +145,17 @@ export class UserData { this.updateFile(); } + public static async clearLocalDataAndReset(resetTutorial = true) { + // Remove local mirrored DB files and cache, then reset preferences + await this.deleteLocalDatabaseFiles(); + await this.invalidateCacheFile(); + // Clear any in-memory caches that could hold stale discovery objects + this.sortedDiscoveries = []; + this.sortedDiscoveriesDistance = []; + + this.resetPreferences(resetTutorial); + } + public static async ensureDataSchemaUpToDate() { await this.populate(); const storedVersion = this.data?.schemaVersion ?? 0; @@ -308,6 +319,22 @@ export class UserData { }).catch(() => undefined), ), ); + + // Also reset in-memory DBs so app doesn't keep using stale data after files are deleted + try { + const { ArtworkDatabase } = await import("@/internal/databases/ArtworkDatabase"); + const { PlaceDatabase } = await import("@/internal/databases/PlaceDatabase"); + const { HeritageDatabase } = await import("@/internal/databases/HeritageDatabase"); + const { BadgeDatabase } = await import("@/internal/databases/BadgeDatabase"); + + ArtworkDatabase.resetData(); + PlaceDatabase.resetData(); + HeritageDatabase.resetData(); + BadgeDatabase.resetData(); + } catch (err) { + // If dynamic import fails for any reason, ignore — best-effort reset + console.warn("Failed to reset in-memory DBs:", err); + } } private static parseCachePayload(parsed: any): { version: number; data: any[] } | null {