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
This commit is contained in:
Hossain Khan
2026-02-08 19:57:58 -05:00
parent 1769a0a91b
commit f45d67b2d5
2 changed files with 72 additions and 46 deletions
+1 -1
View File
@@ -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")
}
@@ -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<TrmnlDeviceConfig?> =
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