From c59df1870d9d72e5681f7bf747ddbb6e9689d9f8 Mon Sep 17 00:00:00 2001 From: Nazar Shcherbiak Date: Tue, 16 Dec 2025 19:15:43 +0200 Subject: [PATCH 1/8] Implement hardResetDatabase on iOS --- ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.m | 2 + .../Offline/RNMBXOfflineModuleLegacy.swift | 147 +++++++++++++++++- ios/RNMBX/RNMBXMapView.swift | 48 +++++- src/modules/offline/offlineManagerLegacy.ts | 15 ++ 4 files changed, 203 insertions(+), 9 deletions(-) diff --git a/ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.m b/ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.m index 36672088fd..544f0652c5 100644 --- a/ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.m +++ b/ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.m @@ -30,6 +30,8 @@ @interface RCT_EXTERN_MODULE(RNMBXOfflineModuleLegacy, RCTEventEmitter OfflineRe reject("migrateOfflineCache error:", error.localizedDescription, error) } } + + func reinitOfflineRegionManager( + _ resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let fileManager = FileManager.default + let appSupport = fileManager.urls(for: .applicationSupportDirectory, + in: .userDomainMask).first! + + // Generate random suffix, e.g. "A1B2" + let randomSuffix = UUID().uuidString.prefix(4) + let folderName = ".mapbox_custom/\(randomSuffix)/map_data" + let targetDir = appSupport.appendingPathComponent(folderName, isDirectory: true) + + // Ensure directory exists + do { + try fileManager.createDirectory( + at: targetDir, + withIntermediateDirectories: true, + attributes: nil + ) + } catch { + NSLog("RNMBXOfflineModuleLegacy: failed to create custom map_data dir: \(error)") + reject("dir_error", "Failed to create custom map_data directory", error) + return + } + + DispatchQueue.main.async { + #if RNMBX_11 + self.offlineRegionManager = OfflineRegionManager( + resourceOptions: .init( + accessToken: RNMBXModule.accessToken!, + dataPathURL: targetDir + ) + ) + #else + self.offlineRegionManager = OfflineRegionManager( + resourceOptions: .init( + accessToken: RNMBXModule.accessToken!, + dataPathURL: targetDir + ) + ) + #endif + + NSLog("RNMBXOfflineModuleLegacy: offlineRegionManager reinitialized with \(targetDir.path)") + resolve(nil) + } + } + + @objc + func hardResetDatabase( + _ resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let fileManager = FileManager.default + let appSupport = fileManager.urls( + for: .applicationSupportDirectory, + in: .userDomainMask + ).first! + + let defaultMapDataDir = appSupport.appendingPathComponent( + ".mapbox/map_data", + isDirectory: true + ) + let customMapDataDir = appSupport.appendingPathComponent( + ".mapbox_custom", + isDirectory: true + ) + + DispatchQueue.global(qos: .userInitiated).async { + do { + if fileManager.fileExists(atPath: defaultMapDataDir.path) { + try fileManager.removeItem(at: defaultMapDataDir) + } + + if fileManager.fileExists(atPath: customMapDataDir.path) { + try fileManager.removeItem(at: customMapDataDir) + } + } catch { + NSLog("RNMBXOfflineModuleLegacy: hardResetDatabase failed: \(error)") + DispatchQueue.main.async { + reject("hard_reset_error", "Failed to remove Mapbox offline directories", error) + } + return + } + + DispatchQueue.main.async { + self.reinitOfflineRegionManager(resolve, reject: reject) + } + } + } } diff --git a/ios/RNMBX/RNMBXMapView.swift b/ios/RNMBX/RNMBXMapView.swift index 20d9dc8bae..bb83dfaf78 100644 --- a/ios/RNMBX/RNMBXMapView.swift +++ b/ios/RNMBX/RNMBXMapView.swift @@ -254,14 +254,54 @@ open class RNMBXMapView: UIView, RCTInvalidating { if let mapViewImpl = mapViewImpl, let mapViewInstance = createAndAddMapViewImpl(mapViewImpl, self) { _mapView = mapViewInstance } else { - #if RNMBX_11 - _mapView = MapView(frame: self.bounds, mapInitOptions: MapInitOptions()) - #else + + // This logic should be aligned with offlineRegionManager initialization @see ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.swift + let fileManager = FileManager.default + let appSupport = fileManager.urls( + for: .applicationSupportDirectory, + in: .userDomainMask + ).first! + + // Default: Library/Application Support/.mapbox/map_data + let defaultDataPath = appSupport.appendingPathComponent(".mapbox/map_data") + + // Preferred: Library/Application Support/.mapbox_custom/XXXX/map_data/map_data.db + var customDataPath: URL? = nil + let customRoot = appSupport.appendingPathComponent(".mapbox_custom", isDirectory: true) + + if fileManager.fileExists(atPath: customRoot.path), + let entries = try? fileManager.contentsOfDirectory( + at: customRoot, + includingPropertiesForKeys: [.isDirectoryKey], + options: [] + ) { + for entry in entries { + // Only consider subdirectories under .mapbox_custom + if let isDir = try? entry.resourceValues(forKeys: [.isDirectoryKey]).isDirectory, + isDir == true { + let candidateMapData = entry.appendingPathComponent("map_data", isDirectory: true) + let candidateDb = candidateMapData.appendingPathComponent("map_data.db") + + if fileManager.fileExists(atPath: candidateDb.path) { + customDataPath = candidateMapData + break + } + } + } + } + + let dataPathURL = customDataPath ?? defaultDataPath + let accessToken = RNMBXModule.accessToken if accessToken == nil { Logger.log(level: .error, message: "No accessToken set, please call Mapbox.setAccessToken(...)") } - let resourceOptions = ResourceOptions(accessToken: accessToken ?? "") + let resourceOptions = ResourceOptions(accessToken: accessToken ?? "", dataPathURL: dataPathURL) + + #if RNMBX_11 + _mapView = MapView(frame: self.bounds, mapInitOptions: MapInitOptions(resourceOptions: resourceOptions)) + #else + _mapView = MapView(frame: frame, mapInitOptions: MapInitOptions(resourceOptions: resourceOptions)) #endif _mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight] diff --git a/src/modules/offline/offlineManagerLegacy.ts b/src/modules/offline/offlineManagerLegacy.ts index 523071f673..58845994e2 100644 --- a/src/modules/offline/offlineManagerLegacy.ts +++ b/src/modules/offline/offlineManagerLegacy.ts @@ -116,6 +116,21 @@ class OfflineManagerLegacy { await MapboxOfflineManager.migrateOfflineCache(); } + /** + * Fully resets offline region database file by deleting both `.mapbox/map_data` and `.mapbox_custom` directories, + * then reinitializes the OfflineRegionManager to point to a new data directory (iOS only). + * + * On iOS, this is required because the Mapbox SDK does not automatically recreate the `map_data` database after deletion. + * After calling this, all subsequent offline region operations will use a fresh, isolated storage location. + * + * On Android, only `.mapbox/map_data` is removed; explicit reinitialization is not needed as the Mapbox C++ manager recreates the database automatically. + */ + async hardResetDatabase() { + await MapboxOfflineManager.hardResetDatabase(); + this._offlinePacks = {}; + await this._initialize(true); + } + /** * Deletes the existing database, which includes both the ambient cache and offline packs, then reinitializes it. * From ce219cc55a3acbe9cd5466cf837421a9ad864888 Mon Sep 17 00:00:00 2001 From: Nazar Shcherbiak Date: Tue, 16 Dec 2025 22:04:41 +0200 Subject: [PATCH 2/8] Provide hardResetDatabase on Android --- .../rnmbx/modules/RNMBXOfflineModuleLegacy.kt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/android/src/main/java/com/rnmapbox/rnmbx/modules/RNMBXOfflineModuleLegacy.kt b/android/src/main/java/com/rnmapbox/rnmbx/modules/RNMBXOfflineModuleLegacy.kt index 764b21f8a5..37e65885c0 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/modules/RNMBXOfflineModuleLegacy.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/modules/RNMBXOfflineModuleLegacy.kt @@ -451,4 +451,23 @@ class RNMBXOfflineModuleLegacy(private val mReactContext: ReactApplicationContex promise.reject(mes) } } + + @ReactMethod + fun hardResetDatabase(promise: Promise) { + Thread { + try { + val mapDataDir = File(mReactContext.filesDir.absolutePath + "/.mapbox/map_data") + Log.d(LOG_TAG, "hardResetDatabase: deleting ${mapDataDir.absolutePath}") + if (mapDataDir.exists()) { + val deleted = mapDataDir.deleteRecursively() + Log.d(LOG_TAG, "hardResetDatabase: deleteRecursively result=$deleted") + } + Log.d(LOG_TAG, "hardResetDatabase: done") + promise.resolve(null) + } catch (t: Throwable) { + Log.e(LOG_TAG, "hardResetDatabase: failed", t) + promise.reject("hardResetDatabase error:", t) + } + }.start() + } } \ No newline at end of file From 24765ef643aedbd22ec33134d9cee7229c97bf49 Mon Sep 17 00:00:00 2001 From: Nazar Shcherbiak Date: Tue, 16 Dec 2025 22:11:07 +0200 Subject: [PATCH 3/8] Update bridge --- src/modules/offline/offlineManagerLegacy.ts | 26 ++++++++++----------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/modules/offline/offlineManagerLegacy.ts b/src/modules/offline/offlineManagerLegacy.ts index 58845994e2..5438f9c6fd 100644 --- a/src/modules/offline/offlineManagerLegacy.ts +++ b/src/modules/offline/offlineManagerLegacy.ts @@ -117,30 +117,30 @@ class OfflineManagerLegacy { } /** - * Fully resets offline region database file by deleting both `.mapbox/map_data` and `.mapbox_custom` directories, - * then reinitializes the OfflineRegionManager to point to a new data directory (iOS only). + * Deletes the existing database, which includes both the ambient cache and offline packs, then reinitializes it. * - * On iOS, this is required because the Mapbox SDK does not automatically recreate the `map_data` database after deletion. - * After calling this, all subsequent offline region operations will use a fresh, isolated storage location. + * @example + * await Mapbox.offlineManager.resetDatabase(); * - * On Android, only `.mapbox/map_data` is removed; explicit reinitialization is not needed as the Mapbox C++ manager recreates the database automatically. + * @return {void} */ - async hardResetDatabase() { - await MapboxOfflineManager.hardResetDatabase(); + async resetDatabase(): Promise { + await MapboxOfflineManager.resetDatabase(); this._offlinePacks = {}; await this._initialize(true); } /** - * Deletes the existing database, which includes both the ambient cache and offline packs, then reinitializes it. + * Fully resets offline region database file by deleting both `.mapbox/map_data` and `.mapbox_custom` directories, + * then reinitializes the OfflineRegionManager to point to a new data directory (iOS only). * - * @example - * await Mapbox.offlineManager.resetDatabase(); + * On iOS, this is required because the Mapbox SDK does not automatically recreate the `map_data` database after deletion. + * After calling this, all subsequent offline region operations will use a fresh, isolated storage location. * - * @return {void} + * On Android, only `.mapbox/map_data` is removed; explicit reinitialization is not needed as the Mapbox C++ manager recreates the database automatically. */ - async resetDatabase(): Promise { - await MapboxOfflineManager.resetDatabase(); + async hardResetDatabase(): Promise { + await MapboxOfflineManager.hardResetDatabase(); this._offlinePacks = {}; await this._initialize(true); } From 8004e4b29ea641ab9c5f95c1f29704c07792e0bd Mon Sep 17 00:00:00 2001 From: Nazar Shcherbiak Date: Tue, 16 Dec 2025 22:24:43 +0200 Subject: [PATCH 4/8] Fixes after review --- .../com/rnmapbox/rnmbx/modules/RNMBXOfflineModuleLegacy.kt | 3 +-- ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.swift | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/android/src/main/java/com/rnmapbox/rnmbx/modules/RNMBXOfflineModuleLegacy.kt b/android/src/main/java/com/rnmapbox/rnmbx/modules/RNMBXOfflineModuleLegacy.kt index 37e65885c0..a7860916a5 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/modules/RNMBXOfflineModuleLegacy.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/modules/RNMBXOfflineModuleLegacy.kt @@ -459,8 +459,7 @@ class RNMBXOfflineModuleLegacy(private val mReactContext: ReactApplicationContex val mapDataDir = File(mReactContext.filesDir.absolutePath + "/.mapbox/map_data") Log.d(LOG_TAG, "hardResetDatabase: deleting ${mapDataDir.absolutePath}") if (mapDataDir.exists()) { - val deleted = mapDataDir.deleteRecursively() - Log.d(LOG_TAG, "hardResetDatabase: deleteRecursively result=$deleted") + mapDataDir.deleteRecursively() } Log.d(LOG_TAG, "hardResetDatabase: done") promise.resolve(null) diff --git a/ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.swift b/ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.swift index fba8bfd746..fa9c6b94ec 100644 --- a/ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.swift +++ b/ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.swift @@ -6,6 +6,7 @@ class RNMBXOfflineModuleLegacy: RCTEventEmitter { final let CompleteRegionDownloadState = 2 lazy var offlineRegionManager: OfflineRegionManager = { + // This logic should be aligned with MapView initialization @see ios/RNMBX/RNMBXMapView.swift let fileManager = FileManager.default let appSupport = fileManager.urls( for: .applicationSupportDirectory, From 6229bfd1783ddd842e77262a0352814616b9c81f Mon Sep 17 00:00:00 2001 From: Nazar Shcherbiak Date: Wed, 17 Dec 2025 10:28:50 +0200 Subject: [PATCH 5/8] Align Android hardResetDatabase with iOS implementation --- .../rnmbx/components/mapview/RNMBXMapView.kt | 25 ++++++ .../rnmbx/modules/RNMBXOfflineModuleLegacy.kt | 90 ++++++++++++++++--- src/modules/offline/offlineManagerLegacy.ts | 6 +- 3 files changed, 105 insertions(+), 16 deletions(-) diff --git a/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/RNMBXMapView.kt b/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/RNMBXMapView.kt index ba55b7d1f4..03a1673e14 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/RNMBXMapView.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/RNMBXMapView.kt @@ -1190,6 +1190,27 @@ open class RNMBXMapView(private val mContext: Context, var mManager: RNMBXMapVie return null; } + + // This logic should be aligned with OfflineManager data path @see android/src/main/java/com/rnmapbox/rnmbx/modules/RNMBXOfflineModuleLegacy.kt + private fun resolveDataPath(): File { + val filesDir = mContext.filesDir + val defaultMapDataDir = File(filesDir, ".mapbox/map_data") + val customRoot = File(filesDir, ".mapbox_custom") + + if (customRoot.exists()) { + customRoot.listFiles { entry -> entry.isDirectory }?.forEach { entry -> + val candidateMapData = File(entry, "map_data") + val candidateDb = File(candidateMapData, "map_data.db") + + if (candidateDb.exists()) { + return candidateMapData + } + } + } + + return defaultMapDataDir + } + fun createMapView() : MapView { var created = false; mapViewImpl?.also {impl -> @@ -1199,6 +1220,10 @@ open class RNMBXMapView(private val mContext: Context, var mManager: RNMBXMapVie } } if (!created) { + val targetPath = resolveDataPath() + + MapboxMapsOptions.dataPath = targetPath.absolutePath + var options: MapInitOptions? = null if (surfaceView == false) { options = MapInitOptions(context = mContext, textureView = true) diff --git a/android/src/main/java/com/rnmapbox/rnmbx/modules/RNMBXOfflineModuleLegacy.kt b/android/src/main/java/com/rnmapbox/rnmbx/modules/RNMBXOfflineModuleLegacy.kt index a7860916a5..500111b147 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/modules/RNMBXOfflineModuleLegacy.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/modules/RNMBXOfflineModuleLegacy.kt @@ -27,6 +27,8 @@ import com.rnmapbox.rnmbx.utils.ConvertUtils import com.rnmapbox.rnmbx.utils.extensions.toGeometryCollection import com.rnmapbox.rnmbx.utils.writableArrayOf import com.rnmapbox.rnmbx.v11compat.offlinemanager.getOfflineRegionManager +import com.mapbox.maps.MapboxMapsOptions +import java.util.UUID import org.json.JSONException import org.json.JSONObject import java.io.File @@ -55,10 +57,44 @@ class RNMBXOfflineModuleLegacy(private val mReactContext: ReactApplicationContex return REACT_CLASS } - val offlineRegionManager: OfflineRegionManager by lazy { - getOfflineRegionManager { - RNMBXModule.getAccessToken(mReactContext) + @Volatile + private var offlineRegionManager: OfflineRegionManager = createOfflineRegionManager() + + // This logic should be aligned with MapView data path @see android/src/main/java/com/rnmapbox/rnmbx/components/mapview/RNMBXMapView.kt + private fun resolveOfflineDataPath(): File { + val filesDir = mReactContext.filesDir + val defaultMapDataDir = File(filesDir, ".mapbox/map_data") + val customRoot = File(filesDir, ".mapbox_custom") + + if (customRoot.exists()) { + customRoot.listFiles { entry -> entry.isDirectory }?.forEach { entry -> + val candidateMapData = File(entry, "map_data") + val candidateDb = File(candidateMapData, "map_data.db") + + if (candidateDb.exists()) { + return candidateMapData + } + } + } + + return defaultMapDataDir + } + + private fun createOfflineRegionManager(dataPath: File? = null): OfflineRegionManager { + val targetPath = dataPath ?: resolveOfflineDataPath() + if (!targetPath.exists()) { + targetPath.mkdirs() } + + MapboxMapsOptions.dataPath = targetPath.absolutePath + return getOfflineRegionManager { RNMBXModule.getAccessToken(mReactContext) } + } + + private fun generateCustomDataPath(): File { + val filesDir = mReactContext.filesDir + val customRoot = File(filesDir, ".mapbox_custom") + val randomSuffix = UUID.randomUUID().toString().take(4) + return File(customRoot, "$randomSuffix/map_data") } private fun makeDefinition( @@ -377,9 +413,6 @@ class RNMBXOfflineModuleLegacy(private val mReactContext: ReactApplicationContex @ReactMethod fun setTileCountLimit(tileCountLimit: Int) { UiThreadUtil.runOnUiThread { - val offlineRegionManager = getOfflineRegionManager { - RNMBXModule.getAccessToken(mReactContext) - } offlineRegionManager.setOfflineMapboxTileCountLimit(tileCountLimit.toLong()) } } @@ -452,17 +485,50 @@ class RNMBXOfflineModuleLegacy(private val mReactContext: ReactApplicationContex } } + fun reinitOfflineRegionManager(promise: Promise) { + try { + val targetPath = generateCustomDataPath() + if (!targetPath.exists()) { + targetPath.mkdirs() + } + + UiThreadUtil.runOnUiThread { + try { + offlineRegionManager = createOfflineRegionManager(targetPath) + Log.d(LOG_TAG, "reinitOfflineRegionManager: using ${targetPath.absolutePath}") + promise.resolve(null) + } catch (t: Throwable) { + Log.e(LOG_TAG, "reinitOfflineRegionManager: failed", t) + promise.reject("reinitOfflineRegionManager error:", t) + } + } + } catch (t: Throwable) { + Log.e(LOG_TAG, "reinitOfflineRegionManager: failed to prepare path", t) + promise.reject("reinitOfflineRegionManager error:", t) + } + } + @ReactMethod fun hardResetDatabase(promise: Promise) { Thread { try { - val mapDataDir = File(mReactContext.filesDir.absolutePath + "/.mapbox/map_data") - Log.d(LOG_TAG, "hardResetDatabase: deleting ${mapDataDir.absolutePath}") - if (mapDataDir.exists()) { - mapDataDir.deleteRecursively() + val filesDir = mReactContext.filesDir + val defaultMapDataDir = File(filesDir, ".mapbox/map_data") + val customMapDataDir = File(filesDir, ".mapbox_custom") + + Log.d(LOG_TAG, "hardResetDatabase: deleting ${defaultMapDataDir.absolutePath} and ${customMapDataDir.absolutePath}") + + if (defaultMapDataDir.exists()) { + defaultMapDataDir.deleteRecursively() + } + + if (customMapDataDir.exists()) { + customMapDataDir.deleteRecursively() + } + + UiThreadUtil.runOnUiThread { + reinitOfflineRegionManager(promise) } - Log.d(LOG_TAG, "hardResetDatabase: done") - promise.resolve(null) } catch (t: Throwable) { Log.e(LOG_TAG, "hardResetDatabase: failed", t) promise.reject("hardResetDatabase error:", t) diff --git a/src/modules/offline/offlineManagerLegacy.ts b/src/modules/offline/offlineManagerLegacy.ts index 5438f9c6fd..8b4ab3d288 100644 --- a/src/modules/offline/offlineManagerLegacy.ts +++ b/src/modules/offline/offlineManagerLegacy.ts @@ -132,12 +132,10 @@ class OfflineManagerLegacy { /** * Fully resets offline region database file by deleting both `.mapbox/map_data` and `.mapbox_custom` directories, - * then reinitializes the OfflineRegionManager to point to a new data directory (iOS only). + * then reinitializes the OfflineRegionManager to point to a new data directory. * - * On iOS, this is required because the Mapbox SDK does not automatically recreate the `map_data` database after deletion. + * Reinitialization is required because the Mapbox SDK does not automatically recreate the `map_data` database after deletion. * After calling this, all subsequent offline region operations will use a fresh, isolated storage location. - * - * On Android, only `.mapbox/map_data` is removed; explicit reinitialization is not needed as the Mapbox C++ manager recreates the database automatically. */ async hardResetDatabase(): Promise { await MapboxOfflineManager.hardResetDatabase(); From 2546275c93f8b936a68ba8dee8644f3380c68fac Mon Sep 17 00:00:00 2001 From: Nazar Shcherbiak Date: Thu, 23 Jul 2026 12:24:22 +0300 Subject: [PATCH 6/8] Fixes after review --- .../rnmbx/components/mapview/RNMBXMapView.kt | 2 +- ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.swift | 16 ++++------------ ios/RNMBX/RNMBXMapView.swift | 8 +++----- src/modules/offline/offlineManagerLegacy.ts | 1 + 4 files changed, 9 insertions(+), 18 deletions(-) diff --git a/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/RNMBXMapView.kt b/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/RNMBXMapView.kt index 03a1673e14..445c4dbc26 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/RNMBXMapView.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/RNMBXMapView.kt @@ -71,6 +71,7 @@ import com.rnmapbox.rnmbx.v11compat.event.* import com.rnmapbox.rnmbx.v11compat.feature.* import com.rnmapbox.rnmbx.v11compat.mapboxmap.* import com.rnmapbox.rnmbx.v11compat.ornamentsettings.* +import java.io.File import org.json.JSONException import org.json.JSONObject @@ -1624,4 +1625,3 @@ fun OrnamentSettings.setPosAndMargins(posAndMargins: ReadableMap?) { } - diff --git a/ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.swift b/ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.swift index fa9c6b94ec..4a6daa2eee 100644 --- a/ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.swift +++ b/ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.swift @@ -44,12 +44,8 @@ class RNMBXOfflineModuleLegacy: RCTEventEmitter { let dataPathURL = customDataPath ?? defaultDataPath #if RNMBX_11 - return OfflineRegionManager( - resourceOptions: .init( - accessToken: RNMBXModule.accessToken!, - dataPathURL: dataPathURL - ) - ) + MapboxMapsOptions.dataPath = dataPathURL + return OfflineRegionManager() #else return OfflineRegionManager( resourceOptions: .init( @@ -509,12 +505,8 @@ func getRegionByName(name: String, offlineRegions: [OfflineRegion]) -> OfflineRe DispatchQueue.main.async { #if RNMBX_11 - self.offlineRegionManager = OfflineRegionManager( - resourceOptions: .init( - accessToken: RNMBXModule.accessToken!, - dataPathURL: targetDir - ) - ) + MapboxMapsOptions.dataPath = targetDir + self.offlineRegionManager = OfflineRegionManager() #else self.offlineRegionManager = OfflineRegionManager( resourceOptions: .init( diff --git a/ios/RNMBX/RNMBXMapView.swift b/ios/RNMBX/RNMBXMapView.swift index d5b69940fe..4beaf97eef 100644 --- a/ios/RNMBX/RNMBXMapView.swift +++ b/ios/RNMBX/RNMBXMapView.swift @@ -296,12 +296,11 @@ open class RNMBXMapView: UIView, RCTInvalidating { if accessToken == nil { Logger.log(level: .error, message: "No accessToken set, please call Mapbox.setAccessToken(...)") } - let resourceOptions = ResourceOptions(accessToken: accessToken ?? "", dataPathURL: dataPathURL) - #if RNMBX_11 - _mapView = MapView(frame: self.bounds, mapInitOptions: MapInitOptions(resourceOptions: resourceOptions)) + MapboxMapsOptions.dataPath = dataPathURL + _mapView = MapView(frame: self.bounds, mapInitOptions: MapInitOptions()) #else - + let resourceOptions = ResourceOptions(accessToken: accessToken ?? "", dataPathURL: dataPathURL) _mapView = MapView(frame: frame, mapInitOptions: MapInitOptions(resourceOptions: resourceOptions)) #endif _mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight] @@ -1963,4 +1962,3 @@ class RNMBXPointAnnotationManager : AnnotationInteractionDelegate { manager.refresh() } } - diff --git a/src/modules/offline/offlineManagerLegacy.ts b/src/modules/offline/offlineManagerLegacy.ts index 8b4ab3d288..95a0717625 100644 --- a/src/modules/offline/offlineManagerLegacy.ts +++ b/src/modules/offline/offlineManagerLegacy.ts @@ -136,6 +136,7 @@ class OfflineManagerLegacy { * * Reinitialization is required because the Mapbox SDK does not automatically recreate the `map_data` database after deletion. * After calling this, all subsequent offline region operations will use a fresh, isolated storage location. + * Unmount active MapView instances before calling this; SDK data path is fixed at MapView creation. */ async hardResetDatabase(): Promise { await MapboxOfflineManager.hardResetDatabase(); From 49009a61cfe59055f3b5e808ec1e516f3d92326b Mon Sep 17 00:00:00 2001 From: Nazar Shcherbiak Date: Thu, 23 Jul 2026 12:40:05 +0300 Subject: [PATCH 7/8] Extend UUID --- .../com/rnmapbox/rnmbx/modules/RNMBXOfflineModuleLegacy.kt | 4 ++-- ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.swift | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/android/src/main/java/com/rnmapbox/rnmbx/modules/RNMBXOfflineModuleLegacy.kt b/android/src/main/java/com/rnmapbox/rnmbx/modules/RNMBXOfflineModuleLegacy.kt index 500111b147..bd703d3409 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/modules/RNMBXOfflineModuleLegacy.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/modules/RNMBXOfflineModuleLegacy.kt @@ -93,7 +93,7 @@ class RNMBXOfflineModuleLegacy(private val mReactContext: ReactApplicationContex private fun generateCustomDataPath(): File { val filesDir = mReactContext.filesDir val customRoot = File(filesDir, ".mapbox_custom") - val randomSuffix = UUID.randomUUID().toString().take(4) + val randomSuffix = UUID.randomUUID().toString() return File(customRoot, "$randomSuffix/map_data") } @@ -535,4 +535,4 @@ class RNMBXOfflineModuleLegacy(private val mReactContext: ReactApplicationContex } }.start() } -} \ No newline at end of file +} diff --git a/ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.swift b/ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.swift index 4a6daa2eee..0c4b211026 100644 --- a/ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.swift +++ b/ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.swift @@ -485,8 +485,7 @@ func getRegionByName(name: String, offlineRegions: [OfflineRegion]) -> OfflineRe let appSupport = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! - // Generate random suffix, e.g. "A1B2" - let randomSuffix = UUID().uuidString.prefix(4) + let randomSuffix = UUID().uuidString let folderName = ".mapbox_custom/\(randomSuffix)/map_data" let targetDir = appSupport.appendingPathComponent(folderName, isDirectory: true) From 76a5eacf09277c3a0b81687cdcaf079d24312497 Mon Sep 17 00:00:00 2001 From: Nazar Shcherbiak Date: Thu, 23 Jul 2026 12:48:37 +0300 Subject: [PATCH 8/8] Resolve new map data path in shared util --- .../rnmbx/components/mapview/RNMBXMapView.kt | 26 +----- .../rnmbx/modules/RNMBXOfflineModuleLegacy.kt | 34 +------- .../com/rnmapbox/rnmbx/utils/MapDataPath.kt | 26 ++++++ .../Offline/RNMBXOfflineModuleLegacy.swift | 80 +++++++------------ ios/RNMBX/RNMBXMapDataPath.swift | 56 +++++++++++++ ios/RNMBX/RNMBXMapView.swift | 46 ++--------- 6 files changed, 122 insertions(+), 146 deletions(-) create mode 100644 android/src/main/java/com/rnmapbox/rnmbx/utils/MapDataPath.kt create mode 100644 ios/RNMBX/RNMBXMapDataPath.swift diff --git a/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/RNMBXMapView.kt b/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/RNMBXMapView.kt index 445c4dbc26..bfee71a348 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/RNMBXMapView.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/RNMBXMapView.kt @@ -71,7 +71,6 @@ import com.rnmapbox.rnmbx.v11compat.event.* import com.rnmapbox.rnmbx.v11compat.feature.* import com.rnmapbox.rnmbx.v11compat.mapboxmap.* import com.rnmapbox.rnmbx.v11compat.ornamentsettings.* -import java.io.File import org.json.JSONException import org.json.JSONObject @@ -1190,28 +1189,6 @@ open class RNMBXMapView(private val mContext: Context, var mManager: RNMBXMapVie } return null; } - - - // This logic should be aligned with OfflineManager data path @see android/src/main/java/com/rnmapbox/rnmbx/modules/RNMBXOfflineModuleLegacy.kt - private fun resolveDataPath(): File { - val filesDir = mContext.filesDir - val defaultMapDataDir = File(filesDir, ".mapbox/map_data") - val customRoot = File(filesDir, ".mapbox_custom") - - if (customRoot.exists()) { - customRoot.listFiles { entry -> entry.isDirectory }?.forEach { entry -> - val candidateMapData = File(entry, "map_data") - val candidateDb = File(candidateMapData, "map_data.db") - - if (candidateDb.exists()) { - return candidateMapData - } - } - } - - return defaultMapDataDir - } - fun createMapView() : MapView { var created = false; mapViewImpl?.also {impl -> @@ -1221,7 +1198,7 @@ open class RNMBXMapView(private val mContext: Context, var mManager: RNMBXMapVie } } if (!created) { - val targetPath = resolveDataPath() + val targetPath = resolveMapDataPath(mContext.filesDir) MapboxMapsOptions.dataPath = targetPath.absolutePath @@ -1624,4 +1601,3 @@ fun OrnamentSettings.setPosAndMargins(posAndMargins: ReadableMap?) { this.margins = margins } - diff --git a/android/src/main/java/com/rnmapbox/rnmbx/modules/RNMBXOfflineModuleLegacy.kt b/android/src/main/java/com/rnmapbox/rnmbx/modules/RNMBXOfflineModuleLegacy.kt index bd703d3409..f1b8b53a11 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/modules/RNMBXOfflineModuleLegacy.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/modules/RNMBXOfflineModuleLegacy.kt @@ -25,10 +25,11 @@ import com.mapbox.maps.OfflineRegionStatus import com.mapbox.maps.OfflineRegionTilePyramidDefinition import com.rnmapbox.rnmbx.utils.ConvertUtils import com.rnmapbox.rnmbx.utils.extensions.toGeometryCollection +import com.rnmapbox.rnmbx.utils.newMapDataPath +import com.rnmapbox.rnmbx.utils.resolveMapDataPath import com.rnmapbox.rnmbx.utils.writableArrayOf import com.rnmapbox.rnmbx.v11compat.offlinemanager.getOfflineRegionManager import com.mapbox.maps.MapboxMapsOptions -import java.util.UUID import org.json.JSONException import org.json.JSONObject import java.io.File @@ -60,28 +61,8 @@ class RNMBXOfflineModuleLegacy(private val mReactContext: ReactApplicationContex @Volatile private var offlineRegionManager: OfflineRegionManager = createOfflineRegionManager() - // This logic should be aligned with MapView data path @see android/src/main/java/com/rnmapbox/rnmbx/components/mapview/RNMBXMapView.kt - private fun resolveOfflineDataPath(): File { - val filesDir = mReactContext.filesDir - val defaultMapDataDir = File(filesDir, ".mapbox/map_data") - val customRoot = File(filesDir, ".mapbox_custom") - - if (customRoot.exists()) { - customRoot.listFiles { entry -> entry.isDirectory }?.forEach { entry -> - val candidateMapData = File(entry, "map_data") - val candidateDb = File(candidateMapData, "map_data.db") - - if (candidateDb.exists()) { - return candidateMapData - } - } - } - - return defaultMapDataDir - } - private fun createOfflineRegionManager(dataPath: File? = null): OfflineRegionManager { - val targetPath = dataPath ?: resolveOfflineDataPath() + val targetPath = dataPath ?: resolveMapDataPath(mReactContext.filesDir) if (!targetPath.exists()) { targetPath.mkdirs() } @@ -90,13 +71,6 @@ class RNMBXOfflineModuleLegacy(private val mReactContext: ReactApplicationContex return getOfflineRegionManager { RNMBXModule.getAccessToken(mReactContext) } } - private fun generateCustomDataPath(): File { - val filesDir = mReactContext.filesDir - val customRoot = File(filesDir, ".mapbox_custom") - val randomSuffix = UUID.randomUUID().toString() - return File(customRoot, "$randomSuffix/map_data") - } - private fun makeDefinition( bounds: CoordinateBounds, options: ReadableMap @@ -487,7 +461,7 @@ class RNMBXOfflineModuleLegacy(private val mReactContext: ReactApplicationContex fun reinitOfflineRegionManager(promise: Promise) { try { - val targetPath = generateCustomDataPath() + val targetPath = newMapDataPath(mReactContext.filesDir) if (!targetPath.exists()) { targetPath.mkdirs() } diff --git a/android/src/main/java/com/rnmapbox/rnmbx/utils/MapDataPath.kt b/android/src/main/java/com/rnmapbox/rnmbx/utils/MapDataPath.kt new file mode 100644 index 0000000000..a815d540ce --- /dev/null +++ b/android/src/main/java/com/rnmapbox/rnmbx/utils/MapDataPath.kt @@ -0,0 +1,26 @@ +package com.rnmapbox.rnmbx.utils + +import java.io.File +import java.util.UUID + +fun resolveMapDataPath(filesDir: File): File { + val defaultMapDataDir = File(filesDir, ".mapbox/map_data") + val customRoot = File(filesDir, ".mapbox_custom") + + if (customRoot.exists()) { + customRoot.listFiles { entry -> entry.isDirectory }?.forEach { entry -> + val candidateMapData = File(entry, "map_data") + val candidateDb = File(candidateMapData, "map_data.db") + + if (candidateDb.exists()) { + return candidateMapData + } + } + } + + return defaultMapDataDir +} + +fun newMapDataPath(filesDir: File): File { + return File(filesDir, ".mapbox_custom/${UUID.randomUUID()}/map_data") +} diff --git a/ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.swift b/ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.swift index 0c4b211026..dd1f868d1a 100644 --- a/ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.swift +++ b/ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.swift @@ -6,53 +6,23 @@ class RNMBXOfflineModuleLegacy: RCTEventEmitter { final let CompleteRegionDownloadState = 2 lazy var offlineRegionManager: OfflineRegionManager = { - // This logic should be aligned with MapView initialization @see ios/RNMBX/RNMBXMapView.swift - let fileManager = FileManager.default - let appSupport = fileManager.urls( - for: .applicationSupportDirectory, - in: .userDomainMask - ).first! - - // Default: Library/Application Support/.mapbox/map_data - let defaultDataPath = appSupport.appendingPathComponent(".mapbox/map_data") - - // Preferred: Library/Application Support/.mapbox_custom/XXXX/map_data/map_data.db - var customDataPath: URL? = nil - let customRoot = appSupport.appendingPathComponent(".mapbox_custom", isDirectory: true) - - if fileManager.fileExists(atPath: customRoot.path), - let entries = try? fileManager.contentsOfDirectory( - at: customRoot, - includingPropertiesForKeys: [.isDirectoryKey], - options: [] - ) { - for entry in entries { - // Only consider subdirectories under .mapbox_custom - if let isDir = try? entry.resourceValues(forKeys: [.isDirectoryKey]).isDirectory, - isDir == true { - let candidateMapData = entry.appendingPathComponent("map_data", isDirectory: true) - let candidateDb = candidateMapData.appendingPathComponent("map_data.db") - - if fileManager.fileExists(atPath: candidateDb.path) { - customDataPath = candidateMapData - break - } - } - } - } - - let dataPathURL = customDataPath ?? defaultDataPath + let dataPathURL = try? RNMBXMapDataPath() #if RNMBX_11 - MapboxMapsOptions.dataPath = dataPathURL + if let dataPathURL = dataPathURL { + MapboxMapsOptions.dataPath = dataPathURL + } return OfflineRegionManager() #else - return OfflineRegionManager( - resourceOptions: .init( - accessToken: RNMBXModule.accessToken!, - dataPathURL: dataPathURL + if let dataPathURL = dataPathURL { + return OfflineRegionManager( + resourceOptions: .init( + accessToken: RNMBXModule.accessToken ?? "", + dataPathURL: dataPathURL + ) ) - ) + } + return OfflineRegionManager(resourceOptions: .init(accessToken: RNMBXModule.accessToken ?? "")) #endif }() @@ -482,12 +452,14 @@ func getRegionByName(name: String, offlineRegions: [OfflineRegion]) -> OfflineRe reject: @escaping RCTPromiseRejectBlock ) { let fileManager = FileManager.default - let appSupport = fileManager.urls(for: .applicationSupportDirectory, - in: .userDomainMask).first! + let targetDir: URL - let randomSuffix = UUID().uuidString - let folderName = ".mapbox_custom/\(randomSuffix)/map_data" - let targetDir = appSupport.appendingPathComponent(folderName, isDirectory: true) + do { + targetDir = try RNMBXNewMapDataPath() + } catch { + reject("dir_error", error.localizedDescription, error) + return + } // Ensure directory exists do { @@ -509,7 +481,7 @@ func getRegionByName(name: String, offlineRegions: [OfflineRegion]) -> OfflineRe #else self.offlineRegionManager = OfflineRegionManager( resourceOptions: .init( - accessToken: RNMBXModule.accessToken!, + accessToken: RNMBXModule.accessToken ?? "", dataPathURL: targetDir ) ) @@ -526,10 +498,14 @@ func getRegionByName(name: String, offlineRegions: [OfflineRegion]) -> OfflineRe reject: @escaping RCTPromiseRejectBlock ) { let fileManager = FileManager.default - let appSupport = fileManager.urls( - for: .applicationSupportDirectory, - in: .userDomainMask - ).first! + let appSupport: URL + + do { + appSupport = try RNMBXApplicationSupportDirectory() + } catch { + reject("dir_error", error.localizedDescription, error) + return + } let defaultMapDataDir = appSupport.appendingPathComponent( ".mapbox/map_data", diff --git a/ios/RNMBX/RNMBXMapDataPath.swift b/ios/RNMBX/RNMBXMapDataPath.swift new file mode 100644 index 0000000000..1cf4a774cf --- /dev/null +++ b/ios/RNMBX/RNMBXMapDataPath.swift @@ -0,0 +1,56 @@ +import Foundation + +private let RNMBXMapDataPathErrorDomain = "RNMBXMapDataPath" + +func RNMBXApplicationSupportDirectory() throws -> URL { + guard let appSupport = FileManager.default.urls( + for: .applicationSupportDirectory, + in: .userDomainMask + ).first else { + throw NSError( + domain: RNMBXMapDataPathErrorDomain, + code: 1, + userInfo: [NSLocalizedDescriptionKey: "Application Support directory unavailable"] + ) + } + + return appSupport +} + +func RNMBXMapDataPath() throws -> URL { + let fileManager = FileManager.default + let appSupport = try RNMBXApplicationSupportDirectory() + let defaultDataPath = appSupport.appendingPathComponent(".mapbox/map_data", isDirectory: true) + let customRoot = appSupport.appendingPathComponent(".mapbox_custom", isDirectory: true) + + guard fileManager.fileExists(atPath: customRoot.path), + let entries = try? fileManager.contentsOfDirectory( + at: customRoot, + includingPropertiesForKeys: [.isDirectoryKey], + options: [] + ) else { + return defaultDataPath + } + + for entry in entries { + if let isDir = try? entry.resourceValues(forKeys: [.isDirectoryKey]).isDirectory, + isDir == true { + let candidateMapData = entry.appendingPathComponent("map_data", isDirectory: true) + let candidateDb = candidateMapData.appendingPathComponent("map_data.db") + + if fileManager.fileExists(atPath: candidateDb.path) { + return candidateMapData + } + } + } + + return defaultDataPath +} + +func RNMBXNewMapDataPath() throws -> URL { + let appSupport = try RNMBXApplicationSupportDirectory() + return appSupport.appendingPathComponent( + ".mapbox_custom/\(UUID().uuidString)/map_data", + isDirectory: true + ) +} diff --git a/ios/RNMBX/RNMBXMapView.swift b/ios/RNMBX/RNMBXMapView.swift index 4beaf97eef..cb6cfef2f3 100644 --- a/ios/RNMBX/RNMBXMapView.swift +++ b/ios/RNMBX/RNMBXMapView.swift @@ -254,53 +254,21 @@ open class RNMBXMapView: UIView, RCTInvalidating { if let mapViewImpl = mapViewImpl, let mapViewInstance = createAndAddMapViewImpl(mapViewImpl, self) { _mapView = mapViewInstance } else { - - // This logic should be aligned with offlineRegionManager initialization @see ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.swift - let fileManager = FileManager.default - let appSupport = fileManager.urls( - for: .applicationSupportDirectory, - in: .userDomainMask - ).first! - - // Default: Library/Application Support/.mapbox/map_data - let defaultDataPath = appSupport.appendingPathComponent(".mapbox/map_data") - - // Preferred: Library/Application Support/.mapbox_custom/XXXX/map_data/map_data.db - var customDataPath: URL? = nil - let customRoot = appSupport.appendingPathComponent(".mapbox_custom", isDirectory: true) - - if fileManager.fileExists(atPath: customRoot.path), - let entries = try? fileManager.contentsOfDirectory( - at: customRoot, - includingPropertiesForKeys: [.isDirectoryKey], - options: [] - ) { - for entry in entries { - // Only consider subdirectories under .mapbox_custom - if let isDir = try? entry.resourceValues(forKeys: [.isDirectoryKey]).isDirectory, - isDir == true { - let candidateMapData = entry.appendingPathComponent("map_data", isDirectory: true) - let candidateDb = candidateMapData.appendingPathComponent("map_data.db") - - if fileManager.fileExists(atPath: candidateDb.path) { - customDataPath = candidateMapData - break - } - } - } - } - - let dataPathURL = customDataPath ?? defaultDataPath + let dataPathURL = try? RNMBXMapDataPath() let accessToken = RNMBXModule.accessToken if accessToken == nil { Logger.log(level: .error, message: "No accessToken set, please call Mapbox.setAccessToken(...)") } #if RNMBX_11 - MapboxMapsOptions.dataPath = dataPathURL + if let dataPathURL = dataPathURL { + MapboxMapsOptions.dataPath = dataPathURL + } _mapView = MapView(frame: self.bounds, mapInitOptions: MapInitOptions()) #else - let resourceOptions = ResourceOptions(accessToken: accessToken ?? "", dataPathURL: dataPathURL) + let resourceOptions = dataPathURL.map { + ResourceOptions(accessToken: accessToken ?? "", dataPathURL: $0) + } ?? ResourceOptions(accessToken: accessToken ?? "") _mapView = MapView(frame: frame, mapInitOptions: MapInitOptions(resourceOptions: resourceOptions)) #endif _mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]