From f45d67b2d5901bf5c363467bf4f01238d5899b5b Mon Sep 17 00:00:00 2001 From: Hossain Khan Date: Sun, 8 Feb 2026 19:57:58 -0500 Subject: [PATCH] feat: auto-migrate usetrmnl.com to trmnl.com for TRMNL devices - Add domain migration logic in TrmnlDeviceConfigDataStore.deviceConfigFlow - Automatically detects and updates usetrmnl.com -> trmnl.com for TRMNL device type - Migration runs once on config load and persists updated URL - Only affects TRMNL devices (BYOS custom URLs unchanged) - Fixes issue where existing users kept old usetrmnl.com URLs after #240 - See: https://github.com/usetrmnl/trmnl-android/issues/240 --- app/build.gradle.kts | 2 +- .../data/TrmnlDeviceConfigDataStore.kt | 116 +++++++++++------- 2 files changed, 72 insertions(+), 46 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7a1dd60..bf57a1b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -103,7 +103,7 @@ android { // Use fake API response for local development and testing purposes. // ℹ️ To override during local development, change this value to `"false"` // or, you can change the value in the `RepositoryConfigProvider` - buildConfigField("Boolean", "USE_FAKE_API", "true") + buildConfigField("Boolean", "USE_FAKE_API", "false") signingConfig = signingConfigs.getByName("debug") } diff --git a/app/src/main/java/ink/trmnl/android/data/TrmnlDeviceConfigDataStore.kt b/app/src/main/java/ink/trmnl/android/data/TrmnlDeviceConfigDataStore.kt index bc575b7..8693cbf 100644 --- a/app/src/main/java/ink/trmnl/android/data/TrmnlDeviceConfigDataStore.kt +++ b/app/src/main/java/ink/trmnl/android/data/TrmnlDeviceConfigDataStore.kt @@ -226,58 +226,84 @@ class TrmnlDeviceConfigDataStore * - Logs: "Loading device config (legacy): type=..., userApiToken=..." * - Only returns config if `ACCESS_TOKEN_KEY` exists (required field) * + * **Domain Migration:** + * - Automatically migrates `usetrmnl.com` to `trmnl.com` for TRMNL device types + * - See: https://github.com/usetrmnl/trmnl-android/issues/240 + * * This approach ensures seamless migration from older app versions while * maintaining forward compatibility with newer storage format. */ val deviceConfigFlow: Flow = - context.deviceConfigStore.data.map { preferences -> - val configJson = preferences[CONFIG_JSON_KEY] - if (configJson != null) { - try { - val config: TrmnlDeviceConfig? = deviceConfigAdapter.fromJson(configJson) - Timber.tag(TAG).d( - "Loading device config (JSON): type=${config?.type}", - ) - config - } catch (e: Exception) { - Timber.tag(TAG).e(e, "Failed to parse device config") - null - } - } else { - // Legacy migration path - build config from individual preferences - val type = - preferences[DEVICE_TYPE_KEY]?.let { - try { - deviceTypeAdapter.fromJson(it) - } catch (e: Exception) { - TrmnlDeviceType.TRMNL - } - } ?: TrmnlDeviceType.TRMNL - - val token = preferences[ACCESS_TOKEN_KEY] - val url = preferences[API_BASE_URL_KEY] ?: TRMNL_API_SERVER_BASE_URL - val refreshRate = preferences[REFRESH_RATE_SEC_KEY] ?: DEFAULT_REFRESH_INTERVAL_SEC - val deviceMacId = preferences[DEVICE_MAC_ID_KEY] - val isMasterDevice = preferences[IS_MASTER_DEVICE_KEY]?.toBoolean() - - Timber.tag(TAG).d( - "Loading device config (legacy): type=$type, deviceApiToken=${token.obfuscated()}", - ) - - if (token != null) { - TrmnlDeviceConfig( - type = type, - apiBaseUrl = url, - apiAccessToken = token, - deviceMacId = deviceMacId, - refreshRateSecs = refreshRate, - isMasterDevice = isMasterDevice, - ) + context.deviceConfigStore.data + .map { preferences -> + val configJson = preferences[CONFIG_JSON_KEY] + if (configJson != null) { + try { + val config: TrmnlDeviceConfig? = deviceConfigAdapter.fromJson(configJson) + Timber.tag(TAG).d( + "Loading device config (JSON): type=${config?.type}", + ) + config + } catch (e: Exception) { + Timber.tag(TAG).e(e, "Failed to parse device config") + null + } } else { - null + // Legacy migration path - build config from individual preferences + val type = + preferences[DEVICE_TYPE_KEY]?.let { + try { + deviceTypeAdapter.fromJson(it) + } catch (e: Exception) { + TrmnlDeviceType.TRMNL + } + } ?: TrmnlDeviceType.TRMNL + + val token = preferences[ACCESS_TOKEN_KEY] + val url = preferences[API_BASE_URL_KEY] ?: TRMNL_API_SERVER_BASE_URL + val refreshRate = preferences[REFRESH_RATE_SEC_KEY] ?: DEFAULT_REFRESH_INTERVAL_SEC + val deviceMacId = preferences[DEVICE_MAC_ID_KEY] + val isMasterDevice = preferences[IS_MASTER_DEVICE_KEY]?.toBoolean() + + Timber.tag(TAG).d( + "Loading device config (legacy): type=$type, deviceApiToken=${token.obfuscated()}", + ) + + if (token != null) { + TrmnlDeviceConfig( + type = type, + apiBaseUrl = url, + apiAccessToken = token, + deviceMacId = deviceMacId, + refreshRateSecs = refreshRate, + isMasterDevice = isMasterDevice, + ) + } else { + null + } + } + }.map { config -> + // Migrate usetrmnl.com -> trmnl.com for TRMNL device types + // See: https://github.com/usetrmnl/trmnl-android/issues/240 + if (config != null && + config.type == TrmnlDeviceType.TRMNL && + config.apiBaseUrl.contains("usetrmnl.com", ignoreCase = true) + ) { + val newUrl = config.apiBaseUrl.replace("usetrmnl.com", "trmnl.com", ignoreCase = true) + Timber.tag(TAG).i( + "Migrating API base URL from ${config.apiBaseUrl} to $newUrl for TRMNL device", + ) + val migratedConfig = config.copy(apiBaseUrl = newUrl) + // Save the migrated config back to DataStore synchronously + // Using runBlocking is acceptable here as this is a one-time migration + runBlocking { + saveDeviceConfig(migratedConfig) + } + migratedConfig + } else { + config } } - } /** * Saves the complete device configuration