From a8e1ea106816bfb9a96f3e6da57b7e15c0c37bea Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Mon, 27 Jul 2026 12:11:23 -0400 Subject: [PATCH] fix(navigation): back-fill newly-added nav bar buttons into persisted order Users who customized their nav bar order before Tips was added to the NavBarButton enum (#1121) had a persisted order string without Tips. NavBarConfig.deserialize only fell back to defaultOrder on an empty list, so it reconstructed the stored order verbatim, missing Tips. The scanner nav bar filter can only remove buttons, never add them, so enabling the tipping beta flag never surfaced the Tips tab on those devices. deserialize now merges any button missing from the persisted order, inserting each at its position in defaultOrder. Self-heals affected devices on next launch with no migration. --- .../app/core/navigation/NavBarConfig.kt | 20 +++++- .../app/core/navigation/NavBarConfigTest.kt | 65 +++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 apps/flipcash/core/src/test/kotlin/com/flipcash/app/core/navigation/NavBarConfigTest.kt diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/navigation/NavBarConfig.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/navigation/NavBarConfig.kt index 65b9fd23de..0334daefc7 100644 --- a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/navigation/NavBarConfig.kt +++ b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/navigation/NavBarConfig.kt @@ -13,11 +13,29 @@ data class NavBarConfig( fun deserialize(value: String): NavBarConfig { if (value.isBlank()) return Default val parts = value.split("|") - val order = parts.getOrNull(0) + val stored = parts.getOrNull(0) ?.split(",") ?.mapNotNull { runCatching { NavBarButton.valueOf(it) }.getOrNull() } ?.ifEmpty { NavBarButton.defaultOrder } ?: NavBarButton.defaultOrder + // Back-fill any buttons added after this order was persisted (e.g. Tips), + // inserting each at its position in defaultOrder so it lands where intended + // rather than getting appended. Without this, a persisted order that predates + // a new button would never surface it, even when its feature flag is enabled. + val order = if (stored.containsAll(NavBarButton.defaultOrder)) { + stored + } else { + NavBarButton.defaultOrder.fold(stored) { acc, button -> + if (button in acc) { + acc + } else { + val insertAt = NavBarButton.defaultOrder + .subList(0, NavBarButton.defaultOrder.indexOf(button)) + .let { preceding -> acc.indexOfLast { it in preceding } + 1 } + acc.toMutableList().apply { add(insertAt, button) } + } + } + } val label = parts.getOrNull(1) ?.let { runCatching { GiveButtonLabel.valueOf(it) }.getOrNull() } ?: GiveButtonLabel.Give diff --git a/apps/flipcash/core/src/test/kotlin/com/flipcash/app/core/navigation/NavBarConfigTest.kt b/apps/flipcash/core/src/test/kotlin/com/flipcash/app/core/navigation/NavBarConfigTest.kt new file mode 100644 index 0000000000..751b9835f2 --- /dev/null +++ b/apps/flipcash/core/src/test/kotlin/com/flipcash/app/core/navigation/NavBarConfigTest.kt @@ -0,0 +1,65 @@ +package com.flipcash.app.core.navigation + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class NavBarConfigTest { + + @Test + fun `blank string yields the default config`() { + assertEquals(NavBarConfig.Default, NavBarConfig.deserialize("")) + } + + @Test + fun `round-trips a full, current order`() { + val config = NavBarConfig( + order = listOf(NavBarButton.Wallet, NavBarButton.Give, NavBarButton.Discover, NavBarButton.Send, NavBarButton.Tips), + giveButtonLabel = GiveButtonLabel.Cash, + ) + assertEquals(config, NavBarConfig.deserialize(config.serialize())) + } + + @Test + fun `back-fills a button added after the order was persisted, at its default position`() { + // A nav bar order persisted before Tips was added to the enum/defaultOrder. + val legacy = "Discover,Give,Send,Wallet|Cash" + + val order = NavBarConfig.deserialize(legacy).order + + assertTrue(NavBarButton.Tips in order, "Tips should be back-filled into a legacy order") + // Inserted at its defaultOrder position (between Send and Wallet), not appended. + assertEquals( + listOf(NavBarButton.Discover, NavBarButton.Give, NavBarButton.Send, NavBarButton.Tips, NavBarButton.Wallet), + order, + ) + } + + @Test + fun `back-fill preserves a user's custom ordering of existing buttons`() { + val legacy = "Wallet,Send,Give,Discover|Cash" + + val order = NavBarConfig.deserialize(legacy).order + + assertTrue(NavBarButton.Tips in order) + // Existing buttons keep the user's reversed order; only the missing one is added. + assertEquals( + listOf(NavBarButton.Wallet, NavBarButton.Send, NavBarButton.Give, NavBarButton.Discover), + order.filterNot { it == NavBarButton.Tips }, + ) + } + + @Test + fun `unknown button names are dropped and missing known ones back-filled`() { + val order = NavBarConfig.deserialize("Discover,Bogus,Give,Send,Wallet|Cash").order + + assertEquals(NavBarButton.defaultOrder, order) + } + + @Test + fun `empty order falls back to the default order`() { + val order = NavBarConfig.deserialize("|Cash").order + + assertEquals(NavBarButton.defaultOrder, order) + } +}