From 2fff9effac8edcab129d511371d19adcead5be56 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Mon, 6 Jul 2026 12:01:21 -0400 Subject: [PATCH] fix(send): sync contacts on in-list grant and fix on-Flipcash scroll offset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Keep the contacts permission gate visible until the full sync completes (via ContactSyncComplete) instead of advancing the moment device contacts land. Advancing early rendered the list before on-Flipcash contacts were fetched, then prepended them above the viewport once they arrived — pushing them offscreen so the user had to scroll up. - Dispatch ContactsGranted when the user allows access from the in-list rationale card so the contact sync fires, mirroring the gate. - Hoist ContactAccessHandle out of ContactList; the screen now owns the single handle and wires both Granted (sync) and Picked (add contacts). Signed-off-by: Brandon McAnsh --- .../internal/screens/ContactListScreen.kt | 13 ++++++++++++- .../screens/ContactsPermissionGateScreen.kt | 10 +++++++++- .../internal/screens/components/ContactList.kt | 10 ++++------ 3 files changed, 25 insertions(+), 8 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 7b6615342..a4753ea1d 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 @@ -20,6 +20,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.flipcash.app.analytics.Button import com.flipcash.app.core.AppRoute import com.flipcash.app.core.send.SendResult import com.flipcash.app.core.send.SendStep @@ -28,6 +29,7 @@ import com.flipcash.app.directsend.internal.screens.components.ContactList import com.flipcash.app.permissions.ContactAccessResult import com.flipcash.app.permissions.rememberContactAccessHandle import com.flipcash.features.directsend.R +import com.getcode.libs.analytics.LocalAnalytics import com.getcode.navigation.flow.LocalOuterCodeNavigator import com.getcode.navigation.flow.flowSharedViewModel import com.getcode.navigation.flow.rememberFlowNavigator @@ -47,6 +49,7 @@ internal fun ContactListScreen() { val flowNavigator = rememberFlowNavigator() val viewModel = flowSharedViewModel() val navigator = LocalOuterCodeNavigator.current + val analytics = LocalAnalytics.current val state by viewModel.stateFlow.collectAsStateWithLifecycle() @@ -72,6 +75,14 @@ internal fun ContactListScreen() { isPickerMode = state.isPickerMode, ) { result -> when (result) { + ContactAccessResult.Granted -> { + // Granting via the in-list rationale card kicks off the same contact + // sync the permission gate does; otherwise the newly-allowed contacts + // are never fetched. + analytics.action(Button.AllowContacts) + viewModel.dispatchEvent(SendFlowViewModel.Event.ContactsGranted) + } + is ContactAccessResult.Picked -> { viewModel.dispatchEvent(SendFlowViewModel.Event.ContactsPicked(result.contacts)) } @@ -161,8 +172,8 @@ internal fun ContactListScreen() { items = state.listItems, searchState = state.searchState, listState = listState, + accessHandle = accessHandle, isPickerMode = state.isPickerMode, - onAddMoreContacts = { accessHandle.launch() }, onItemClick = { contact -> viewModel.dispatchEvent(SendFlowViewModel.Event.OnContactClicked(contact)) }, 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 15f5ded9b..785c2e885 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 @@ -78,8 +78,16 @@ internal fun ContactsPermissionGateScreen() { // If this step has been removed from the flow (e.g. chats arrived while we're // on this screen, or on re-open when chats already exist), advance immediately. // FlowNavigator.proceed() handles removed steps by replacing with the first remaining step. + // + // Exception: while a sync is actively running (the user just granted access), + // the device contacts land first and remove this gate from the steps, but the + // on-Flipcash contacts are fetched in a second pass. If we advanced now, the list + // would render with only the "not on Flipcash" section, then prepend the + // on-Flipcash contacts above the viewport once they arrive — pushing them + // offscreen. Let ContactSyncComplete drive navigation instead so the list is + // fully formed before it's shown. val gateStillInSteps = state.steps.any { it is SendStep.ContactsGate } - if (!gateStillInSteps) { + if (!gateStillInSteps && !state.contactSyncState.loading) { LaunchedEffect(Unit) { flowNavigator.proceed() } return } diff --git a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/components/ContactList.kt b/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/components/ContactList.kt index b25cc1b49..d09a55218 100644 --- a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/components/ContactList.kt +++ b/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/components/ContactList.kt @@ -48,7 +48,7 @@ import com.flipcash.app.contacts.ui.ContactAvatar import com.flipcash.app.core.android.extensions.launchAppSettings import com.flipcash.app.core.contacts.DeviceContact import com.flipcash.app.directsend.internal.ContactListItem -import com.flipcash.app.permissions.rememberContactAccessHandle +import com.flipcash.app.permissions.ContactAccessHandle import com.flipcash.app.theme.FlipcashThemeWrapper import com.flipcash.features.directsend.R import com.flipcash.shared.chat.ui.AnimatedConversationPaymentsPreview @@ -65,16 +65,13 @@ internal fun ContactList( items: List, searchState: TextFieldState, listState: LazyListState, + accessHandle: ContactAccessHandle, modifier: Modifier = Modifier, isPickerMode: Boolean = false, - onAddMoreContacts: () -> Unit = {}, onItemClick: (ContactListItem.ContactRow) -> Unit = {}, onItemDismissed: (ContactListItem.ContactRow) -> Unit = {}, ) { val context = LocalContext.current - val accessHandle = rememberContactAccessHandle( - isPickerMode = isPickerMode, - ) LazyColumn( modifier = Modifier @@ -188,7 +185,7 @@ internal fun ContactList( if (isPickerMode) { item { PickerModeHeader( - onAddMoreContacts = onAddMoreContacts, + onAddMoreContacts = { accessHandle.launch() }, ) } } @@ -563,5 +560,6 @@ private fun ContactListPreview() { items = items, searchState = TextFieldState(), listState = rememberLazyListState(), + accessHandle = ContactAccessHandle(launch = {}), ) } \ No newline at end of file