From 52041f4bcd76a9b4182113d8cab3eac7ef5a9b1b Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 4 Jun 2026 10:24:30 -0400 Subject: [PATCH 01/11] fix(compose): prevent stale onConfirm capture in SlideToConfirm onConfirm was captured by a LaunchedEffect(Unit) without rememberUpdatedState. If the caller recomposes with a new lambda, the stale version executes on swipe-to-confirm. --- .../main/kotlin/com/getcode/ui/components/SlideToConfirm.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/components/src/main/kotlin/com/getcode/ui/components/SlideToConfirm.kt b/ui/components/src/main/kotlin/com/getcode/ui/components/SlideToConfirm.kt index de66eb614..8797ffb82 100644 --- a/ui/components/src/main/kotlin/com/getcode/ui/components/SlideToConfirm.kt +++ b/ui/components/src/main/kotlin/com/getcode/ui/components/SlideToConfirm.kt @@ -162,6 +162,7 @@ fun SlideToConfirm( }, ) { val currentIsLoading by rememberUpdatedState(isLoading) + val currentOnConfirm by rememberUpdatedState(onConfirm) val hapticFeedback = LocalHapticFeedback.current val density = LocalDensity.current val thumbSize = Thumb.Size @@ -210,7 +211,7 @@ fun SlideToConfirm( .filter { it == 1f } .collect { hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - onConfirm() + currentOnConfirm() // Give the caller a moment to set isLoading = true. // If they don't, the confirmation was rejected — reset. delay(200) From 76e81c6e6d07ae07d26fb21fa78b38f483bb9c18 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 4 Jun 2026 10:26:12 -0400 Subject: [PATCH 02/11] fix(compose): cache collection transforms in per-item composables Wrap sortedBy/groupBy, map, and forEachIndexed transforms in remember(key) so they do not re-run on every recomposition inside LazyColumn item factories. --- .../app/bill/customization/features/TextureControls.kt | 9 ++++++--- .../ui/components/chat/messagecontents/Feedback.kt | 8 +++++--- .../chat/messagecontents/MessageTextContent.kt | 4 +++- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/apps/flipcash/features/bill-customization/src/main/kotlin/com/flipcash/app/bill/customization/features/TextureControls.kt b/apps/flipcash/features/bill-customization/src/main/kotlin/com/flipcash/app/bill/customization/features/TextureControls.kt index f36709b95..a08a7b4ae 100644 --- a/apps/flipcash/features/bill-customization/src/main/kotlin/com/flipcash/app/bill/customization/features/TextureControls.kt +++ b/apps/flipcash/features/bill-customization/src/main/kotlin/com/flipcash/app/bill/customization/features/TextureControls.kt @@ -91,10 +91,13 @@ internal fun TextureControls( } } + val blendModes = remember(state.blendModes) { + state.blendModes.map { it.blendMode } + } + val selectedTabIndex by remember(state.selectedBlendMode) { derivedStateOf { - val option = state.selectedBlendMode.blendMode - state.blendModes.map { it.blendMode }.indexOf(option).takeIf { it >= 0 } ?: 0 + blendModes.indexOf(state.selectedBlendMode.blendMode).takeIf { it >= 0 } ?: 0 } } @@ -106,7 +109,7 @@ internal fun TextureControls( indicator = {}, // no indicator divider = {} // no divider ) { - state.blendModes.map { it.blendMode }.forEachIndexed { index, mode -> + blendModes.forEachIndexed { index, mode -> BlendModeTab( mode = mode, isSelected = index == selectedTabIndex, diff --git a/ui/components/src/main/kotlin/com/getcode/ui/components/chat/messagecontents/Feedback.kt b/ui/components/src/main/kotlin/com/getcode/ui/components/chat/messagecontents/Feedback.kt index 8ee49dcdf..855af1f55 100644 --- a/ui/components/src/main/kotlin/com/getcode/ui/components/chat/messagecontents/Feedback.kt +++ b/ui/components/src/main/kotlin/com/getcode/ui/components/chat/messagecontents/Feedback.kt @@ -67,9 +67,11 @@ internal fun Feedback( ) } - val rxns = reactions - .sortedBy { it.sentAt } - .groupBy { it.emoji } + val rxns = remember(reactions) { + reactions + .sortedBy { it.sentAt } + .groupBy { it.emoji } + } rxns.onEach { (emoji, occurrences) -> EmojiCounter( diff --git a/ui/components/src/main/kotlin/com/getcode/ui/components/chat/messagecontents/MessageTextContent.kt b/ui/components/src/main/kotlin/com/getcode/ui/components/chat/messagecontents/MessageTextContent.kt index 2a35dccca..5b03d15de 100644 --- a/ui/components/src/main/kotlin/com/getcode/ui/components/chat/messagecontents/MessageTextContent.kt +++ b/ui/components/src/main/kotlin/com/getcode/ui/components/chat/messagecontents/MessageTextContent.kt @@ -352,7 +352,9 @@ private fun MarkupTextHandler( options.onMarkupClicked != null -> { val markupTextHelper = remember { MarkupTextHelper() } - val markups = options.markupsToResolve.map { Markup.create(it) } + val markups = remember(options.markupsToResolve) { + options.markupsToResolve.map { Markup.create(it) } + } val annotatedString = markupTextHelper.annotate(text.text, markups) val markupHoist = LocalTextLayoutResult.current From 28400b04a065c7a98b41df76be2dbcd8039cf3fa Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 4 Jun 2026 10:26:28 -0400 Subject: [PATCH 03/11] fix(compose): use collectAsStateWithLifecycle in CashScreenContent Last remaining collectAsState() call in production code. Stops flow collection when the app is backgrounded. --- .../com/flipcash/app/cash/internal/CashScreenContent.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/flipcash/features/cash/src/main/kotlin/com/flipcash/app/cash/internal/CashScreenContent.kt b/apps/flipcash/features/cash/src/main/kotlin/com/flipcash/app/cash/internal/CashScreenContent.kt index 8de8bb958..0e8910a57 100644 --- a/apps/flipcash/features/cash/src/main/kotlin/com/flipcash/app/cash/internal/CashScreenContent.kt +++ b/apps/flipcash/features/cash/src/main/kotlin/com/flipcash/app/cash/internal/CashScreenContent.kt @@ -7,7 +7,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -22,7 +22,7 @@ import com.getcode.ui.theme.CodeButton @Composable internal fun GiveScreenContent(viewModel: CashScreenViewModel) { val navigator = LocalCodeNavigator.current - val state by viewModel.stateFlow.collectAsState() + val state by viewModel.stateFlow.collectAsStateWithLifecycle() Column( modifier = Modifier.fillMaxSize(), ) { From f592e6e20f82d7f53090afded91fc19b19b20e22 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 4 Jun 2026 10:26:55 -0400 Subject: [PATCH 04/11] fix(compose): use mutableLongStateOf to avoid autoboxing Replace mutableStateOf(0L) with mutableLongStateOf(0L) to avoid Long autoboxing on every state read. --- .../src/main/kotlin/com/getcode/ui/biometrics/Biometrics.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/biometrics/src/main/kotlin/com/getcode/ui/biometrics/Biometrics.kt b/ui/biometrics/src/main/kotlin/com/getcode/ui/biometrics/Biometrics.kt index d50a45d10..87ebe97bd 100644 --- a/ui/biometrics/src/main/kotlin/com/getcode/ui/biometrics/Biometrics.kt +++ b/ui/biometrics/src/main/kotlin/com/getcode/ui/biometrics/Biometrics.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -62,7 +63,7 @@ fun rememberBiometricsState( } var lastAuthTimestamp by remember { - mutableStateOf(0L) + mutableLongStateOf(0L) } LaunchedEffect(checkBiometrics, requireBiometrics, canAuthenticate) { From 65fa033aefa910f4bf1a3fb05e3c86539ba9fc5e Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 4 Jun 2026 10:34:07 -0400 Subject: [PATCH 05/11] fix(compose): defer animation reads to draw phase via graphicsLayer Replace Modifier.alpha(animatedFloat) and Modifier.rotate(animatedFloat) with graphicsLayer equivalents so animated values are read in the draw phase instead of triggering recomposition every frame. --- .../com/flipcash/app/internal/ui/navigation/MainRoot.kt | 4 ++-- .../app/shareapp/internal/ShareAppScreenContent.kt | 6 +++--- .../com/getcode/ui/components/bars/BottomBarContainer.kt | 7 +++---- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/MainRoot.kt b/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/MainRoot.kt index 499192d8c..4392ab73d 100644 --- a/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/MainRoot.kt +++ b/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/MainRoot.kt @@ -18,7 +18,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.res.painterResource import androidx.navigation3.runtime.NavKey import com.flipcash.app.android.R @@ -79,7 +79,7 @@ internal fun MainRoot(deepLink: () -> DeepLink?) { label = "loading visibility" ) CodeCircularProgressIndicator( - modifier = Modifier.alpha(loadingAlpha) + modifier = Modifier.graphicsLayer { alpha = loadingAlpha } ) } } diff --git a/apps/flipcash/features/shareapp/src/main/kotlin/com/flipcash/app/shareapp/internal/ShareAppScreenContent.kt b/apps/flipcash/features/shareapp/src/main/kotlin/com/flipcash/app/shareapp/internal/ShareAppScreenContent.kt index 8397e5f10..defd0a3cc 100644 --- a/apps/flipcash/features/shareapp/src/main/kotlin/com/flipcash/app/shareapp/internal/ShareAppScreenContent.kt +++ b/apps/flipcash/features/shareapp/src/main/kotlin/com/flipcash/app/shareapp/internal/ShareAppScreenContent.kt @@ -21,7 +21,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.draw.scale import androidx.compose.ui.geometry.Rect import androidx.compose.ui.layout.boundsInWindow @@ -113,7 +113,7 @@ internal fun ShareAppScreenContent() { Text( modifier = Modifier .padding(bottom = CodeTheme.dimens.grid.x4) - .alpha(contentAlpha), + .graphicsLayer { alpha = contentAlpha }, text = stringResource(R.string.subtitle_scanToDownload), style = CodeTheme.typography.textLarge, textAlign = TextAlign.Center @@ -139,7 +139,7 @@ internal fun ShareAppScreenContent() { ) Row( - modifier = Modifier.alpha(contentAlpha), + modifier = Modifier.graphicsLayer { alpha = contentAlpha }, horizontalArrangement = Arrangement.spacedBy( space = CodeTheme.dimens.inset, alignment = Alignment.CenterHorizontally diff --git a/ui/components/src/main/kotlin/com/getcode/ui/components/bars/BottomBarContainer.kt b/ui/components/src/main/kotlin/com/getcode/ui/components/bars/BottomBarContainer.kt index 2e25040e3..485128179 100644 --- a/ui/components/src/main/kotlin/com/getcode/ui/components/bars/BottomBarContainer.kt +++ b/ui/components/src/main/kotlin/com/getcode/ui/components/bars/BottomBarContainer.kt @@ -42,9 +42,8 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clipToBounds -import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString @@ -121,7 +120,7 @@ fun BottomBarContainer(barMessages: BarMessages, onShown: (BottomBarManager.Bott Box( modifier = Modifier .fillMaxSize() - .alpha(scrimAlpha) + .graphicsLayer { alpha = scrimAlpha } .background(CodeTheme.colors.scrim) .rememberedClickable( indication = null, @@ -257,7 +256,7 @@ fun BottomBarView( color = LocalContentColor.current.copy(alpha = 0.8f), ) Icon( - modifier = Modifier.rotate(rotation), + modifier = Modifier.graphicsLayer { rotationZ = rotation }, imageVector = Icons.Rounded.ExpandMore, contentDescription = null, tint = LocalContentColor.current.copy(alpha = 0.8f), From a8b5cba5b457686578e8ecb28dc28a48894cad31 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 4 Jun 2026 10:35:11 -0400 Subject: [PATCH 06/11] fix(compose): use viewModel as LaunchedEffect key for event flow collection Replace LaunchedEffect(Unit) with LaunchedEffect(viewModel) so the effect restarts if the ViewModel instance changes, and is consistent across composables. --- .../app/directsend/internal/screens/ContactListScreen.kt | 4 ++-- .../internal/screens/ContactsPermissionGateScreen.kt | 2 +- .../kotlin/com/flipcash/app/tokens/PhantomWalletScreens.kt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/ContactListScreen.kt b/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/ContactListScreen.kt index cf04d946e..c43e33558 100644 --- a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/ContactListScreen.kt +++ b/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/ContactListScreen.kt @@ -88,7 +88,7 @@ internal fun ContactListScreen() { val state by viewModel.stateFlow.collectAsStateWithLifecycle() - LaunchedEffect(Unit) { + LaunchedEffect(viewModel) { viewModel.eventFlow .filterIsInstance() .collect { event -> @@ -96,7 +96,7 @@ internal fun ContactListScreen() { } } - LaunchedEffect(Unit) { + LaunchedEffect(viewModel) { viewModel.eventFlow .filterIsInstance() .collect { event -> diff --git a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/ContactsPermissionGateScreen.kt b/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/ContactsPermissionGateScreen.kt index 23775602b..241097756 100644 --- a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/ContactsPermissionGateScreen.kt +++ b/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/ContactsPermissionGateScreen.kt @@ -50,7 +50,7 @@ internal fun ContactsPermissionGateScreen() { } } - LaunchedEffect(Unit) { + LaunchedEffect(viewModel) { viewModel.eventFlow .filterIsInstance() .collect { flowNavigator.proceed() } diff --git a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/PhantomWalletScreens.kt b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/PhantomWalletScreens.kt index 44776c399..d6ee1bcd3 100644 --- a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/PhantomWalletScreens.kt +++ b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/PhantomWalletScreens.kt @@ -47,7 +47,7 @@ internal fun PhantomConnectConfirmationScreen() { .launchIn(this) } - LaunchedEffect(Unit) { + LaunchedEffect(viewModel) { viewModel.eventFlow .filterIsInstance() .onEach { flowNavigator.navigateTo(SwapStep.PhantomConfirmTransaction) } From 640e1b52299715325af57dbc9109a2d6701dd275 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 4 Jun 2026 10:35:55 -0400 Subject: [PATCH 07/11] fix(compose): add stable keys to lazy lists Add key parameters to items() calls in DeviceLogsScreen, UserFlagsEditor, FrequentlyUsedEmojis, and EmojiSearch so Compose can diff items correctly during recomposition instead of recomposing every item. --- .../app/devicelogs/internal/DeviceLogsScreen.kt | 5 ++++- .../flipcash/app/userflags/internal/UserFlagsEditor.kt | 10 ++++++++-- .../main/kotlin/com/getcode/ui/emojis/EmojiSearch.kt | 5 ++++- .../com/getcode/ui/emojis/FrequentlyUsedEmojis.kt | 5 ++++- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/apps/flipcash/features/device-logs/src/main/kotlin/com/flipcash/app/devicelogs/internal/DeviceLogsScreen.kt b/apps/flipcash/features/device-logs/src/main/kotlin/com/flipcash/app/devicelogs/internal/DeviceLogsScreen.kt index e2d5cd2e2..401642dd6 100644 --- a/apps/flipcash/features/device-logs/src/main/kotlin/com/flipcash/app/devicelogs/internal/DeviceLogsScreen.kt +++ b/apps/flipcash/features/device-logs/src/main/kotlin/com/flipcash/app/devicelogs/internal/DeviceLogsScreen.kt @@ -196,7 +196,10 @@ private fun LogList( ), verticalArrangement = Arrangement.spacedBy(2.dp), ) { - items(lines.asReversed()) { line -> + items( + items = lines.asReversed(), + key = { it.hashCode() }, + ) { line -> LogLine(line = line, filter = filter) } } diff --git a/apps/flipcash/features/userflags/src/main/kotlin/com/flipcash/app/userflags/internal/UserFlagsEditor.kt b/apps/flipcash/features/userflags/src/main/kotlin/com/flipcash/app/userflags/internal/UserFlagsEditor.kt index d7abc992a..ada552e55 100644 --- a/apps/flipcash/features/userflags/src/main/kotlin/com/flipcash/app/userflags/internal/UserFlagsEditor.kt +++ b/apps/flipcash/features/userflags/src/main/kotlin/com/flipcash/app/userflags/internal/UserFlagsEditor.kt @@ -75,13 +75,19 @@ private fun UserFlagsEditor( ) { // Read-only section item { SectionHeader("Read-Only") } - items(state.readOnlyEntries) { entry -> + items( + items = state.readOnlyEntries, + key = { it.label }, + ) { entry -> ReadOnlyRow(entry) } // Editable section item { SectionHeader("Overridable") } - items(state.editableEntries) { entry -> + items( + items = state.editableEntries, + key = { it.field.label }, + ) { entry -> EditableRow( modifier = Modifier.fillMaxWidth(), entry = entry, diff --git a/ui/emojis/src/main/kotlin/com/getcode/ui/emojis/EmojiSearch.kt b/ui/emojis/src/main/kotlin/com/getcode/ui/emojis/EmojiSearch.kt index 8d429c651..7d033dfe2 100644 --- a/ui/emojis/src/main/kotlin/com/getcode/ui/emojis/EmojiSearch.kt +++ b/ui/emojis/src/main/kotlin/com/getcode/ui/emojis/EmojiSearch.kt @@ -51,7 +51,10 @@ fun EmojiSearchResults( bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + CodeTheme.dimens.grid.x2 ) ) { - items(results.orEmpty()) { emoji -> + items( + items = results.orEmpty(), + key = { it.unicode }, + ) { emoji -> Row( modifier = Modifier .clickable { onSelected(emoji.unicode) }, diff --git a/ui/emojis/src/main/kotlin/com/getcode/ui/emojis/FrequentlyUsedEmojis.kt b/ui/emojis/src/main/kotlin/com/getcode/ui/emojis/FrequentlyUsedEmojis.kt index c4082662d..2f3a7fc60 100644 --- a/ui/emojis/src/main/kotlin/com/getcode/ui/emojis/FrequentlyUsedEmojis.kt +++ b/ui/emojis/src/main/kotlin/com/getcode/ui/emojis/FrequentlyUsedEmojis.kt @@ -49,7 +49,10 @@ fun FrequentlyUsedEmojis( .height(CodeTheme.dimens.grid.x9), contentPadding = PaddingValues(horizontal = CodeTheme.dimens.inset) ) { - items(frequentlyUsedEmojis) { emoji -> + items( + items = frequentlyUsedEmojis, + key = { it }, + ) { emoji -> EmojiRender( emoji = emoji, size = DpSize( From a01061299527f565e6a74c266eb3e3bba5096ab1 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 4 Jun 2026 10:37:46 -0400 Subject: [PATCH 08/11] fix(compose): use rememberSaveable for user-driven UI flags Replace remember with rememberSaveable for isExportSeedRequested and isStoragePermissionGranted so these flags survive configuration changes. --- .../app/backupkey/internal/BackupKeyScreenContent.kt | 5 +++-- .../app/login/internal/screens/AccessKeyScreenContent.kt | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/flipcash/features/backupkey/src/main/kotlin/com/flipcash/app/backupkey/internal/BackupKeyScreenContent.kt b/apps/flipcash/features/backupkey/src/main/kotlin/com/flipcash/app/backupkey/internal/BackupKeyScreenContent.kt index 8226304cb..641a70e01 100644 --- a/apps/flipcash/features/backupkey/src/main/kotlin/com/flipcash/app/backupkey/internal/BackupKeyScreenContent.kt +++ b/apps/flipcash/features/backupkey/src/main/kotlin/com/flipcash/app/backupkey/internal/BackupKeyScreenContent.kt @@ -25,6 +25,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -59,8 +60,8 @@ internal fun BackupKeyScreenContent(viewModel: BackupKeyScreenViewModel) { val resources = LocalResources.current val dataState by viewModel.uiFlow.collectAsStateWithLifecycle() - var isExportSeedRequested by remember { mutableStateOf(false) } - var isStoragePermissionGranted by remember { mutableStateOf(false) } + var isExportSeedRequested by rememberSaveable { mutableStateOf(false) } + var isStoragePermissionGranted by rememberSaveable { mutableStateOf(false) } val isAccessKeyVisible = remember { MutableTransitionState(false) } val onPermissionResult = { result: PermissionResult -> diff --git a/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/internal/screens/AccessKeyScreenContent.kt b/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/internal/screens/AccessKeyScreenContent.kt index 5f347c7ff..0c741b026 100644 --- a/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/internal/screens/AccessKeyScreenContent.kt +++ b/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/internal/screens/AccessKeyScreenContent.kt @@ -28,6 +28,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -78,8 +79,8 @@ internal fun AccessKeyScreen( val composeScope = rememberCoroutineScope() - var isExportSeedRequested by remember { mutableStateOf(false) } - var isStoragePermissionGranted by remember { mutableStateOf(false) } + var isExportSeedRequested by rememberSaveable { mutableStateOf(false) } + var isStoragePermissionGranted by rememberSaveable { mutableStateOf(false) } val onPermissionResult = { result: PermissionResult -> isStoragePermissionGranted = result == PermissionResult.Granted From 3a1f8a1036c700134b8ca7e7a17103233714d3ad Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 4 Jun 2026 10:38:17 -0400 Subject: [PATCH 09/11] fix(compose): use staticCompositionLocalOf for singleton controllers LocalAppUpdater and LocalCoinbaseOnRampController are provided once at app startup and never change. Using staticCompositionLocalOf avoids unnecessary recomposition tracking overhead. --- .../main/kotlin/com/flipcash/app/updates/LocalAppUpdater.kt | 4 ++-- .../kotlin/com/flipcash/app/onramp/CoinbaseOnRampLocal.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/flipcash/shared/appupdates/src/main/kotlin/com/flipcash/app/updates/LocalAppUpdater.kt b/apps/flipcash/shared/appupdates/src/main/kotlin/com/flipcash/app/updates/LocalAppUpdater.kt index 46a09b29e..59f9283f4 100644 --- a/apps/flipcash/shared/appupdates/src/main/kotlin/com/flipcash/app/updates/LocalAppUpdater.kt +++ b/apps/flipcash/shared/appupdates/src/main/kotlin/com/flipcash/app/updates/LocalAppUpdater.kt @@ -1,10 +1,10 @@ package com.flipcash.app.updates -import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.staticCompositionLocalOf import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -val LocalAppUpdater = compositionLocalOf { StubAppUpdateController() } +val LocalAppUpdater = staticCompositionLocalOf { StubAppUpdateController() } class StubAppUpdateController(updateInfo: UpdateInfo? = null): AppUpdateController { override val availableUpdate: StateFlow = MutableStateFlow(updateInfo) diff --git a/apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/CoinbaseOnRampLocal.kt b/apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/CoinbaseOnRampLocal.kt index 28ea59547..2490308fb 100644 --- a/apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/CoinbaseOnRampLocal.kt +++ b/apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/CoinbaseOnRampLocal.kt @@ -1,6 +1,6 @@ package com.flipcash.app.onramp -import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.staticCompositionLocalOf val LocalCoinbaseOnRampController = - compositionLocalOf { throw IllegalStateException() } + staticCompositionLocalOf { throw IllegalStateException() } From ae4c2ccfa9cbc3b254877757bf05a73d3d04f0be Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 4 Jun 2026 10:50:56 -0400 Subject: [PATCH 10/11] fix(compose): correct parameter ordering in shared components Reorder parameters to follow Compose API guidelines: required params first, then modifier, then optional params, then trailing lambdas. Also fix modifier chain ordering in Pill and CodeToggleSwitch so the caller modifier is applied outermost. --- .../src/main/kotlin/com/getcode/ui/components/Badge.kt | 2 +- .../src/main/kotlin/com/getcode/ui/components/Pill.kt | 5 ++--- .../kotlin/com/getcode/ui/components/SegmentedControl.kt | 6 +++--- .../kotlin/com/getcode/ui/theme/CodeSegmentedControl.kt | 4 ++-- .../src/main/kotlin/com/getcode/ui/theme/CodeSwitch.kt | 6 +++--- 5 files changed, 11 insertions(+), 12 deletions(-) diff --git a/ui/components/src/main/kotlin/com/getcode/ui/components/Badge.kt b/ui/components/src/main/kotlin/com/getcode/ui/components/Badge.kt index 878998cdb..d105c6e28 100644 --- a/ui/components/src/main/kotlin/com/getcode/ui/components/Badge.kt +++ b/ui/components/src/main/kotlin/com/getcode/ui/components/Badge.kt @@ -23,8 +23,8 @@ import com.getcode.theme.CodeTheme @Composable fun Badge( - modifier: Modifier = Modifier, count: Int, + modifier: Modifier = Modifier, showMoreUnread: Boolean = count > 99, color: Color = CodeTheme.colors.brand, contentColor: Color = Color.White, diff --git a/ui/components/src/main/kotlin/com/getcode/ui/components/Pill.kt b/ui/components/src/main/kotlin/com/getcode/ui/components/Pill.kt index a10b56465..fa3d3fe15 100644 --- a/ui/components/src/main/kotlin/com/getcode/ui/components/Pill.kt +++ b/ui/components/src/main/kotlin/com/getcode/ui/components/Pill.kt @@ -20,11 +20,11 @@ import com.getcode.theme.CodeTheme @Composable fun Pill( + text: String, modifier: Modifier = Modifier, backgroundColor: Color = Black50, contentColor: Color = Color.White, shape: CornerBasedShape = CircleShape, - text: String, textStyle: TextStyle = CodeTheme.typography.caption, contentPadding: PaddingValues = PaddingValues( horizontal = CodeTheme.dimens.grid.x2, @@ -60,11 +60,10 @@ fun Pill( content: @Composable () -> Unit, ) { Row( - modifier = Modifier + modifier = modifier .wrapContentSize() .clip(shape) .background(backgroundColor) - .then(modifier) .padding(contentPadding) ) { CompositionLocalProvider(LocalContentColor provides contentColor) { diff --git a/ui/components/src/main/kotlin/com/getcode/ui/components/SegmentedControl.kt b/ui/components/src/main/kotlin/com/getcode/ui/components/SegmentedControl.kt index 804c9b0d8..da4f15297 100644 --- a/ui/components/src/main/kotlin/com/getcode/ui/components/SegmentedControl.kt +++ b/ui/components/src/main/kotlin/com/getcode/ui/components/SegmentedControl.kt @@ -14,12 +14,12 @@ import androidx.compose.ui.unit.Dp fun SegmentedControl( options: List, selected: T?, + onSelectionChanged: (T) -> Unit, + modifier: Modifier = Modifier, colors: SegmentedButtonColors = SegmentedButtonDefaults.colors(), contentPadding: PaddingValues = SegmentedButtonDefaults.ContentPadding, - mapper: @Composable SingleChoiceSegmentedButtonRowScope.(T) -> Unit, space: Dp = SegmentedButtonDefaults.BorderWidth, - modifier: Modifier = Modifier, - onSelectionChanged: (T) -> Unit, + mapper: @Composable SingleChoiceSegmentedButtonRowScope.(T) -> Unit, ) { SingleChoiceSegmentedButtonRow( modifier = modifier, diff --git a/ui/components/src/main/kotlin/com/getcode/ui/theme/CodeSegmentedControl.kt b/ui/components/src/main/kotlin/com/getcode/ui/theme/CodeSegmentedControl.kt index 7e03d8927..e76df86e4 100644 --- a/ui/components/src/main/kotlin/com/getcode/ui/theme/CodeSegmentedControl.kt +++ b/ui/components/src/main/kotlin/com/getcode/ui/theme/CodeSegmentedControl.kt @@ -14,9 +14,9 @@ import com.getcode.ui.components.SegmentedControl fun CodeSegmentedControl( options: List, selected: T?, - mapper: @Composable SingleChoiceSegmentedButtonRowScope.(T) -> Unit, - modifier: Modifier = Modifier, onSelectionChanged: (T) -> Unit, + modifier: Modifier = Modifier, + mapper: @Composable SingleChoiceSegmentedButtonRowScope.(T) -> Unit, ) { SegmentedControl( options = options, diff --git a/ui/components/src/main/kotlin/com/getcode/ui/theme/CodeSwitch.kt b/ui/components/src/main/kotlin/com/getcode/ui/theme/CodeSwitch.kt index 30b772e94..f21fb3a05 100644 --- a/ui/components/src/main/kotlin/com/getcode/ui/theme/CodeSwitch.kt +++ b/ui/components/src/main/kotlin/com/getcode/ui/theme/CodeSwitch.kt @@ -40,9 +40,9 @@ object CodeToggleSwitchDefaults { @Composable fun CodeToggleSwitch( + checked: Boolean, modifier: Modifier = Modifier, enabled: Boolean = true, - checked: Boolean, onCheckedChange: ((Boolean) -> Unit)? = null, ) { val width = 51.dp @@ -62,7 +62,7 @@ fun CodeToggleSwitch( val trackColor by colors.trackColor(enabled = enabled, checked = checked) val thumbColor by colors.thumbColor(enabled = enabled, checked = checked) Canvas( - modifier = Modifier + modifier = modifier .size(width = width, height = height) .addIf(onCheckedChange != null) { Modifier.pointerInput(Unit) { @@ -72,7 +72,7 @@ fun CodeToggleSwitch( } ) } - }.then(modifier) + } ) { drawRoundRect( color = trackColor, From 7ad1fdf8e0808785e2b2442d7944a0643a329ea2 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 4 Jun 2026 10:51:11 -0400 Subject: [PATCH 11/11] fix(compose): add modifier parameter to ConnectionStatus Shared reusable component was missing a modifier parameter, preventing callers from controlling its layout. --- .../com/getcode/ui/components/ConnectionStatusComponent.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/components/src/main/kotlin/com/getcode/ui/components/ConnectionStatusComponent.kt b/ui/components/src/main/kotlin/com/getcode/ui/components/ConnectionStatusComponent.kt index 3ad26108f..20d932c5c 100644 --- a/ui/components/src/main/kotlin/com/getcode/ui/components/ConnectionStatusComponent.kt +++ b/ui/components/src/main/kotlin/com/getcode/ui/components/ConnectionStatusComponent.kt @@ -16,8 +16,8 @@ import com.getcode.utils.network.NetworkState import com.getcode.utils.network.connectivity.NetworkStateProvider @Composable -fun ConnectionStatus(state: NetworkState) { - Row { +fun ConnectionStatus(state: NetworkState, modifier: Modifier = Modifier) { + Row(modifier = modifier) { when { !state.connected && state.type != ConnectionType.Unknown -> { CodeCircularProgressIndicator(