From 2436bd1871db120cb65cb5cd108b91779e09b5e3 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 4 Sep 2026 12:37:46 -0400 Subject: [PATCH 1/2] chore(direct-send): delete the module left behind by the send-tab removal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #1182 removed the send tab and the `AppRoute.Sheets.Send` route it opened, dropped `:apps:flipcash:features:direct-send` from `settings.gradle.kts` and the app's dependencies, and deleted the two maestro flows that drove it — but left the module's sources in the tree. They have sat there uncompiled for 240 commits, and `PhoneGateLandingScreen` still navigates to the deleted route, so re-including the module would not configure. Nothing outside the module referenced `com.flipcash.app.directsend`, and the `SendStep`/`SendResult` flow contract in `:apps:flipcash:core` had no other consumer, so both go with it. AndroidManifest, AppRouter and NotificationService keep their comments explaining why chat deeplinks are deliberately unrouted — that reasoning outlives the module. The baseline profile still carried 315 rules for `com/flipcash/app/directsend` classes that no longer ship; it has not been regenerated since #1029. --- .../baselineProfiles/baseline-prof.txt | 315 --------- .../com/flipcash/app/core/send/SendStep.kt | 32 - apps/flipcash/features/direct-send/.gitignore | 2 - .../features/direct-send/build.gradle.kts | 30 - .../flipcash/app/directsend/SendFlowScreen.kt | 70 -- .../directsend/internal/ContactListBuilder.kt | 142 ---- .../directsend/internal/ContactListItem.kt | 28 - .../directsend/internal/SendFlowViewModel.kt | 306 --------- .../internal/screens/ContactListScreen.kt | 221 ------ .../screens/ContactsPermissionGateScreen.kt | 130 ---- .../screens/PhoneGateLandingScreen.kt | 116 ---- .../screens/components/ContactList.kt | 650 ------------------ .../components/ContactPermissionCallout.kt | 102 --- .../internal/SendFlowViewModelStateTest.kt | 63 -- docs/architecture/features/README.md | 5 +- 15 files changed, 2 insertions(+), 2210 deletions(-) delete mode 100644 apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/send/SendStep.kt delete mode 100644 apps/flipcash/features/direct-send/.gitignore delete mode 100644 apps/flipcash/features/direct-send/build.gradle.kts delete mode 100644 apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/SendFlowScreen.kt delete mode 100644 apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/ContactListBuilder.kt delete mode 100644 apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/ContactListItem.kt delete mode 100644 apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/SendFlowViewModel.kt delete mode 100644 apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/ContactListScreen.kt delete mode 100644 apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/ContactsPermissionGateScreen.kt delete mode 100644 apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/PhoneGateLandingScreen.kt delete mode 100644 apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/components/ContactList.kt delete mode 100644 apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/components/ContactPermissionCallout.kt delete mode 100644 apps/flipcash/features/direct-send/src/test/kotlin/com/flipcash/app/directsend/internal/SendFlowViewModelStateTest.kt diff --git a/apps/flipcash/app/src/release/generated/baselineProfiles/baseline-prof.txt b/apps/flipcash/app/src/release/generated/baselineProfiles/baseline-prof.txt index 6ac6bd44bf..4321f4367a 100644 --- a/apps/flipcash/app/src/release/generated/baselineProfiles/baseline-prof.txt +++ b/apps/flipcash/app/src/release/generated/baselineProfiles/baseline-prof.txt @@ -36876,321 +36876,6 @@ Lcom/flipcash/app/devicelogs/internal/DeviceLogsScreenViewModel_HiltModules_Bind SPLcom/flipcash/app/devicelogs/internal/DeviceLogsScreenViewModel_HiltModules_BindsModule_Binds_LazyMapKey;->()V Lcom/flipcash/app/devicelogs/internal/DeviceLogsScreenViewModel_HiltModules_KeyModule_Provide_LazyMapKey; SPLcom/flipcash/app/devicelogs/internal/DeviceLogsScreenViewModel_HiltModules_KeyModule_Provide_LazyMapKey;->()V -PLcom/flipcash/app/directsend/ComposableSingletons$SendFlowScreenKt;->()V -PLcom/flipcash/app/directsend/ComposableSingletons$SendFlowScreenKt;->()V -PLcom/flipcash/app/directsend/ComposableSingletons$SendFlowScreenKt;->getLambda$-737075343$direct_send()Lkotlin/jvm/functions/Function3; -PLcom/flipcash/app/directsend/ComposableSingletons$SendFlowScreenKt;->getLambda$1245613224$direct_send()Lkotlin/jvm/functions/Function3; -PLcom/flipcash/app/directsend/ComposableSingletons$SendFlowScreenKt;->getLambda$445622165$direct_send()Lkotlin/jvm/functions/Function3; -PLcom/flipcash/app/directsend/ComposableSingletons$SendFlowScreenKt;->lambda_1245613224$lambda$0(Lcom/flipcash/app/core/send/SendStep$ContactList;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit; -PLcom/flipcash/app/directsend/ComposableSingletons$SendFlowScreenKt$$ExternalSyntheticLambda0;->()V -PLcom/flipcash/app/directsend/ComposableSingletons$SendFlowScreenKt$$ExternalSyntheticLambda1;->()V -PLcom/flipcash/app/directsend/ComposableSingletons$SendFlowScreenKt$$ExternalSyntheticLambda2;->()V -PLcom/flipcash/app/directsend/ComposableSingletons$SendFlowScreenKt$$ExternalSyntheticLambda2;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/SendFlowScreenKt;->SendFlowScreen$lambda$0(Landroidx/compose/runtime/State;)Lcom/flipcash/app/directsend/internal/SendFlowViewModel$State; -PLcom/flipcash/app/directsend/SendFlowScreenKt;->SendFlowScreen$lambda$2(Landroidx/compose/runtime/snapshots/SnapshotStateMap;ILandroidx/compose/runtime/Composer;I)Lkotlin/Unit; -PLcom/flipcash/app/directsend/SendFlowScreenKt;->SendFlowScreen(Landroidx/compose/runtime/snapshots/SnapshotStateMap;Landroidx/compose/runtime/Composer;I)V -PLcom/flipcash/app/directsend/SendFlowScreenKt;->SyncStep(Lcom/flipcash/app/core/send/SendStep;Landroidx/compose/runtime/Composer;I)V -PLcom/flipcash/app/directsend/SendFlowScreenKt;->access$SyncStep(Lcom/flipcash/app/core/send/SendStep;Landroidx/compose/runtime/Composer;I)V -PLcom/flipcash/app/directsend/SendFlowScreenKt;->sendEntryProvider(Landroidx/compose/runtime/Composer;I)Lkotlin/jvm/functions/Function1; -PLcom/flipcash/app/directsend/SendFlowScreenKt$$ExternalSyntheticLambda2;->(Lcom/getcode/ui/utils/KeyboardController;Lkotlin/jvm/functions/Function0;)V -PLcom/flipcash/app/directsend/SendFlowScreenKt$$ExternalSyntheticLambda3;->(Landroidx/compose/runtime/snapshots/SnapshotStateMap;I)V -PLcom/flipcash/app/directsend/SendFlowScreenKt$$ExternalSyntheticLambda3;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/SendFlowScreenKt$SyncStep$1$1;->(Lcom/flipcash/app/directsend/internal/SendFlowViewModel;Lcom/flipcash/app/core/send/SendStep;Lkotlin/coroutines/Continuation;)V -PLcom/flipcash/app/directsend/SendFlowScreenKt$SyncStep$1$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; -PLcom/flipcash/app/directsend/SendFlowScreenKt$SyncStep$1$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/SendFlowScreenKt$sendEntryProvider$$inlined$entryProvider$default$1;->()V -PLcom/flipcash/app/directsend/SendFlowScreenKt$sendEntryProvider$$inlined$entryProvider$default$1;->()V -PLcom/flipcash/app/directsend/SendFlowScreenKt$sendEntryProvider$lambda$0$$inlined$annotatedEntry$1;->()V -PLcom/flipcash/app/directsend/SendFlowScreenKt$sendEntryProvider$lambda$0$$inlined$annotatedEntry$1;->()V -PLcom/flipcash/app/directsend/SendFlowScreenKt$sendEntryProvider$lambda$0$$inlined$annotatedEntry$2;->(Ljava/util/Map;)V -PLcom/flipcash/app/directsend/SendFlowScreenKt$sendEntryProvider$lambda$0$$inlined$annotatedEntry$3;->()V -PLcom/flipcash/app/directsend/SendFlowScreenKt$sendEntryProvider$lambda$0$$inlined$annotatedEntry$3;->()V -PLcom/flipcash/app/directsend/SendFlowScreenKt$sendEntryProvider$lambda$0$$inlined$annotatedEntry$4;->(Ljava/util/Map;)V -PLcom/flipcash/app/directsend/SendFlowScreenKt$sendEntryProvider$lambda$0$$inlined$annotatedEntry$5;->()V -PLcom/flipcash/app/directsend/SendFlowScreenKt$sendEntryProvider$lambda$0$$inlined$annotatedEntry$5;->()V -PLcom/flipcash/app/directsend/SendFlowScreenKt$sendEntryProvider$lambda$0$$inlined$annotatedEntry$5;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/SendFlowScreenKt$sendEntryProvider$lambda$0$$inlined$annotatedEntry$6;->(Ljava/util/Map;)V -PLcom/flipcash/app/directsend/SendFlowScreenKt$sendEntryProvider$lambda$0$$inlined$annotatedEntry$6;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/SendFlowScreenKt$sendEntryProvider$lambda$0$$inlined$annotatedEntry$6;->invoke(Ljava/lang/Object;)Ljava/util/Map; -PLcom/flipcash/app/directsend/internal/ContactListItem$ContactRow;->()V -PLcom/flipcash/app/directsend/internal/ContactListItem$ContactRow;->(Lcom/flipcash/app/core/contacts/DeviceContact;ZLjava/lang/String;ILcom/flipcash/services/models/chat/ChatId;Lkotlin/time/Instant;)V -PLcom/flipcash/app/directsend/internal/ContactListItem$ContactRow;->component1()Lcom/flipcash/app/core/contacts/DeviceContact; -PLcom/flipcash/app/directsend/internal/ContactListItem$ContactRow;->component2()Z -PLcom/flipcash/app/directsend/internal/ContactListItem$ContactRow;->equals(Ljava/lang/Object;)Z -PLcom/flipcash/app/directsend/internal/ContactListItem$ContactRow;->getChatId()Lcom/flipcash/services/models/chat/ChatId; -PLcom/flipcash/app/directsend/internal/ContactListItem$ContactRow;->getContact()Lcom/flipcash/app/core/contacts/DeviceContact; -PLcom/flipcash/app/directsend/internal/ContactListItem$ContactRow;->getLastActivity()Lkotlin/time/Instant; -PLcom/flipcash/app/directsend/internal/ContactListItem$ContactRow;->getLastMessagePreview()Ljava/lang/String; -PLcom/flipcash/app/directsend/internal/ContactListItem$ContactRow;->getUnreadCount()I -PLcom/flipcash/app/directsend/internal/ContactListItem$ContactRow;->isOnFlipcash()Z -PLcom/flipcash/app/directsend/internal/SendFlowViewModel;->$r8$lambda$Jp1vvnQwXUiLWjh2gY1cIM-3Ps4(Lcom/flipcash/app/directsend/internal/SendFlowViewModel$Event;Lcom/flipcash/app/directsend/internal/SendFlowViewModel$State;)Lcom/flipcash/app/directsend/internal/SendFlowViewModel$State; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel;->$r8$lambda$UgvvjS_lpZOrA-N1NjVopWBJw4Y(Lcom/flipcash/app/directsend/internal/SendFlowViewModel$Event;Lcom/flipcash/app/directsend/internal/SendFlowViewModel$State;)Lcom/flipcash/app/directsend/internal/SendFlowViewModel$State; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel;->$r8$lambda$d3uIFGlnRrNu3E5Yrg54Hm6PVpo(Lcom/flipcash/app/directsend/internal/SendFlowViewModel$Event;Lcom/flipcash/app/directsend/internal/SendFlowViewModel$State;)Lcom/flipcash/app/directsend/internal/SendFlowViewModel$State; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel;->$r8$lambda$dR0WB_3Ml-ggo5Vizam41ldx57A(Ljava/util/List;Ljava/lang/String;Lcom/flipcash/services/models/chat/ChatMember;)Z -PLcom/flipcash/app/directsend/internal/SendFlowViewModel;->$r8$lambda$tYMqlzmKxOIs3X8VJk4SwM_y5d0(Lcom/flipcash/app/directsend/internal/SendFlowViewModel$State;)Lcom/flipcash/app/directsend/internal/SendFlowViewModel$State; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel;->$r8$lambda$twi43gGAmzOTjspRrYUOS7_HYM4(Lcom/flipcash/app/directsend/internal/SendFlowViewModel$State;)Lcom/flipcash/app/directsend/internal/SendFlowViewModel$State; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel;->()V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel;->(Lcom/flipcash/services/user/UserManager;Lcom/flipcash/app/featureflags/FeatureFlagController;Lcom/flipcash/app/contacts/ContactCoordinator;Lcom/flipcash/shared/chat/ChatCoordinator;Lcom/flipcash/app/tokens/TokenCoordinator;Lcom/flipcash/app/phone/PhoneUtils;Lcom/getcode/util/resources/ResourceHelper;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel;->access$generateListItems(Lcom/flipcash/app/directsend/internal/SendFlowViewModel;Lcom/flipcash/app/contacts/ContactCoordinator$ContactState;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;)Ljava/util/List; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel;->formatPreview(Lcom/flipcash/shared/chat/ChatSummary;Ljava/util/List;Ljava/util/Map;)Ljava/lang/String; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel;->generateListItems$lambda$0$3$3(Ljava/util/List;Ljava/lang/String;Lcom/flipcash/services/models/chat/ChatMember;)Z -HPLcom/flipcash/app/directsend/internal/SendFlowViewModel;->generateListItems(Lcom/flipcash/app/contacts/ContactCoordinator$ContactState;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;)Ljava/util/List; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel;->updateStateForEvent$lambda$0$0(Lcom/flipcash/app/directsend/internal/SendFlowViewModel$Event;Lcom/flipcash/app/directsend/internal/SendFlowViewModel$State;)Lcom/flipcash/app/directsend/internal/SendFlowViewModel$State; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel;->updateStateForEvent$lambda$0$1(Lcom/flipcash/app/directsend/internal/SendFlowViewModel$Event;Lcom/flipcash/app/directsend/internal/SendFlowViewModel$State;)Lcom/flipcash/app/directsend/internal/SendFlowViewModel$State; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel;->updateStateForEvent$lambda$0$10(Lcom/flipcash/app/directsend/internal/SendFlowViewModel$State;)Lcom/flipcash/app/directsend/internal/SendFlowViewModel$State; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel;->updateStateForEvent$lambda$0$7(Lcom/flipcash/app/directsend/internal/SendFlowViewModel$Event;Lcom/flipcash/app/directsend/internal/SendFlowViewModel$State;)Lcom/flipcash/app/directsend/internal/SendFlowViewModel$State; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel;->updateStateForEvent$lambda$0$8(Lcom/flipcash/app/directsend/internal/SendFlowViewModel$State;)Lcom/flipcash/app/directsend/internal/SendFlowViewModel$State; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel;->updateStateForEvent$lambda$0(Lcom/flipcash/app/directsend/internal/SendFlowViewModel$Event;)Lkotlin/jvm/functions/Function1; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$$ExternalSyntheticLambda0;->(Lcom/flipcash/app/directsend/internal/SendFlowViewModel$Event;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$$ExternalSyntheticLambda0;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$$ExternalSyntheticLambda1;->()V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$$ExternalSyntheticLambda1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$$ExternalSyntheticLambda13;->(Lcom/flipcash/app/directsend/internal/SendFlowViewModel$Event;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$$ExternalSyntheticLambda13;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$$ExternalSyntheticLambda3;->()V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$$ExternalSyntheticLambda3;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$$ExternalSyntheticLambda4;->(Ljava/util/List;Ljava/lang/String;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$$ExternalSyntheticLambda4;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$$ExternalSyntheticLambda5;->()V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$$ExternalSyntheticLambda5;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$$ExternalSyntheticLambda7;->(Lcom/flipcash/app/directsend/internal/SendFlowViewModel$Event;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$$ExternalSyntheticLambda7;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$1;->(Lkotlin/coroutines/Continuation;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$1;->invoke(Lcom/flipcash/services/user/UserManager$State;ZZLcom/flipcash/app/contacts/ContactCoordinator$ContactState;Ljava/util/List;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$11;->(Lcom/flipcash/app/directsend/internal/SendFlowViewModel;Lkotlin/coroutines/Continuation;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$11;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$11;->invoke(Lcom/flipcash/app/directsend/internal/ContactListItem$ContactRow;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$11;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$11;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$12;->(Lcom/flipcash/app/directsend/internal/SendFlowViewModel;Lkotlin/coroutines/Continuation;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$13;->(Lcom/flipcash/app/directsend/internal/SendFlowViewModel;Lkotlin/coroutines/Continuation;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$15;->(Lkotlin/coroutines/Continuation;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$15;->invoke(Lcom/flipcash/app/contacts/ContactCoordinator$ContactState;Lcom/flipcash/app/core/send/SendStep;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$15;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$15;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$18;->(Lcom/flipcash/app/directsend/internal/SendFlowViewModel;Lkotlin/coroutines/Continuation;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$2;->(Lcom/flipcash/app/directsend/internal/SendFlowViewModel;Lkotlin/coroutines/Continuation;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$2;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$2;->invoke(Lcom/flipcash/app/directsend/internal/SendFlowViewModel$Event$StepsUpdated;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$2;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$2;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$4$1;->(Landroidx/compose/foundation/text/input/TextFieldState;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$4$1;->invoke()Ljava/lang/CharSequence; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$4$1;->invoke()Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$5;->(Lcom/flipcash/app/directsend/internal/SendFlowViewModel;Lkotlin/coroutines/Continuation;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$5;->invoke(Lcom/flipcash/app/contacts/ContactCoordinator$ContactState;Ljava/lang/CharSequence;Ljava/util/List;Ljava/util/List;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$5;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$5;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$6;->(Lcom/flipcash/app/directsend/internal/SendFlowViewModel;Lkotlin/coroutines/Continuation;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$6;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$6;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$6;->invoke(Ljava/util/List;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$6;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$7;->(Lcom/flipcash/app/directsend/internal/SendFlowViewModel;Lkotlin/coroutines/Continuation;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$9;->(Lcom/flipcash/app/directsend/internal/SendFlowViewModel;Lkotlin/coroutines/Continuation;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$Companion;->()V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$Event$NavigateToChat;->()V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$Event$NavigateToChat;->(Lcom/flipcash/app/core/chat/ChatIdentifier;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$Event$NavigateToChat;->getIdentifier()Lcom/flipcash/app/core/chat/ChatIdentifier; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$Event$OnContactClicked;->()V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$Event$OnContactClicked;->(Lcom/flipcash/app/directsend/internal/ContactListItem$ContactRow;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$Event$OnContactClicked;->getContact()Lcom/flipcash/app/directsend/internal/ContactListItem$ContactRow; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$Event$OnItemsPopulated;->()V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$Event$OnItemsPopulated;->(Ljava/util/List;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$Event$OnItemsPopulated;->getItems()Ljava/util/List; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$Event$OnStepChanged;->()V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$Event$OnStepChanged;->(Lcom/flipcash/app/core/send/SendStep;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$Event$OnStepChanged;->getStep()Lcom/flipcash/app/core/send/SendStep; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$Event$StepsUpdated;->()V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$Event$StepsUpdated;->(Ljava/util/List;Z)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$Event$StepsUpdated;->getSteps()Ljava/util/List; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$Event$StepsUpdated;->isPickerMode()Z -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$State;->()V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$State;->(Ljava/util/List;Lcom/flipcash/app/core/send/SendStep;Landroidx/compose/foundation/text/input/TextFieldState;ZLcom/getcode/view/LoadingSuccessState;Ljava/util/List;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$State;->(Ljava/util/List;Lcom/flipcash/app/core/send/SendStep;Landroidx/compose/foundation/text/input/TextFieldState;ZLcom/getcode/view/LoadingSuccessState;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$State;->copy$default(Lcom/flipcash/app/directsend/internal/SendFlowViewModel$State;Ljava/util/List;Lcom/flipcash/app/core/send/SendStep;Landroidx/compose/foundation/text/input/TextFieldState;ZLcom/getcode/view/LoadingSuccessState;Ljava/util/List;ILjava/lang/Object;)Lcom/flipcash/app/directsend/internal/SendFlowViewModel$State; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$State;->copy(Ljava/util/List;Lcom/flipcash/app/core/send/SendStep;Landroidx/compose/foundation/text/input/TextFieldState;ZLcom/getcode/view/LoadingSuccessState;Ljava/util/List;)Lcom/flipcash/app/directsend/internal/SendFlowViewModel$State; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$State;->equals(Ljava/lang/Object;)Z -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$State;->getCurrentStep()Lcom/flipcash/app/core/send/SendStep; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$State;->getListItems()Ljava/util/List; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$State;->getSearchState()Landroidx/compose/foundation/text/input/TextFieldState; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$State;->getSteps()Ljava/util/List; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$State;->isPickerMode()Z -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$generateListItems$lambda$0$$inlined$compareBy$1;->(Ljava/util/Comparator;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$generateListItems$lambda$0$$inlined$compareByDescending$1;->()V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$generateListItems$lambda$0$$inlined$compareByDescending$1;->compare(Ljava/lang/Object;Ljava/lang/Object;)I -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$generateListItems$lambda$0$$inlined$thenBy$1;->(Ljava/util/Comparator;Ljava/util/Comparator;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$generateListItems$lambda$0$$inlined$thenBy$1;->compare(Ljava/lang/Object;Ljava/lang/Object;)I -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$filter$1;->(Lkotlinx/coroutines/flow/Flow;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$filter$1;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$filter$1$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$filter$1$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$filter$1$2$1;->(Lcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$filter$1$2;Lkotlin/coroutines/Continuation;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$filter$2;->(Lkotlinx/coroutines/flow/Flow;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$filter$2;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$filter$2$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$filterIsInstance$1;->(Lkotlinx/coroutines/flow/Flow;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$filterIsInstance$1;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$filterIsInstance$1$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$filterIsInstance$1$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$filterIsInstance$1$2$1;->(Lcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$filterIsInstance$1$2;Lkotlin/coroutines/Continuation;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$filterIsInstance$2;->(Lkotlinx/coroutines/flow/Flow;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$filterIsInstance$2;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$filterIsInstance$2$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$filterIsInstance$2$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$filterIsInstance$2$2$1;->(Lcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$filterIsInstance$2$2;Lkotlin/coroutines/Continuation;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$filterIsInstance$3;->(Lkotlinx/coroutines/flow/Flow;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$filterIsInstance$3;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$filterIsInstance$3$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$filterIsInstance$3$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$filterIsInstance$3$2$1;->(Lcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$filterIsInstance$3$2;Lkotlin/coroutines/Continuation;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$filterIsInstance$4;->(Lkotlinx/coroutines/flow/Flow;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$filterIsInstance$4;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$filterIsInstance$4$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$filterIsInstance$4$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$filterIsInstance$4$2$1;->(Lcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$filterIsInstance$4$2;Lkotlin/coroutines/Continuation;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$filterIsInstance$5;->(Lkotlinx/coroutines/flow/Flow;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$filterIsInstance$5;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$filterIsInstance$5$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$filterIsInstance$5$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$filterIsInstance$5$2$1;->(Lcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$filterIsInstance$5$2;Lkotlin/coroutines/Continuation;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$flatMapLatest$1;->(Lkotlin/coroutines/Continuation;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$flatMapLatest$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$flatMapLatest$1;->invoke(Lkotlinx/coroutines/flow/FlowCollector;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$flatMapLatest$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$map$1;->(Lkotlinx/coroutines/flow/Flow;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$map$1;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$map$1$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$map$1$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$map$1$2$1;->(Lcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$map$1$2;Lkotlin/coroutines/Continuation;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$map$2;->(Lkotlinx/coroutines/flow/Flow;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$map$2;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$map$2$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$map$3;->(Lkotlinx/coroutines/flow/Flow;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$map$3;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$map$3$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$map$3$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$map$3$2$1;->(Lcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$map$3$2;Lkotlin/coroutines/Continuation;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$map$4;->(Lkotlinx/coroutines/flow/Flow;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$map$4;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$map$4$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$map$4$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$map$4$2$1;->(Lcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$map$4$2;Lkotlin/coroutines/Continuation;)V -PLcom/flipcash/app/directsend/internal/SendFlowViewModel$special$$inlined$map$4$2$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; -Lcom/flipcash/app/directsend/internal/SendFlowViewModel_HiltModules$KeyModule; -SPLcom/flipcash/app/directsend/internal/SendFlowViewModel_HiltModules$KeyModule;->provide()Z -Lcom/flipcash/app/directsend/internal/SendFlowViewModel_HiltModules_BindsModule_Binds_LazyMapKey; -SPLcom/flipcash/app/directsend/internal/SendFlowViewModel_HiltModules_BindsModule_Binds_LazyMapKey;->()V -Lcom/flipcash/app/directsend/internal/SendFlowViewModel_HiltModules_KeyModule_Provide_LazyMapKey; -SPLcom/flipcash/app/directsend/internal/SendFlowViewModel_HiltModules_KeyModule_Provide_LazyMapKey;->()V -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt;->$r8$lambda$41G0-1p8T6zv3BuuwyIBmSrsPeU(Lcom/getcode/navigation/flow/FlowNavigator;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit; -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt;->$r8$lambda$HSuOycTAEXR-rmoY3TNL_U6iOGk(Landroidx/compose/runtime/State;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit; -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt;->$r8$lambda$HeHtwu4wJhGN86LwXkE2xxA_EDM(Lcom/flipcash/app/directsend/internal/SendFlowViewModel;Lcom/flipcash/app/directsend/internal/ContactListItem$ContactRow;)Lkotlin/Unit; -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt;->$r8$lambda$VAgeB501E5PU04ntsgSFbARLTGY(Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/DisposableEffectScope;)Landroidx/compose/runtime/DisposableEffectResult; -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt;->$r8$lambda$mKQLdcPEZ7Hpl25E5HGEv9DXJF4(Lkotlin/jvm/functions/Function1;Z)Lkotlin/Unit; -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt;->$r8$lambda$pVIgszkIbQf-WfpJL2KSQnvGvDg(Landroidx/compose/foundation/lazy/LazyListState;Lcom/flipcash/app/permissions/ContactAccessHandle;Lcom/flipcash/app/directsend/internal/SendFlowViewModel;Landroidx/compose/runtime/State;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit; -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt;->ContactListScreen$lambda$0(Landroidx/compose/runtime/State;)Lcom/flipcash/app/directsend/internal/SendFlowViewModel$State; -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt;->ContactListScreen$lambda$4$0(Lcom/getcode/navigation/flow/FlowNavigator;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit; -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt;->ContactListScreen$lambda$4(Lcom/getcode/navigation/flow/FlowNavigator;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit; -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt;->ContactListScreen$lambda$5$0$0(Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/DisposableEffectScope;)Landroidx/compose/runtime/DisposableEffectResult; -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt;->ContactListScreen$lambda$5$2(Landroidx/compose/runtime/State;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit; -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt;->ContactListScreen$lambda$5$3$0(Lkotlin/jvm/functions/Function1;Z)Lkotlin/Unit; -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt;->ContactListScreen$lambda$5$4$1$0(Lcom/flipcash/app/directsend/internal/SendFlowViewModel;Lcom/flipcash/app/directsend/internal/ContactListItem$ContactRow;)Lkotlin/Unit; -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt;->ContactListScreen$lambda$5$4(Landroidx/compose/foundation/lazy/LazyListState;Lcom/flipcash/app/permissions/ContactAccessHandle;Lcom/flipcash/app/directsend/internal/SendFlowViewModel;Landroidx/compose/runtime/State;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit; -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt;->ContactListScreen$lambda$5(Landroidx/compose/runtime/State;Lcom/flipcash/app/permissions/ContactAccessHandle;Lcom/flipcash/app/directsend/internal/SendFlowViewModel;Landroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit; -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt;->ContactListScreen$lambda$6(ILandroidx/compose/runtime/Composer;I)Lkotlin/Unit; -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt;->ContactListScreen(Landroidx/compose/runtime/Composer;I)V -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$$ExternalSyntheticLambda0;->(Lcom/flipcash/app/directsend/internal/SendFlowViewModel;)V -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$$ExternalSyntheticLambda1;->(Landroidx/compose/runtime/State;)V -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$$ExternalSyntheticLambda1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$$ExternalSyntheticLambda10;->(Lcom/flipcash/app/directsend/internal/SendFlowViewModel;)V -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$$ExternalSyntheticLambda11;->(Lcom/getcode/navigation/flow/FlowNavigator;)V -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$$ExternalSyntheticLambda11;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$$ExternalSyntheticLambda12;->(Lkotlin/jvm/functions/Function1;)V -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$$ExternalSyntheticLambda12;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$$ExternalSyntheticLambda2;->(Lkotlin/jvm/functions/Function1;)V -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$$ExternalSyntheticLambda2;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$$ExternalSyntheticLambda3;->(Landroidx/compose/foundation/lazy/LazyListState;Lcom/flipcash/app/permissions/ContactAccessHandle;Lcom/flipcash/app/directsend/internal/SendFlowViewModel;Landroidx/compose/runtime/State;)V -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$$ExternalSyntheticLambda3;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$$ExternalSyntheticLambda4;->(Lcom/getcode/navigation/flow/FlowNavigator;)V -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$$ExternalSyntheticLambda4;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$$ExternalSyntheticLambda5;->(Landroidx/compose/runtime/State;Lcom/flipcash/app/permissions/ContactAccessHandle;Lcom/flipcash/app/directsend/internal/SendFlowViewModel;)V -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$$ExternalSyntheticLambda5;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$$ExternalSyntheticLambda6;->(I)V -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$$ExternalSyntheticLambda6;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$$ExternalSyntheticLambda7;->(Lcom/getcode/navigation/flow/FlowNavigator;)V -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$$ExternalSyntheticLambda8;->(Lcom/flipcash/app/permissions/ContactAccessHandle;)V -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$$ExternalSyntheticLambda9;->(Lcom/flipcash/app/directsend/internal/SendFlowViewModel;)V -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$$ExternalSyntheticLambda9;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$ContactListScreen$1$1;->(Lcom/flipcash/app/directsend/internal/SendFlowViewModel;Lcom/getcode/navigation/core/CodeNavigator;Lkotlin/coroutines/Continuation;)V -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$ContactListScreen$1$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$ContactListScreen$1$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$ContactListScreen$1$1$1;->(Lcom/getcode/navigation/core/CodeNavigator;)V -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$ContactListScreen$1$1$invokeSuspend$$inlined$filterIsInstance$1;->(Lkotlinx/coroutines/flow/Flow;)V -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$ContactListScreen$1$1$invokeSuspend$$inlined$filterIsInstance$1;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$ContactListScreen$1$1$invokeSuspend$$inlined$filterIsInstance$1$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$ContactListScreen$1$1$invokeSuspend$$inlined$filterIsInstance$1$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$ContactListScreen$1$1$invokeSuspend$$inlined$filterIsInstance$1$2$1;->(Lcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$ContactListScreen$1$1$invokeSuspend$$inlined$filterIsInstance$1$2;Lkotlin/coroutines/Continuation;)V -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$ContactListScreen$2$1;->(Lcom/flipcash/app/directsend/internal/SendFlowViewModel;Lcom/getcode/navigation/flow/FlowNavigator;Lkotlin/coroutines/Continuation;)V -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$ContactListScreen$2$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$ContactListScreen$2$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$ContactListScreen$2$1$1;->(Lcom/getcode/navigation/flow/FlowNavigator;)V -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$ContactListScreen$2$1$1;->emit(Lcom/flipcash/app/directsend/internal/SendFlowViewModel$Event$NavigateToChat;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$ContactListScreen$2$1$1;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$ContactListScreen$2$1$invokeSuspend$$inlined$filterIsInstance$1;->(Lkotlinx/coroutines/flow/Flow;)V -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$ContactListScreen$2$1$invokeSuspend$$inlined$filterIsInstance$1;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$ContactListScreen$2$1$invokeSuspend$$inlined$filterIsInstance$1$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$ContactListScreen$2$1$invokeSuspend$$inlined$filterIsInstance$1$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$ContactListScreen$2$1$invokeSuspend$$inlined$filterIsInstance$1$2$1;->(Lcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$ContactListScreen$2$1$invokeSuspend$$inlined$filterIsInstance$1$2;Lkotlin/coroutines/Continuation;)V -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$ContactListScreen$4$2$1;->(Lcom/getcode/ui/components/PullToRevealState;Landroidx/compose/foundation/lazy/LazyListState;Landroidx/compose/runtime/State;Lkotlin/coroutines/Continuation;)V -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$ContactListScreen$4$2$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$ContactListScreen$4$2$1;->invokeSuspend$lambda$0(Lcom/getcode/ui/components/PullToRevealState;Landroidx/compose/foundation/lazy/LazyListState;Landroidx/compose/runtime/State;)Z -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$ContactListScreen$4$2$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$ContactListScreen$4$2$1$$ExternalSyntheticLambda0;->(Lcom/getcode/ui/components/PullToRevealState;Landroidx/compose/foundation/lazy/LazyListState;Landroidx/compose/runtime/State;)V -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$ContactListScreen$4$2$1$$ExternalSyntheticLambda0;->invoke()Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$ContactListScreen$4$2$1$2;->(Lcom/getcode/ui/components/PullToRevealState;)V -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$ContactListScreen$4$2$1$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$ContactListScreen$4$2$1$2;->emit(ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$ContactListScreen$lambda$5$0$0$$inlined$onDispose$1;->(Lkotlin/jvm/functions/Function1;)V -PLcom/flipcash/app/directsend/internal/screens/ContactListScreenKt$ContactListScreen$lambda$5$0$0$$inlined$onDispose$1;->dispose()V -PLcom/flipcash/app/directsend/internal/screens/components/ComposableSingletons$ContactListKt;->()V -PLcom/flipcash/app/directsend/internal/screens/components/ComposableSingletons$ContactListKt;->()V -PLcom/flipcash/app/directsend/internal/screens/components/ComposableSingletons$ContactListKt;->getLambda$1232723110$direct_send()Lkotlin/jvm/functions/Function3; -PLcom/flipcash/app/directsend/internal/screens/components/ComposableSingletons$ContactListKt$$ExternalSyntheticLambda0;->()V -PLcom/flipcash/app/directsend/internal/screens/components/ContactListKt;->$r8$lambda$EH63oSxj9W2KVijNDmWQdgz2d_U(Landroidx/compose/foundation/text/input/TextFieldState;Landroidx/compose/foundation/lazy/LazyItemScope;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit; -PLcom/flipcash/app/directsend/internal/screens/components/ContactListKt;->$r8$lambda$m_hlk50Tj9dmTrnrT-xgTBJTN0I(Ljava/util/List;Landroidx/compose/foundation/text/input/TextFieldState;Lcom/flipcash/app/permissions/ContactAccessHandle;ZLkotlin/jvm/functions/Function1;Landroid/content/Context;Lkotlin/jvm/functions/Function0;Landroidx/compose/foundation/lazy/LazyListScope;)Lkotlin/Unit; -PLcom/flipcash/app/directsend/internal/screens/components/ContactListKt;->$r8$lambda$r_Rxu_DANWE5sXZ576EOgfmwLBA(ILcom/flipcash/app/directsend/internal/ContactListItem;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/screens/components/ContactListKt;->ContactList$lambda$3$0$0(Landroidx/compose/foundation/text/input/TextFieldState;Landroidx/compose/foundation/lazy/LazyItemScope;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit; -PLcom/flipcash/app/directsend/internal/screens/components/ContactListKt;->ContactList$lambda$3$0$1(ILcom/flipcash/app/directsend/internal/ContactListItem;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/screens/components/ContactListKt;->ContactList$lambda$3$0(Ljava/util/List;Landroidx/compose/foundation/text/input/TextFieldState;Lcom/flipcash/app/permissions/ContactAccessHandle;ZLkotlin/jvm/functions/Function1;Landroid/content/Context;Lkotlin/jvm/functions/Function0;Landroidx/compose/foundation/lazy/LazyListScope;)Lkotlin/Unit; -PLcom/flipcash/app/directsend/internal/screens/components/ContactListKt;->ContactList(Ljava/util/List;Landroidx/compose/foundation/text/input/TextFieldState;Landroidx/compose/foundation/lazy/LazyListState;Landroidx/compose/ui/Modifier;ZLkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V -HPLcom/flipcash/app/directsend/internal/screens/components/ContactListKt;->ContactRowItem(Lcom/flipcash/app/core/contacts/DeviceContact;ZLandroidx/compose/ui/Modifier;Ljava/lang/String;IZLkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;II)V -PLcom/flipcash/app/directsend/internal/screens/components/ContactListKt;->EmptySearchState(Ljava/lang/String;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V -PLcom/flipcash/app/directsend/internal/screens/components/ContactListKt;->access$ContactRowItem(Lcom/flipcash/app/core/contacts/DeviceContact;ZLandroidx/compose/ui/Modifier;Ljava/lang/String;IZLkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;II)V -PLcom/flipcash/app/directsend/internal/screens/components/ContactListKt$$ExternalSyntheticLambda1;->(Lcom/flipcash/app/core/contacts/DeviceContact;ZLandroidx/compose/ui/Modifier;Ljava/lang/String;IZLkotlin/jvm/functions/Function0;II)V -PLcom/flipcash/app/directsend/internal/screens/components/ContactListKt$$ExternalSyntheticLambda13;->(Ljava/util/List;Landroidx/compose/foundation/text/input/TextFieldState;Lcom/flipcash/app/permissions/ContactAccessHandle;ZLkotlin/jvm/functions/Function1;Landroid/content/Context;Lkotlin/jvm/functions/Function0;)V -PLcom/flipcash/app/directsend/internal/screens/components/ContactListKt$$ExternalSyntheticLambda13;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/screens/components/ContactListKt$$ExternalSyntheticLambda14;->(Ljava/util/List;Landroidx/compose/foundation/text/input/TextFieldState;Landroidx/compose/foundation/lazy/LazyListState;Landroidx/compose/ui/Modifier;ZLkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;II)V -PLcom/flipcash/app/directsend/internal/screens/components/ContactListKt$$ExternalSyntheticLambda2;->(Landroidx/compose/foundation/text/input/TextFieldState;)V -PLcom/flipcash/app/directsend/internal/screens/components/ContactListKt$$ExternalSyntheticLambda2;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/screens/components/ContactListKt$$ExternalSyntheticLambda3;->()V -PLcom/flipcash/app/directsend/internal/screens/components/ContactListKt$$ExternalSyntheticLambda3;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/screens/components/ContactListKt$$ExternalSyntheticLambda7;->(Ljava/lang/String;Landroidx/compose/ui/Modifier;II)V -PLcom/flipcash/app/directsend/internal/screens/components/ContactListKt$ContactList$4$1$3$1$1;->(Lkotlin/jvm/functions/Function1;Lcom/flipcash/app/directsend/internal/ContactListItem;)V -PLcom/flipcash/app/directsend/internal/screens/components/ContactListKt$ContactList$4$1$3$1$1;->invoke()Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/screens/components/ContactListKt$ContactList$4$1$3$1$1;->invoke()V -PLcom/flipcash/app/directsend/internal/screens/components/ContactListKt$ContactList$lambda$3$0$$inlined$itemsIndexed$default$1;->(Lkotlin/jvm/functions/Function2;Ljava/util/List;)V -PLcom/flipcash/app/directsend/internal/screens/components/ContactListKt$ContactList$lambda$3$0$$inlined$itemsIndexed$default$1;->invoke(I)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/screens/components/ContactListKt$ContactList$lambda$3$0$$inlined$itemsIndexed$default$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/screens/components/ContactListKt$ContactList$lambda$3$0$$inlined$itemsIndexed$default$2;->(Ljava/util/List;)V -PLcom/flipcash/app/directsend/internal/screens/components/ContactListKt$ContactList$lambda$3$0$$inlined$itemsIndexed$default$2;->invoke(I)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/screens/components/ContactListKt$ContactList$lambda$3$0$$inlined$itemsIndexed$default$2;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -PLcom/flipcash/app/directsend/internal/screens/components/ContactListKt$ContactList$lambda$3$0$$inlined$itemsIndexed$default$3;->(Ljava/util/List;Ljava/util/List;Lkotlin/jvm/functions/Function1;)V -PLcom/flipcash/app/directsend/internal/screens/components/ContactListKt$ContactList$lambda$3$0$$inlined$itemsIndexed$default$3;->invoke(Landroidx/compose/foundation/lazy/LazyItemScope;ILandroidx/compose/runtime/Composer;I)V -PLcom/flipcash/app/directsend/internal/screens/components/ContactListKt$ContactList$lambda$3$0$$inlined$itemsIndexed$default$3;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; PLcom/flipcash/app/discovery/TokenDiscoveryScreenKt;->$r8$lambda$K354TV-SjBby80YD3qS4120xWSM(Lcom/getcode/navigation/core/CodeNavigator;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit; PLcom/flipcash/app/discovery/TokenDiscoveryScreenKt;->TokenDiscoveryEventHandler(Lcom/flipcash/app/discovery/internal/TokenDiscoveryViewModel;Lcom/getcode/navigation/core/CodeNavigator;Landroidx/compose/runtime/Composer;I)V PLcom/flipcash/app/discovery/TokenDiscoveryScreenKt;->TokenDiscoveryScreen$lambda$1$0(Lcom/getcode/navigation/core/CodeNavigator;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit; diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/send/SendStep.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/send/SendStep.kt deleted file mode 100644 index 693cdecf38..0000000000 --- a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/send/SendStep.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.flipcash.app.core.send - -import android.os.Parcelable -import com.getcode.navigation.flow.FlowStep -import kotlinx.parcelize.Parcelize -import kotlinx.serialization.Serializable - -/** - * Steps inside the Send flow. Rendered inside a [com.getcode.navigation.flow.FlowHost] - * by `SendFlowScreen` in the `direct-send` feature module. - */ -@Serializable -sealed interface SendStep : FlowStep, Parcelable { - @Parcelize - @Serializable - data object PhoneGate : SendStep - - @Parcelize - @Serializable - data object ContactsGate : SendStep - - @Parcelize - @Serializable - data object ContactList : SendStep -} - -@Serializable -sealed interface SendResult : Parcelable { - @Parcelize - @Serializable - data class Sent(val amount: com.getcode.opencode.model.financial.Fiat) : SendResult -} diff --git a/apps/flipcash/features/direct-send/.gitignore b/apps/flipcash/features/direct-send/.gitignore deleted file mode 100644 index 9f2a078806..0000000000 --- a/apps/flipcash/features/direct-send/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -build/ -.gradle/ diff --git a/apps/flipcash/features/direct-send/build.gradle.kts b/apps/flipcash/features/direct-send/build.gradle.kts deleted file mode 100644 index a3c55dd240..0000000000 --- a/apps/flipcash/features/direct-send/build.gradle.kts +++ /dev/null @@ -1,30 +0,0 @@ -plugins { - alias(libs.plugins.flipcash.android.feature) -} - -android { - namespace = "${Gradle.flipcashNamespace}.features.directsend" -} - -dependencies { - testImplementation(kotlin("test")) - testImplementation(libs.bundles.unit.testing) - testImplementation(libs.mockito.kotlin) - testImplementation(libs.robolectric) - - implementation(libs.kotlin.stdlib) - implementation(project(":apps:flipcash:shared:analytics")) - implementation(project(":libs:datetime")) - implementation(project(":libs:logging")) - implementation(project(":libs:messaging")) - implementation(project(":libs:permissions:bindings")) - implementation(project(":apps:flipcash:shared:featureflags")) - implementation(project(":apps:flipcash:shared:funding")) - implementation(project(":apps:flipcash:shared:permissions")) - implementation(project(":apps:flipcash:shared:chat")) - implementation(project(":apps:flipcash:shared:chat-ui")) - implementation(project(":apps:flipcash:shared:contacts")) - implementation(project(":apps:flipcash:shared:phone")) - implementation(project(":apps:flipcash:shared:tokens")) - implementation(project(":services:flipcash")) -} diff --git a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/SendFlowScreen.kt b/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/SendFlowScreen.kt deleted file mode 100644 index d2c3ae5b24..0000000000 --- a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/SendFlowScreen.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.flipcash.app.directsend - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation3.runtime.NavEntry -import androidx.navigation3.runtime.NavKey -import androidx.navigation3.runtime.entryProvider -import com.flipcash.app.core.send.SendResult -import com.flipcash.app.core.send.SendStep -import com.flipcash.app.directsend.internal.SendFlowViewModel -import com.flipcash.app.directsend.internal.screens.ContactListScreen -import com.flipcash.app.directsend.internal.screens.ContactsPermissionGateScreen -import com.flipcash.app.directsend.internal.screens.PhoneGateLandingScreen -import com.getcode.navigation.annotatedEntry -import com.getcode.navigation.flow.FlowExitReason -import com.getcode.navigation.flow.FlowHost -import com.getcode.navigation.flow.flowSharedViewModel -import com.getcode.navigation.results.NavResultStateRegistry -import com.getcode.navigation.scenes.LocalBottomSheetDismissDispatcher -import com.getcode.ui.utils.rememberKeyboardController - -@Composable -fun SendFlowScreen(resultStateRegistry: NavResultStateRegistry) { - val sheetDismiss = LocalBottomSheetDismissDispatcher.current - val viewModel = hiltViewModel() - val state by viewModel.stateFlow.collectAsStateWithLifecycle() - val keyboard = rememberKeyboardController() - FlowHost( - steps = state.steps, - resumeAt = 0, - resultStateRegistry = resultStateRegistry, - onExit = { reason, _ -> - when (reason) { - is FlowExitReason.Completed, - FlowExitReason.BackedOutOfRoot, - FlowExitReason.Canceled -> keyboard.hideIfVisible { sheetDismiss() } - } - }, - entryProvider = sendEntryProvider(), - ) -} - -@Composable -private fun sendEntryProvider(): (NavKey) -> NavEntry { - return entryProvider { - annotatedEntry { - SyncStep(it) - PhoneGateLandingScreen() - } - annotatedEntry { - SyncStep(it) - ContactsPermissionGateScreen() - } - annotatedEntry { - SyncStep(it) - ContactListScreen() - } - } -} - -@Composable -private fun SyncStep(step: SendStep) { - val viewModel = flowSharedViewModel() - LaunchedEffect(step) { - viewModel.dispatchEvent(SendFlowViewModel.Event.OnStepChanged(step)) - } -} diff --git a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/ContactListBuilder.kt b/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/ContactListBuilder.kt deleted file mode 100644 index d9a5693d77..0000000000 --- a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/ContactListBuilder.kt +++ /dev/null @@ -1,142 +0,0 @@ -package com.flipcash.app.directsend.internal - -import com.flipcash.app.contacts.ContactCoordinator.ContactState -import com.flipcash.app.core.contacts.DeviceContact -import com.flipcash.app.phone.PhoneUtils -import com.flipcash.features.directsend.R -import com.flipcash.services.models.chat.ChatMember -import com.flipcash.services.models.chat.ChatType -import com.flipcash.services.user.UserManager -import com.flipcash.shared.chat.ChatSummary -import com.flipcash.shared.chat.ui.toConversationReference -import com.getcode.opencode.model.financial.Token -import com.getcode.solana.keys.Mint -import com.getcode.util.resources.ResourceHelper -import javax.inject.Inject - -/** - * Builds the send-flow contact list from the raw inputs: device contacts, the - * chat feed, the current search string, and known tokens. - * - * Pure and free of the ViewModel/flow machinery so it can be unit-tested - * directly. Ephemeral overlays (e.g. typing indicators) are applied on top of - * this output separately — see [SendFlowViewModel] — so they don't force a - * re-filter/re-sort of the whole list. - */ -internal class ContactListBuilder @Inject constructor( - private val userManager: UserManager, - private val phoneUtils: PhoneUtils, - private val resources: ResourceHelper, -) { - - fun build( - contactState: ContactState, - searchString: String, - chatFeed: List, - tokensByMint: Map, - ): List = buildList { - val selfId = userManager.accountId - val selfPhone = userManager.profile?.verifiedPhoneNumber - - val allContacts = contactState.contacts.values - .filter { selfPhone == null || it.e164 != selfPhone } - val filtered = if (searchString.isBlank()) { - allContacts - } else { - allContacts.filter { - it.displayName.contains(searchString, ignoreCase = true) || - it.e164.contains(searchString, ignoreCase = true) - } - } - - val dmChats = chatFeed.filter { it.metadata.type == ChatType.CONTACT_DM } - // Build a reverse lookup: e164 -> chatId string for contacts with DMs - val e164ToChatId = contactState.dmChatIds - - // Recents — driven by the chat feed, enriched with contact info - val recentsE164s = mutableSetOf() - val recentRows = dmChats.mapNotNull { summary -> - val chatId = summary.metadata.chatId - val chatIdStr = chatId.toString() - - // Try to match this chat to a device contact - val e164 = e164ToChatId.entries - .firstOrNull { it.value == chatIdStr }?.key - val deviceContact = e164 - ?.takeIf { selfPhone == null || it != selfPhone } - ?.let { contactState.contacts[it] } - - val contact = if (deviceContact != null) { - if (searchString.isNotBlank() && - !deviceContact.displayName.contains(searchString, ignoreCase = true) && - !deviceContact.e164.contains(searchString, ignoreCase = true) - ) { - return@mapNotNull null - } - recentsE164s += deviceContact.e164 - deviceContact - } else { - // Non-contact DM — build contact from chat member profile - val isSelf = { member: ChatMember -> - member.userId == selfId || (selfPhone != null && member.userProfile.verifiedPhoneNumber == selfPhone) - } - val otherMember = summary.metadata.members - .firstOrNull { !isSelf(it) } ?: return@mapNotNull null - val phone = otherMember.userProfile.verifiedPhoneNumber - val formattedPhone = phone?.let { phoneUtils.formatNumber(it) } - val displayName = otherMember.userProfile.displayName.takeIf { it.isNotBlank() } - ?: formattedPhone - ?: return@mapNotNull null - - val unknown = DeviceContact.unknownContact( - e164 = phone.orEmpty(), - displayName = displayName, - displayNumber = formattedPhone, - ) - if (searchString.isNotBlank() && - !unknown.displayName.contains(searchString, ignoreCase = true) - ) { - return@mapNotNull null - } - if (unknown.e164.isNotEmpty()) { - recentsE164s += unknown.e164 - } - unknown - } - - ContactListItem.ContactRow( - contact = contact, - isOnFlipcash = true, - lastActivity = summary.metadata.lastActivity, - conversation = summary.toConversationReference(selfId, tokensByMint, resources), - ) - } - - // On Flipcash — contacts that haven't chatted yet, use joinedAt as their sort timestamp - val flipcashRows = filtered - .filter { it.e164 in contactState.flipcashE164s && it.e164 !in recentsE164s } - .map { contact -> - ContactListItem.ContactRow( - contact = contact, - isOnFlipcash = true, - lastActivity = contactState.joinedAtByE164[contact.e164], - ) - } - - val flipcashCombined = (recentRows + flipcashRows).sortedWith( - compareByDescending { it.lastActivity } - .thenBy(String.CASE_INSENSITIVE_ORDER) { it.contact.displayName } - ) - - val excludedE164s = recentsE164s + contactState.flipcashE164s - val other = filtered - .filter { it.e164 !in excludedE164s } - .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.displayName }) - - addAll(flipcashCombined) - if (other.isNotEmpty()) { - add(ContactListItem.Header(resources.getString(R.string.title_nonFlipcashContacts))) - other.forEach { add(ContactListItem.ContactRow(it, isOnFlipcash = false)) } - } - } -} diff --git a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/ContactListItem.kt b/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/ContactListItem.kt deleted file mode 100644 index 8ecfcbd95c..0000000000 --- a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/ContactListItem.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.flipcash.app.directsend.internal - -import com.flipcash.app.core.contacts.DeviceContact -import com.flipcash.services.models.chat.ChatId -import com.flipcash.shared.chat.ui.ConversationReference -import kotlin.time.Instant - -internal sealed interface ContactListItem { - data class Header(val title: String) : ContactListItem - - /** - * A single contact in the send list. - * - * [lastActivity] is the sort key and is present for any row that should be - * ranked by recency — a chat's last activity for recents, or the contact's - * joinedAt for on-Flipcash contacts without a chat yet — so it lives on the - * row rather than inside [conversation]. - * - * [conversation] holds everything derived from an existing DM and is `null` - * for contacts with no chat (on-Flipcash-but-never-messaged, or off-Flipcash). - */ - data class ContactRow( - val contact: DeviceContact, - val isOnFlipcash: Boolean, - val lastActivity: Instant? = null, - val conversation: ConversationReference? = null, - ) : ContactListItem -} diff --git a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/SendFlowViewModel.kt b/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/SendFlowViewModel.kt deleted file mode 100644 index d23fb6e8d3..0000000000 --- a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/SendFlowViewModel.kt +++ /dev/null @@ -1,306 +0,0 @@ -package com.flipcash.app.directsend.internal - -import androidx.compose.foundation.text.input.TextFieldState -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.runtime.snapshotFlow -import androidx.lifecycle.viewModelScope -import com.flipcash.app.contacts.ContactCoordinator -import com.flipcash.app.contacts.device.PickedContactData -import com.flipcash.app.core.chat.ChatIdentifier -import com.flipcash.app.core.contacts.DeviceContact -import com.flipcash.app.core.send.SendStep -import com.flipcash.app.featureflags.FeatureFlag -import com.flipcash.app.featureflags.FeatureFlagController -import com.flipcash.app.permissions.PickedContact -import com.flipcash.app.tokens.TokenCoordinator -import com.flipcash.features.directsend.R -import com.flipcash.services.models.chat.ChatType -import com.flipcash.services.user.UserManager -import com.flipcash.shared.chat.ChatCoordinator -import com.getcode.manager.BottomBarManager -import com.getcode.util.resources.ResourceHelper -import com.getcode.view.BaseViewModel -import com.getcode.view.LoadingSuccessState -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.take -import javax.inject.Inject -import kotlin.time.Duration.Companion.seconds - -@HiltViewModel -internal class SendFlowViewModel @Inject constructor( - private val userManager: UserManager, - featureFlags: FeatureFlagController, - private val contactCoordinator: ContactCoordinator, - chatCoordinator: ChatCoordinator, - tokenCoordinator: TokenCoordinator, - private val contactListBuilder: ContactListBuilder, - private val resources: ResourceHelper, -) : BaseViewModel( - initialState = State(), - updateStateForEvent = updateStateForEvent, -) { - - data class State @OptIn(ExperimentalMaterial3Api::class) constructor( - val steps: List = listOf(SendStep.ContactList), - val currentStep: SendStep? = null, - val searchState: TextFieldState = TextFieldState(), - val isPickerMode: Boolean = false, - val contactSyncState: LoadingSuccessState = LoadingSuccessState(), - val listItems: List = emptyList(), - val hasCalloutBeenDismissed: Boolean = true, - ) - - sealed interface Event { - data class StepsUpdated(val steps: List, val isPickerMode: Boolean) : Event - data class OnStepChanged(val step: SendStep) : Event - - data object ContactsGranted : Event - data class ContactsPicked(val contacts: List) : Event - data class OnItemsPopulated(val items: List) : Event - data class ContactSyncStateUpdated( - val loading: Boolean = false, - val success: Boolean = false, - val error: Boolean = false, - ) : Event - - data object ContactSyncComplete : Event - data class OnContactClicked(val contact: ContactListItem.ContactRow) : Event - data class ContactRemoved(val e164: String) : Event - data class SendInvite(val contact: DeviceContact) : Event - - data class NavigateToChat(val identifier: ChatIdentifier) : Event - data object ContactsRevoked : Event - - /** User tapped dismiss on the permission callout. */ - data object DismissedPermissionCallout : Event - - /** Observed dismissal state from [ContactCoordinator.isPermissionCalloutDismissed]. */ - data class CalloutDismissalChanged(val dismissed: Boolean) : Event - } - - init { - combine( - userManager.state, - featureFlags.observe(FeatureFlag.ContactPickerMode), - contactCoordinator.state, - chatCoordinator.feed(ChatType.CONTACT_DM), - ) { userState, contactPickerMode, contactState, chats -> - val hasLinkedPhone = userState.userProfile?.verifiedPhoneNumber != null - val hasContacts = contactState.contacts.isNotEmpty() - val needsContacts = !hasContacts && !contactState.hasEverSynced - - val steps = buildList { - if (!hasLinkedPhone) add(SendStep.PhoneGate) - if (needsContacts && chats.isEmpty()) add(SendStep.ContactsGate) - add(SendStep.ContactList) - } - Event.StepsUpdated(steps = steps, isPickerMode = contactPickerMode) - }.onEach { event -> - dispatchEvent(event) - }.launchIn(viewModelScope) - - // Base list — expensive to build (filter + sort), so it only recomputes - // when contacts, search, the chat feed, or tokens change. - val baseItems = combine( - contactCoordinator.state, - stateFlow - .map { it.searchState } - .distinctUntilChanged() - .flatMapLatest { snapshotFlow { it.text } }, - chatCoordinator.feed(ChatType.CONTACT_DM), - tokenCoordinator.tokens, - ) { contactState, searchText, chatFeed, tokens -> - contactListBuilder.build( - contactState = contactState, - searchString = searchText.toString(), - chatFeed = chatFeed, - tokensByMint = tokens.associateBy { it.address }, - ) - } - - // Typing is ephemeral and flips on/off frequently — overlay it on top of - // the base list with a cheap map rather than rebuilding + re-sorting the - // whole list on every typing tick. distinctUntilChanged means we only - // recompute when the set of typing chats changes. - val typingChatIds = chatCoordinator.state - .map { chatState -> - val selfId = userManager.accountId - chatState.typingIndicators - .filterValues { typists -> typists.any { it.userId != selfId } } - .keys - } - .distinctUntilChanged() - - combine(baseItems, typingChatIds) { items, typing -> - if (typing.isEmpty()) { - items - } else { - items.map { item -> - val conversation = (item as? ContactListItem.ContactRow)?.conversation - if (conversation != null && conversation.chatId in typing) { - item.copy(conversation = conversation.copy(isTyping = true)) - } else { - item - } - } - } - }.onEach { items -> - dispatchEvent(Event.OnItemsPopulated(items)) - }.launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .onEach { - dispatchEvent(Event.ContactSyncStateUpdated(loading = true)) - contactCoordinator.sync() - .onSuccess { - dispatchEvent(Event.ContactSyncStateUpdated(success = true)) - delay(1.seconds) - dispatchEvent(Event.ContactSyncComplete) - } - .onFailure { - dispatchEvent(Event.ContactSyncStateUpdated(error = true)) - delay(1.seconds) - } - dispatchEvent(Event.ContactSyncStateUpdated()) - }.launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { - it.contacts.map { contact -> - PickedContactData( - phoneNumber = contact.phoneNumber, - displayName = contact.displayName, - photoUri = contact.photoUri, - ) - } - } - .onEach { contacts -> - dispatchEvent(Event.ContactSyncStateUpdated(loading = true)) - contactCoordinator.addPickedContacts(contacts) - .onSuccess { - dispatchEvent(Event.ContactSyncStateUpdated(success = true)) - delay(1.seconds) - dispatchEvent(Event.ContactSyncComplete) - } - .onFailure { - dispatchEvent(Event.ContactSyncStateUpdated(error = true)) - delay(1.seconds) - } - dispatchEvent(Event.ContactSyncStateUpdated()) - }.launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { it.contact } - .onEach { row -> - val (contact, isOnFlipcash) = row - if (isOnFlipcash) { - val identifier = if (contact.e164.isNotEmpty()) { - ChatIdentifier.ByContact( - contact = contact, - chatId = row.conversation?.chatId - ) - } else { - ChatIdentifier.ByChatId(row.conversation!!.chatId) - } - dispatchEvent(Event.NavigateToChat(identifier)) - } else { - dispatchEvent(Event.SendInvite(contact)) - } - }.launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .onEach { event -> contactCoordinator.removeContact(event.e164) } - .launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .onEach { contactCoordinator.clearServerContactSetIfRevoked() } - .launchIn(viewModelScope) - - // Observe the persisted callout-dismissal state so the callout hides when - // dismissed and re-appears if the coordinator ever expires the dismissal. - contactCoordinator.isPermissionCalloutDismissed - .onEach { dismissed -> dispatchEvent(Event.CalloutDismissalChanged(dismissed)) } - .launchIn(viewModelScope) - - // Persist the dismissal when the user taps dismiss. - eventFlow - .filterIsInstance() - .onEach { contactCoordinator.onPermissionCalloutDismissed() } - .launchIn(viewModelScope) - - combine( - contactCoordinator.state, - stateFlow.map { it.currentStep }, - ) { contactState, currentStep -> - contactState to currentStep - } - .filter { (cs, _) -> cs.hasDiscoveredFlipcashContacts && cs.flipcashE164s.isNotEmpty() } - .filter { (_, step) -> step is SendStep.ContactList } - .take(1) - .onEach { (contactState, _) -> - val count = contactState.flipcashE164s.size - contactCoordinator.consumeContactsDiscovery() - BottomBarManager.showInfo( - title = resources.getQuantityString( - R.plurals.prompt_title_contactsAlreadyOnFlipcash, - count, - count.toString(), - ), - message = resources.getString(R.string.prompt_description_contactsAlreadyOnFlipcash), - ) - } - .launchIn(viewModelScope) - } - - companion object { - val updateStateForEvent: (Event) -> ((State) -> State) = { event -> - when (event) { - is Event.StepsUpdated -> { state -> - state.copy(steps = event.steps, isPickerMode = event.isPickerMode) - } - - is Event.OnStepChanged -> { state -> - state.copy(currentStep = event.step) - } - - is Event.ContactsGranted -> { state -> state } - is Event.ContactsPicked -> { state -> state } - is Event.ContactSyncStateUpdated -> { state -> - state.copy( - contactSyncState = LoadingSuccessState( - event.loading, - event.success, - event.error - ) - ) - } - - is Event.ContactRemoved -> { state -> state } - is Event.ContactSyncComplete -> { state -> state } - is Event.OnItemsPopulated -> { state -> state.copy(listItems = event.items) } - is Event.OnContactClicked -> { state -> state } - is Event.SendInvite -> { state -> state } - is Event.NavigateToChat -> { state -> state } - is Event.ContactsRevoked -> { state -> state } - is Event.DismissedPermissionCallout -> { state -> state } - is Event.CalloutDismissalChanged -> { state -> - state.copy(hasCalloutBeenDismissed = event.dismissed) - } - } - } - } -} 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 deleted file mode 100644 index ec1ecebfb3..0000000000 --- a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/ContactListScreen.kt +++ /dev/null @@ -1,221 +0,0 @@ -package com.flipcash.app.directsend.internal.screens - -import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.spring -import androidx.compose.animation.expandVertically -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.shrinkVertically -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.snapshotFlow -import androidx.compose.ui.Alignment -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 -import com.flipcash.app.directsend.internal.SendFlowViewModel -import com.flipcash.app.directsend.internal.screens.components.ContactList -import com.flipcash.app.directsend.internal.screens.components.ContactPermissionCallout -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.flowSharedViewModel -import com.getcode.navigation.flow.rememberFlowNavigator -import com.getcode.theme.CodeTheme -import com.getcode.ui.components.AppBarDefaults -import com.getcode.ui.components.AppBarWithTitle -import com.getcode.ui.components.PullToReveal -import com.getcode.ui.components.SearchInput -import com.getcode.ui.components.rememberPullToRevealState -import com.getcode.ui.theme.CodeScaffold -import com.getcode.ui.utils.LocalSheetGesturesState -import kotlinx.coroutines.flow.filterIsInstance - - -@Composable -internal fun ContactListScreen() { - val flowNavigator = rememberFlowNavigator() - val viewModel = flowSharedViewModel() - val analytics = LocalAnalytics.current - - val state by viewModel.stateFlow.collectAsStateWithLifecycle() - - LaunchedEffect(viewModel) { - viewModel.eventFlow - .filterIsInstance() - .collect { event -> - flowNavigator.navigate(AppRoute.Main.InviteContact(event.contact.e164)) - } - } - - LaunchedEffect(viewModel) { - viewModel.eventFlow - .filterIsInstance() - .collect { event -> - flowNavigator.navigate( - AppRoute.Messaging.Chat(identifier = event.identifier) - ) - } - } - - val accessHandle = rememberContactAccessHandle( - 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)) - } - - else -> Unit - } - } - - CodeScaffold( - topBar = { - AppBarWithTitle( - title = stringResource(R.string.title_send), - titleAlignment = Alignment.CenterHorizontally, - endContent = { - AppBarDefaults.Close { flowNavigator.exitCanceled() } - }, - ) - }, - ) { innerPadding -> - val listState = rememberLazyListState() - - // Search bar is hidden by default; an explicit pull-down on the list reveals it. - val pullToRevealState = rememberPullToRevealState() - - // PullToReveal drives this: the sheet's drag-to-dismiss is suppressed while a - // pull is being captured (and while search is hidden), so a downward pull - // reveals search instead of dismissing — even on a single hard pull — and is - // restored once the gesture ends with search shown. Always re-enable on exit. - val setSheetGesturesEnabled = LocalSheetGesturesState.current - DisposableEffect(setSheetGesturesEnabled) { - onDispose { setSheetGesturesEnabled(true) } - } - - // Collapse the search bar again once the user scrolls into the list without - // an active query. - LaunchedEffect(pullToRevealState) { - snapshotFlow { - pullToRevealState.isRevealed && - listState.firstVisibleItemIndex > 0 && - state.searchState.text.isEmpty() - }.collect { shouldHide -> - if (shouldHide) pullToRevealState.hide() - } - } - - // When the user grants contacts access from the inline callout, the sync runs - // in two passes: device contacts land first (only the "Not on Flipcash" - // section), then the "On Flipcash" contacts are fetched and prepended to the - // top. LazyColumn's key-based scroll anchoring pins the previously-visible - // first row, which pushes the freshly-prepended "On Flipcash" section above the - // viewport. Keep the list pinned to the top while a user-initiated sync is - // settling so that section stays visible instead of being scrolled offscreen. - LaunchedEffect(listState) { - snapshotFlow { - (state.contactSyncState.loading || state.contactSyncState.success) to - state.listItems.size - }.collect { (syncing, _) -> - if (syncing && listState.firstVisibleItemIndex != 0) { - listState.scrollToItem(0) - } - } - } - - Column( - modifier = Modifier.padding(innerPadding), - ) { - ContactPermissionCallout( - modifier = Modifier.padding( - start = CodeTheme.dimens.inset, - end = CodeTheme.dimens.inset, - top = CodeTheme.dimens.grid.x3, - ), - state = state, - accessHandle = accessHandle, - onDismiss = { - viewModel.dispatchEvent(SendFlowViewModel.Event.DismissedPermissionCallout) - } - ) - - PullToReveal( - state = pullToRevealState, - modifier = Modifier.weight(1f), - onSuppressDismissChange = { suppress -> setSheetGesturesEnabled(!suppress) }, - // Telegram (iOS)-style reveal: a soft, low-stiffness spring that springs - // down and settles with a single gentle overshoot — the bit of bounce - // that makes it feel alive. Fade is synced so it moves in lockstep. - enter = fadeIn( - animationSpec = spring(stiffness = Spring.StiffnessLow), - ) + expandVertically( - animationSpec = spring( - dampingRatio = Spring.DampingRatioMediumBouncy, - stiffness = Spring.StiffnessLow, - ), - ), - // Collapse quickly and cleanly with no bounce. - exit = fadeOut( - animationSpec = spring(stiffness = Spring.StiffnessMedium), - ) + shrinkVertically( - animationSpec = spring( - dampingRatio = Spring.DampingRatioNoBouncy, - stiffness = Spring.StiffnessMedium, - ), - ), - revealContent = { - Row( - modifier = Modifier - .padding(horizontal = CodeTheme.dimens.grid.x3) - .padding(vertical = CodeTheme.dimens.grid.x3), - ) { - SearchInput( - modifier = Modifier - .weight(1f) - .testTag("send_search_field"), - state = state.searchState, - contentPadding = PaddingValues(start = CodeTheme.dimens.grid.x1), - ) - } - }, - ) { - ContactList( - items = state.listItems, - searchState = state.searchState, - listState = listState, - accessHandle = accessHandle, - isPickerMode = state.isPickerMode, - onItemClick = { contact -> - viewModel.dispatchEvent(SendFlowViewModel.Event.OnContactClicked(contact)) - }, - onItemDismissed = { contact -> - viewModel.dispatchEvent(SendFlowViewModel.Event.ContactRemoved(contact.contact.e164)) - }, - ) - } - } - } -} 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 deleted file mode 100644 index c10feada0a..0000000000 --- a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/ContactsPermissionGateScreen.kt +++ /dev/null @@ -1,130 +0,0 @@ -package com.flipcash.app.directsend.internal.screens - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.flipcash.app.analytics.Button -import com.flipcash.app.core.send.SendResult -import com.flipcash.app.core.send.SendStep -import com.flipcash.app.directsend.internal.ContactListItem -import com.flipcash.app.directsend.internal.SendFlowViewModel -import com.flipcash.app.permissions.ContactAccessResult -import com.flipcash.app.permissions.internal.contacts.ContactRationaleContent -import com.flipcash.app.permissions.internal.contacts.ContactScreenContent -import com.flipcash.app.permissions.rememberContactAccessHandle -import com.flipcash.features.directsend.R -import com.getcode.libs.analytics.LocalAnalytics -import com.getcode.navigation.flow.flowSharedViewModel -import com.getcode.navigation.flow.rememberFlowNavigator -import com.getcode.ui.components.AppBarDefaults -import com.getcode.ui.components.AppBarWithTitle -import com.getcode.ui.theme.CodeScaffold -import com.getcode.util.permissions.PermissionResult -import kotlinx.coroutines.flow.filterIsInstance - -@Composable -internal fun ContactsPermissionGateScreen() { - val flowNavigator = rememberFlowNavigator() - val viewModel = flowSharedViewModel() - val state by viewModel.stateFlow.collectAsStateWithLifecycle() - val analytics = LocalAnalytics.current - - val accessHandle = rememberContactAccessHandle( - isPickerMode = state.isPickerMode, - ) { result -> - when (result) { - ContactAccessResult.Granted -> { - analytics.action(Button.AllowContacts) - viewModel.dispatchEvent(SendFlowViewModel.Event.ContactsGranted) - } - is ContactAccessResult.Picked -> { - analytics.action(Button.AllowContacts) - viewModel.dispatchEvent(SendFlowViewModel.Event.ContactsPicked(result.contacts)) - } - ContactAccessResult.Denied, - ContactAccessResult.PermanentlyDenied -> Unit - ContactAccessResult.Canceled -> Unit - } - } - - LaunchedEffect(viewModel) { - viewModel.eventFlow - .filterIsInstance() - .collect { flowNavigator.proceed() } - } - - // React to permission status changes when returning from Settings - LaunchedEffect(accessHandle.permissionStatus) { - when (accessHandle.permissionStatus) { - PermissionResult.Granted -> { - viewModel.dispatchEvent(SendFlowViewModel.Event.ContactsGranted) - } - PermissionResult.Denied, - PermissionResult.PermanentlyDenied -> { - viewModel.dispatchEvent(SendFlowViewModel.Event.ContactsRevoked) - } - PermissionResult.NotRequested -> Unit - } - } - - val permissionDenied = accessHandle.permissionStatus == PermissionResult.Denied || - accessHandle.permissionStatus == PermissionResult.PermanentlyDenied - - val showRationale = permissionDenied && !state.isPickerMode - - // 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 && !state.contactSyncState.loading) { - LaunchedEffect(Unit) { flowNavigator.proceed() } - return - } - - val hasChats = state.listItems.any { it is ContactListItem.ContactRow && it.conversation != null } - - // If denied but user has existing chats, skip the gate entirely - if (showRationale && hasChats) { - LaunchedEffect(Unit) { flowNavigator.proceed() } - return - } - - CodeScaffold( - topBar = { - AppBarWithTitle( - title = stringResource(R.string.title_send), - titleAlignment = Alignment.CenterHorizontally, - endContent = { - AppBarDefaults.Close { flowNavigator.exitCanceled() } - }, - ) - }, - ) { innerPadding -> - Box(modifier = Modifier.padding(innerPadding)) { - if (showRationale) { - ContactRationaleContent(simplified = true) - } else { - ContactScreenContent( - accessHandle = accessHandle, - simplified = true, - isLoading = state.contactSyncState.loading, - isSuccess = state.contactSyncState.success, - ) - } - } - } -} diff --git a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/PhoneGateLandingScreen.kt b/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/PhoneGateLandingScreen.kt deleted file mode 100644 index 446b0784b1..0000000000 --- a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/PhoneGateLandingScreen.kt +++ /dev/null @@ -1,116 +0,0 @@ -package com.flipcash.app.directsend.internal.screens - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewWrapper -import com.flipcash.app.core.AppRoute -import com.flipcash.app.core.send.SendResult -import com.flipcash.app.core.send.SendStep -import com.flipcash.app.theme.FlipcashThemeWrapper -import com.flipcash.features.directsend.R -import com.flipcash.shared.chat.ui.AnimatedConversationPreview -import com.getcode.navigation.flow.LocalFlowNavigator -import com.getcode.navigation.flow.PreviewFlowNavigator -import com.getcode.navigation.flow.rememberFlowNavigator -import com.getcode.opencode.compose.ExchangeStub -import com.getcode.opencode.compose.LocalExchange -import com.getcode.theme.CodeTheme -import com.getcode.ui.components.AppBarDefaults -import com.getcode.ui.components.AppBarWithTitle -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import com.getcode.ui.theme.CodeScaffold - - -@Composable -internal fun PhoneGateLandingScreen() { - val flowNavigator = rememberFlowNavigator() - - CodeScaffold( - topBar = { - AppBarWithTitle( - title = "", - endContent = { - AppBarDefaults.Close { flowNavigator.exitCanceled() } - }, - ) - }, - bottomBar = { - CodeButton( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = CodeTheme.dimens.inset) - .padding(bottom = CodeTheme.dimens.grid.x3) - .navigationBarsPadding(), - buttonState = ButtonState.Filled, - text = stringResource(R.string.action_next), - onClick = { - flowNavigator.navigate( - AppRoute.Verification( - origin = AppRoute.Sheets.Send(), - includePhone = true, - includeEmail = false, - linkForPayment = true, - target = AppRoute.Sheets.Send(resumed = true), - ) - ) - }, - ) - } - ) { innerPadding -> - Box( - modifier = Modifier - .fillMaxSize() - .padding(innerPadding) - ) { - Column( - modifier = Modifier.align(Alignment.Center), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - AnimatedConversationPreview(animate = false) - - Text( - modifier = Modifier.padding(top = CodeTheme.dimens.grid.x8), - text = stringResource(R.string.title_sendFeatureIntro), - style = CodeTheme.typography.displaySmall, - color = CodeTheme.colors.textMain, - ) - Text( - modifier = Modifier - .fillMaxWidth(0.8f) - .padding(top = CodeTheme.dimens.grid.x2) - .padding(horizontal = CodeTheme.dimens.inset), - text = stringResource(R.string.subtitle_sendFeatureIntro), - style = CodeTheme.typography.textSmall, - color = CodeTheme.colors.textSecondary, - textAlign = TextAlign.Center, - ) - } - } - } -} - -@Composable -@Preview -@PreviewWrapper(FlipcashThemeWrapper::class) -private fun PreviewPhoneGateScreen() { - CompositionLocalProvider( - LocalFlowNavigator provides PreviewFlowNavigator(), - LocalExchange provides ExchangeStub(context = LocalContext.current), - ) { - PhoneGateLandingScreen() - } -} \ No newline at end of file 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 deleted file mode 100644 index 5eed94567f..0000000000 --- a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/components/ContactList.kt +++ /dev/null @@ -1,650 +0,0 @@ -package com.flipcash.app.directsend.internal.screens.components - -import android.text.format.DateFormat -import androidx.compose.animation.animateColorAsState -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.requiredSize -import androidx.compose.foundation.layout.requiredWidth -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.text.input.TextFieldState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.Search -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.scale -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewWrapper -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.zIndex -import com.flipcash.app.core.contacts.DeviceContact -import com.flipcash.app.directsend.internal.ContactListItem -import com.flipcash.app.permissions.ContactAccessHandle -import com.flipcash.app.theme.FlipcashThemeWrapper -import com.flipcash.features.directsend.R -import com.flipcash.services.models.chat.ChatId -import com.flipcash.shared.chat.ui.ChatListRow -import com.flipcash.shared.chat.ui.ConversationReference -import com.flipcash.shared.chat.ui.ChatRowSubtitle -import com.flipcash.shared.chat.ui.ChatRowTrailing -import com.flipcash.shared.chat.ui.SubtitleText -import com.flipcash.shared.common.ui.ContactAvatar -import com.getcode.theme.CodeTheme -import com.getcode.theme.White10 -import com.getcode.theme.extraSmall -import com.getcode.ui.core.verticalScrollStateGradient -import com.getcode.util.formatLocalized -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toLocalDateTime -import kotlin.compareTo -import kotlin.time.Clock -import kotlin.time.Duration.Companion.days -import kotlin.time.Duration.Companion.hours -import kotlin.time.Duration.Companion.minutes -import kotlin.time.Instant - -@Composable -internal fun ContactList( - items: List, - searchState: TextFieldState, - listState: LazyListState, - accessHandle: ContactAccessHandle, - modifier: Modifier = Modifier, - isPickerMode: Boolean = false, - onItemClick: (ContactListItem.ContactRow) -> Unit = {}, - onItemDismissed: (ContactListItem.ContactRow) -> Unit = {}, -) { - val context = LocalContext.current - - LazyColumn( - modifier = Modifier - .testTag("send_contact_list") - .verticalScrollStateGradient( - scrollState = listState, - color = CodeTheme.colors.background, - isLongGradient = true, - ) - .then(modifier), - state = listState, - contentPadding = PaddingValues(bottom = CodeTheme.dimens.grid.x3), - ) { - if (items.isEmpty() && searchState.text.isNotEmpty()) { - item(key = "empty_search") { - EmptySearchState( - searchQuery = searchState.text.toString(), - modifier = Modifier.fillParentMaxSize(), - ) - } - } - - if (items.isEmpty() && searchState.text.isEmpty()) { - item(key = "empty_contacts") { - EmptyContactsState( - modifier = Modifier.fillParentMaxSize(), - ) - } - } - - itemsIndexed( - items = items, - key = { _, item -> - when (item) { - is ContactListItem.Header -> item.title - is ContactListItem.ContactRow -> item.conversation?.chatId?.toString() - ?: item.contact.e164 - } - } - ) { index, item -> - when (item) { - is ContactListItem.Header -> ContactGroupHeader( - text = item.title, - modifier = Modifier.animateItem(), - ) - - is ContactListItem.ContactRow -> { - val isLastInSection = - index == items.lastIndex || - items[index + 1] is ContactListItem.Header - - ContactRowItem( - contact = item.contact, - isOnFlipcash = item.isOnFlipcash, - lastMessagePreview = item.conversation?.lastMessagePreview, - unreadCount = item.conversation?.unreadCount ?: 0, - isTyping = item.conversation?.isTyping == true, - showDivider = !isLastInSection, - lastActivity = item.lastActivity, - ) { - onItemClick(item) - } - } - } - } - - if (isPickerMode) { - item { - PickerModeHeader( - onAddMoreContacts = { accessHandle.launch() }, - ) - } - } - } -} - -@Composable -private fun PickerModeHeader( - onAddMoreContacts: () -> Unit, - modifier: Modifier = Modifier, -) { - val avatarSize = CodeTheme.dimens.staticGrid.x8 - val overlap = CodeTheme.dimens.staticGrid.x4 - - Column(modifier = modifier.fillMaxWidth()) { - HorizontalDivider( - color = CodeTheme.colors.divider, - modifier = Modifier - .fillMaxWidth() - .height(1.dp) - .padding( - start = CodeTheme.dimens.inset + CodeTheme.dimens.staticGrid.x8 + CodeTheme.dimens.grid.x3, - end = CodeTheme.dimens.inset, - ), - ) - - Row( - modifier = Modifier - .fillMaxWidth() - .clickable(onClick = onAddMoreContacts) - .padding( - horizontal = CodeTheme.dimens.inset, - vertical = CodeTheme.dimens.grid.x3, - ), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2), - ) { - Row(horizontalArrangement = Arrangement.spacedBy(-overlap)) { - Box( - modifier = Modifier - .zIndex(4f) - .requiredSize(avatarSize) - .border( - width = CodeTheme.dimens.border, - color = CodeTheme.colors.background, - shape = CircleShape, - ) - .background( - brush = Brush.linearGradient(CodeTheme.colors.contactAvatar.colors), - shape = CircleShape - ), - contentAlignment = Alignment.Center, - ) { - Icon( - imageVector = Icons.Default.Add, - contentDescription = null, - tint = CodeTheme.colors.textMain, - modifier = Modifier.size(CodeTheme.dimens.staticGrid.x4), - ) - } - repeat(2) { i -> - ContactAvatar( - contact = null, - modifier = Modifier - .zIndex((3 - i).toFloat()) - .requiredSize(avatarSize) - .border( - width = CodeTheme.dimens.border, - color = CodeTheme.colors.background, - shape = CircleShape, - ) - .clip(CircleShape), - ) - } - } - - Text( - modifier = Modifier.weight(1f), - text = stringResource(R.string.title_addMoreContacts), - style = CodeTheme.typography.textSmall, - color = CodeTheme.colors.textSecondary, - ) - - Text( - modifier = Modifier - .background( - color = Color.White, - shape = CodeTheme.shapes.extraSmall, - ) - .padding( - horizontal = CodeTheme.dimens.grid.x2, - vertical = CodeTheme.dimens.grid.x1, - ), - text = stringResource(R.string.action_add), - style = CodeTheme.typography.textMedium, - color = Color.Black, - ) - } - } -} - -@Composable -private fun ContactGroupHeader(text: String, modifier: Modifier = Modifier) { - Box( - modifier = modifier - .fillMaxWidth() - .height(CodeTheme.dimens.grid.x12), - contentAlignment = Alignment.BottomStart, - ) { - Column { - Row(modifier = Modifier.padding(horizontal = CodeTheme.dimens.inset)) { - Text( - modifier = Modifier.padding(bottom = CodeTheme.dimens.grid.x2), - style = CodeTheme.typography.textSmall, - color = CodeTheme.colors.textSecondary, - text = text, - ) - } - HorizontalDivider( - color = CodeTheme.colors.divider, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = CodeTheme.dimens.inset) - .height(1.dp), - ) - } - } -} - -@Composable -private fun ContactRowItem( - contact: DeviceContact, - isOnFlipcash: Boolean, - modifier: Modifier = Modifier, - lastMessagePreview: String? = null, - lastActivity: Instant? = null, - unreadCount: Int = 0, - isTyping: Boolean = false, - showDivider: Boolean = true, - onClick: () -> Unit, -) { - ChatListRow( - modifier = modifier, - avatar = { - ContactAvatar( - contact = contact, - modifier = Modifier - .requiredSize(CodeTheme.dimens.staticGrid.x8) - .clip(CircleShape), - includeBorder = false, - ) - }, - title = { - Text( - modifier = Modifier.weight(1f), - text = contact.displayName, - style = CodeTheme.typography.textMedium, - color = CodeTheme.colors.textMain, - ) - - if (contact.isUnknown) { - Text( - modifier = Modifier.background( - color = CodeTheme.colors.surfaceVariant, - shape = CircleShape, - ).padding( - vertical = CodeTheme.dimens.grid.x1, - horizontal = CodeTheme.dimens.grid.x2, - ), - text = stringResource(R.string.label_unknownContact), - style = CodeTheme.typography.caption.copy(fontSize = 10.sp, lineHeight = 10.sp), - color = CodeTheme.colors.textSecondary, - ) - } - - ChatRowTrailing( - lastActivity = lastActivity, - unreadCount = unreadCount, - canOpen = isOnFlipcash, - ) - }, - subtitle = { - ChatRowSubtitle( - isTyping = isTyping, - preview = lastMessagePreview.takeIf { isOnFlipcash }, - fallback = { - SubtitleText( - if (isOnFlipcash) "${contact.displayName} joined Flipcash" - else contact.displayNumber - ) - } - ) - }, - endAction = { - if (!isOnFlipcash) { - Text( - modifier = Modifier - .background( - color = White10, // ButtonState.Filled10 - shape = CodeTheme.shapes.small, - ) - .padding( - horizontal = CodeTheme.dimens.grid.x2, - vertical = CodeTheme.dimens.grid.x1, - ), - text = stringResource(R.string.action_invite), - style = CodeTheme.typography.textMedium, - color = CodeTheme.colors.textMain, - ) - } - }, - showDivider = showDivider, - onClick = onClick, - ) -} - -@Composable -private fun EmptyContactsState( - modifier: Modifier = Modifier, -) { - Box( - modifier = modifier - .fillMaxSize() - .padding(bottom = CodeTheme.dimens.grid.x12), - contentAlignment = Alignment.Center, - ) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text( - text = stringResource(R.string.title_noContacts), - style = CodeTheme.typography.textLarge, - color = CodeTheme.colors.textMain, - ) - Text( - text = stringResource(R.string.subtitle_noContacts), - style = CodeTheme.typography.textSmall, - color = CodeTheme.colors.textSecondary, - ) - } - } -} - -@Composable -private fun EmptySearchState( - searchQuery: String, - modifier: Modifier = Modifier, -) { - Box( - modifier = modifier - .fillMaxSize() - .padding(bottom = CodeTheme.dimens.grid.x12), - contentAlignment = Alignment.Center, - ) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Image( - modifier = Modifier.size(CodeTheme.dimens.staticGrid.x13), - imageVector = Icons.Filled.Search, - contentDescription = null, - colorFilter = ColorFilter.tint(CodeTheme.colors.textSecondary), - ) - Text( - modifier = Modifier.padding(top = CodeTheme.dimens.grid.x5), - text = stringResource(R.string.title_noSearchResults, searchQuery), - style = CodeTheme.typography.textLarge, - color = CodeTheme.colors.textMain, - ) - Text( - text = stringResource(R.string.subtitle_noSearchResults), - style = CodeTheme.typography.textSmall, - color = CodeTheme.colors.textSecondary, - ) - } - } -} - -@Preview -@PreviewWrapper(FlipcashThemeWrapper::class) -@Composable -private fun ContactListPreview() { - val fakeNames = listOf( - "Alice Anderson", "Bob Baker", "Charlie Chen", "Dana Davis", - "Eli Evans", "Fiona Fisher", "George Garcia", "Hannah Hill", - "Isaac Ingram", "Julia Jones", "Kevin Kim", "Lenny Johnson", - "Maya Martinez", "Noah Nguyen", "Olivia Ortiz", "Paul Park", - "Quinn Quinn", "Rachel Robinson", "Sam Smith", "Tina Torres", - ) - - val flipcashContacts = fakeNames.take(6).mapIndexed { i, name -> - ContactListItem.ContactRow( - contact = DeviceContact( - e164 = "+1555000${1000 + i}", - androidContactId = i.toLong(), - displayName = name, - photoUri = null, - displayNumber = "(555) 000-${1000 + i}", - ), - isOnFlipcash = true, - ) - } - val otherContacts = fakeNames.drop(6).mapIndexed { i, name -> - ContactListItem.ContactRow( - contact = DeviceContact( - e164 = "+1555000${2000 + i}", - androidContactId = (100 + i).toLong(), - displayName = name, - photoUri = null, - displayNumber = "(555) 000-${2000 + i}", - ), - isOnFlipcash = false, - ) - } - - val now = Clock.System.now() - val recents = listOf( - // Unread conversation with a message preview - ContactListItem.ContactRow( - contact = flipcashContacts[0].contact, - isOnFlipcash = true, - lastActivity = now - 5.minutes, - conversation = ConversationReference( - chatId = ChatId(byteArrayOf(1)), - lastMessagePreview = "Sent you $10.00", - unreadCount = 2, - ), - ), - // Contact is currently typing - ContactListItem.ContactRow( - contact = flipcashContacts[1].contact, - isOnFlipcash = true, - lastActivity = now - 1.hours, - conversation = ConversationReference( - chatId = ChatId(byteArrayOf(2)), - lastMessagePreview = "See you soon", - isTyping = true, - ), - ), - // Read conversation from yesterday - ContactListItem.ContactRow( - contact = flipcashContacts[2].contact, - isOnFlipcash = true, - lastActivity = now - 26.hours, - conversation = ConversationReference( - chatId = ChatId(byteArrayOf(3)), - lastMessagePreview = "You: Thanks!", - ), - ), - ) - - val items = buildList { - addAll(recents) - // On-Flipcash contacts without a chat still rank by joinedAt, - // so they carry a lastActivity timestamp too. - addAll( - flipcashContacts.drop(3).mapIndexed { i, row -> - row.copy(lastActivity = now - (i + 2).days) - } - ) - add(ContactListItem.Header("Not On Flipcash Yet")) - addAll(otherContacts) - } - - ContactList( - items = items, - searchState = TextFieldState(), - listState = rememberLazyListState(), - accessHandle = ContactAccessHandle(launch = {}), - ) -} - -@Preview(heightDp = 1400) -@PreviewWrapper(FlipcashThemeWrapper::class) -@Composable -private fun ContactRowItemStatesPreview() { - val now = Clock.System.now() - val known = DeviceContact( - e164 = "+15550001234", - androidContactId = 1L, - displayName = "Bob Baker", - photoUri = null, - displayNumber = "(555) 000-1234", - ) - val unknown = DeviceContact.unknownContact( - e164 = "+15559998888", - displayName = "+1 (555) 999-8888", - displayNumber = "+1 (555) 999-8888", - ) - - Column( - modifier = Modifier - .fillMaxSize() - .background(CodeTheme.colors.background) - .verticalScroll(rememberScrollState()), - ) { - // On Flipcash · active chat · read (message preview, timestamp, chevron) - StateLabel("On Flipcash · active chat · read") - ContactRowItem( - contact = known, - isOnFlipcash = true, - lastMessagePreview = "See you tomorrow!", - lastActivity = now - 30.minutes, - ) {} - - // On Flipcash · active chat · unread (badge + indicator-colored timestamp) - StateLabel("On Flipcash · active chat · unread") - ContactRowItem( - contact = known, - isOnFlipcash = true, - lastMessagePreview = "Sent you $5.00", - lastActivity = now - 5.minutes, - unreadCount = 3, - ) {} - - // On Flipcash · typing (subtitle overridden by "is typing…") - StateLabel("On Flipcash · typing") - ContactRowItem( - contact = known, - isOnFlipcash = true, - lastMessagePreview = "Old message", - lastActivity = now - 2.hours, - isTyping = true, - ) {} - - // On Flipcash · typing + unread - StateLabel("On Flipcash · typing · unread") - ContactRowItem( - contact = known, - isOnFlipcash = true, - lastMessagePreview = "Old message", - lastActivity = now - 2.hours, - unreadCount = 1, - isTyping = true, - ) {} - - // On Flipcash · no chat yet (still shows joinedAt timestamp, no subtitle) - StateLabel("On Flipcash · no chat yet") - ContactRowItem( - contact = known, - isOnFlipcash = true, - lastActivity = now - 3.days, - ) {} - - // On Flipcash · long preview (single line, ellipsized) - StateLabel("On Flipcash · long preview (ellipsized)") - ContactRowItem( - contact = known, - isOnFlipcash = true, - lastMessagePreview = "This is a really long message preview that should be truncated with an ellipsis when it runs out of room", - lastActivity = now - 26.hours, - ) {} - - // On Flipcash · older activity (date formatting instead of time) - StateLabel("On Flipcash · older activity") - ContactRowItem( - contact = known, - isOnFlipcash = true, - lastMessagePreview = "Thanks!", - lastActivity = now - 4.days, - ) {} - - // On Flipcash · unknown contact ("Unknown" label replaces timestamp) - StateLabel("On Flipcash · unknown contact") - ContactRowItem( - contact = unknown, - isOnFlipcash = true, - lastMessagePreview = "Hey, is this you?", - lastActivity = now - 10.minutes, - ) {} - - // Not on Flipcash · invite (phone number subtitle + Invite chip) - StateLabel("Not on Flipcash · invite") - ContactRowItem( - contact = known, - isOnFlipcash = false, - ) {} - } -} - -@Composable -private fun StateLabel(text: String) { - Text( - modifier = Modifier.padding( - start = CodeTheme.dimens.inset, - top = CodeTheme.dimens.grid.x3, - bottom = CodeTheme.dimens.grid.x1, - ), - text = text, - style = CodeTheme.typography.caption, - color = CodeTheme.colors.textSecondary, - ) -} \ No newline at end of file diff --git a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/components/ContactPermissionCallout.kt b/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/components/ContactPermissionCallout.kt deleted file mode 100644 index d68e40580a..0000000000 --- a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/components/ContactPermissionCallout.kt +++ /dev/null @@ -1,102 +0,0 @@ -package com.flipcash.app.directsend.internal.screens.components - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.spring -import androidx.compose.animation.expandVertically -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.scaleIn -import androidx.compose.animation.scaleOut -import androidx.compose.animation.shrinkVertically -import androidx.compose.foundation.layout.size -import androidx.compose.material3.Icon -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import com.flipcash.app.core.android.extensions.launchAppSettings -import com.flipcash.app.core.ui.Callout -import com.flipcash.app.directsend.internal.SendFlowViewModel -import com.flipcash.app.permissions.ContactAccessHandle -import com.flipcash.features.directsend.R -import com.getcode.theme.CodeTheme -import com.getcode.util.permissions.PermissionResult - -@Composable -internal fun ContactPermissionCallout( - state: SendFlowViewModel.State, - accessHandle: ContactAccessHandle, - modifier: Modifier = Modifier, - onDismiss: () -> Unit, -) { - val context = LocalContext.current - AnimatedVisibility( - modifier = modifier, - visible = accessHandle.permissionStatus != PermissionResult.Granted && - !state.isPickerMode && - !state.hasCalloutBeenDismissed, - // Enter mirrors the reveal below: a soft, gently-overshooting spring - // that springs in and scales up from slightly small. - enter = fadeIn( - animationSpec = spring(stiffness = Spring.StiffnessLow), - ) + scaleIn( - initialScale = 0.92f, - animationSpec = spring( - dampingRatio = Spring.DampingRatioMediumBouncy, - stiffness = Spring.StiffnessLow, - ), - ) + expandVertically( - animationSpec = spring( - dampingRatio = Spring.DampingRatioMediumBouncy, - stiffness = Spring.StiffnessLow, - ), - ), - // Dismiss: scale + fade out while the height springs closed, so the - // list below slides up cleanly with no bounce. - exit = fadeOut( - animationSpec = spring(stiffness = Spring.StiffnessMedium), - ) + scaleOut( - targetScale = 0.9f, - animationSpec = spring( - dampingRatio = Spring.DampingRatioNoBouncy, - stiffness = Spring.StiffnessMedium, - ), - ) + shrinkVertically( - animationSpec = spring( - dampingRatio = Spring.DampingRatioNoBouncy, - stiffness = Spring.StiffnessMedium, - ), - ), - ) { - Callout( - title = stringResource(R.string.title_allowFullContactAccess), - description = stringResource(R.string.subtitle_allowFullContactAccess), - icon = { - Icon( - painter = painterResource(com.flipcash.core.R.drawable.ic_callout_warning), - contentDescription = null, - tint = CodeTheme.colors.warning, - modifier = Modifier.size(CodeTheme.dimens.staticGrid.x10), - ) - }, - actionLabel = when (accessHandle.permissionStatus) { - PermissionResult.Denied -> stringResource(R.string.action_settings) - PermissionResult.Granted -> "" // guarded by above - PermissionResult.NotRequested -> stringResource(R.string.action_allowAccess) - PermissionResult.PermanentlyDenied -> stringResource(R.string.action_settings) - }, - onDismiss = onDismiss, - onAction = { - when (accessHandle.permissionStatus) { - PermissionResult.NotRequested -> accessHandle.launch() - PermissionResult.Granted -> Unit - PermissionResult.Denied, - PermissionResult.PermanentlyDenied -> context.launchAppSettings() - } - }, - shape = CodeTheme.shapes.medium, - ) - } -} \ No newline at end of file diff --git a/apps/flipcash/features/direct-send/src/test/kotlin/com/flipcash/app/directsend/internal/SendFlowViewModelStateTest.kt b/apps/flipcash/features/direct-send/src/test/kotlin/com/flipcash/app/directsend/internal/SendFlowViewModelStateTest.kt deleted file mode 100644 index a014245064..0000000000 --- a/apps/flipcash/features/direct-send/src/test/kotlin/com/flipcash/app/directsend/internal/SendFlowViewModelStateTest.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.flipcash.app.directsend.internal - -import com.flipcash.app.core.send.SendStep -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNull - -class SendFlowViewModelStateTest { - - private val reduce = SendFlowViewModel.Companion.updateStateForEvent - - @Test - fun `default state has null currentStep`() { - assertNull(SendFlowViewModel.State().currentStep) - } - - @Test - fun `OnStepChanged updates currentStep`() { - val updated = reduce( - SendFlowViewModel.Event.OnStepChanged(SendStep.ContactList) - )(SendFlowViewModel.State()) - assertEquals(SendStep.ContactList, updated.currentStep) - } - - @Test - fun `OnStepChanged to PhoneGate updates currentStep`() { - val updated = reduce( - SendFlowViewModel.Event.OnStepChanged(SendStep.PhoneGate) - )(SendFlowViewModel.State()) - assertEquals(SendStep.PhoneGate, updated.currentStep) - } - - @Test - fun `OnStepChanged to ContactsGate updates currentStep`() { - val updated = reduce( - SendFlowViewModel.Event.OnStepChanged(SendStep.ContactsGate) - )(SendFlowViewModel.State()) - assertEquals(SendStep.ContactsGate, updated.currentStep) - } - - @Test - fun `OnStepChanged replaces previous step`() { - val state = SendFlowViewModel.State(currentStep = SendStep.PhoneGate) - val updated = reduce( - SendFlowViewModel.Event.OnStepChanged(SendStep.ContactList) - )(state) - assertEquals(SendStep.ContactList, updated.currentStep) - } - - @Test - fun `OnStepChanged does not affect other state`() { - val state = SendFlowViewModel.State( - steps = listOf(SendStep.PhoneGate, SendStep.ContactList), - isPickerMode = true, - ) - val updated = reduce( - SendFlowViewModel.Event.OnStepChanged(SendStep.ContactList) - )(state) - assertEquals(state.steps, updated.steps) - assertEquals(state.isPickerMode, updated.isPickerMode) - assertEquals(state.listItems, updated.listItems) - } -} diff --git a/docs/architecture/features/README.md b/docs/architecture/features/README.md index 488423cbfd..f99eb026e0 100644 --- a/docs/architecture/features/README.md +++ b/docs/architecture/features/README.md @@ -34,7 +34,6 @@ Two recurring shapes show up in the **Pattern** column: |---------|---------|--------------------|-----------------------| | **cash** | Amount entry for a device-to-device cash bill (sends the selected token — a launchpad currency or USDF) | `cash/CashScreen.kt` · `…/internal/CashScreenViewModel` | **VM-driven**; combines token + balance + rate flows, emits `PresentBill`, calls `SessionController.showBill` | | **scanner** | Camera capture of a peer's Kik Code (e.g. on a cash bill) | `scanner/ScannerScreen.kt` | Screen-local; uses `:ui:scanner`, routes scans through `Router.classify` | -| **direct-send** | Send to a contact / phone number | `directsend/SendFlowScreen.kt` · `…/internal/SendFlowViewModel` | **Flow**; contact list + phone gate steps | | **withdrawal** | Withdraw funds on-chain | `withdrawal/WithdrawalFlowScreen.kt` · `withdrawal/WithdrawalViewModel` | **Flow** (`AppRoute.Transfers.Withdrawal`); entry → destination → confirmation | | **deposit** | Add funds via on-ramp / deposit address | `deposit/DepositFlowScreen.kt` · `…/internal/DepositViewModel` | **Flow** (`FlowRouteWithResult`); USDC deposit info, Coinbase on-ramp | | **transactions** | Activity / transaction history | `transactions/TransactionHistoryScreen.kt` · `…/internal/TransactionHistoryViewModel` | **VM-driven**; Paging-backed list from persistence ([05](../05-persistence.md)) | @@ -59,9 +58,9 @@ Two recurring shapes show up in the **Pattern** column: ## Full module list -The 26 feature directories: `advanced`, `appsettings`, `appupdates`, `backupkey`, +The 25 feature directories: `advanced`, `appsettings`, `appupdates`, `backupkey`, `balance`, `bill-customization`, `cash`, `contact-verification`, `currency-creator`, -`deposit`, `device-logs`, `direct-send`, `discovery`, `invite`, `lab`, `login`, +`deposit`, `device-logs`, `discovery`, `invite`, `lab`, `login`, `menu`, `messenger`, `myaccount`, `purchase`, `scanner`, `shareapp`, `tokens`, `transactions`, `userflags`, `withdrawal`. From 9bedf82fbafc76f38e85e260f9079c311fd5dbf4 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 4 Sep 2026 12:41:40 -0400 Subject: [PATCH 2/2] docs(architecture): match the feature catalog to settings.gradle.kts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The catalog had drifted from the modules that actually exist. #1291 deleted the standalone App Settings screen and its module — its one surviving toggle moved into My Account — but the catalog still carried an `appsettings` row pointing at a directory that is gone, and `AppSettingsCoordinator` now lives in `:apps:flipcash:shared:appsettings`, not a feature. `tipping` and `user-profile` were missing from the full module list entirely, and neither had a row despite being user-facing flows. Both get one, replacing `appsettings` with `menu` — the "You" tab where those settings rows ended up. The list now matches `settings.gradle.kts` exactly: 26 modules, the same 26 the header claims. (`features/home/` still exists on disk as stale build output; it has no tracked files and left `settings.gradle.kts` with the module it held.) --- docs/architecture/features/README.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/architecture/features/README.md b/docs/architecture/features/README.md index f99eb026e0..b310b78041 100644 --- a/docs/architecture/features/README.md +++ b/docs/architecture/features/README.md @@ -36,6 +36,7 @@ Two recurring shapes show up in the **Pattern** column: | **scanner** | Camera capture of a peer's Kik Code (e.g. on a cash bill) | `scanner/ScannerScreen.kt` | Screen-local; uses `:ui:scanner`, routes scans through `Router.classify` | | **withdrawal** | Withdraw funds on-chain | `withdrawal/WithdrawalFlowScreen.kt` · `withdrawal/WithdrawalViewModel` | **Flow** (`AppRoute.Transfers.Withdrawal`); entry → destination → confirmation | | **deposit** | Add funds via on-ramp / deposit address | `deposit/DepositFlowScreen.kt` · `…/internal/DepositViewModel` | **Flow** (`FlowRouteWithResult`); USDC deposit info, Coinbase on-ramp | +| **tipping** | Tip card, tip chats, and tip amount entry | `tipping/TippingFlowScreen.kt`, `tipping/TipCardScreen.kt` · `…/internal/TipFlowViewModel` | **Flow** (`AppRoute.Sheets.Tips`); intro → tip card → tips steps | | **transactions** | Activity / transaction history | `transactions/TransactionHistoryScreen.kt` · `…/internal/TransactionHistoryViewModel` | **VM-driven**; Paging-backed list from persistence ([05](../05-persistence.md)) | ## C. Currencies & tokens @@ -52,17 +53,18 @@ Two recurring shapes show up in the **Pattern** column: |---------|---------|--------------------|-----------------------| | **myaccount** | Account screen + user profile | `myaccount/MyAccountScreen.kt`, `myaccount/UserProfileScreen.kt` · `…/internal/MyAccountScreenViewModel` | **VM-driven**; reactive feature-flag / staff binding, event-driven nav | | **messenger** | In-app chat & chat-amount entry | `messenger/ChatFlowScreen.kt` · `…/internal/ChatViewModel` | **Flow** (`AppRoute.Messaging.Chat`); Paging-backed messages | -| **appsettings** | App preferences | `apps/flipcash/features/appsettings/…` | **VM-driven**; backed by `AppSettingsCoordinator` (`LocalAppSettings`) | +| **menu** | The "You" tab — own tip card, share / download, and the settings list | `menu/MenuScreen.kt` · `…/internal/MenuScreenViewModel` | **VM-driven**; pushes the `AppRoute.Menu.*` rows (My Account, Advanced, Blocked, …) | +| **user-profile** | Edit display name, `@handle`, photo, and minimum DM tip | `userprofile/UserProfileSetupFlowScreen.kt` · `…/internal/name/NameEntryViewModel`, `…/username/UsernameEntryViewModel` | **Flow** (`FlowRouteWithResult`); caller picks which `UpdateProfileStep`s to run | | **invite / shareapp** | Invite & share flows | `apps/flipcash/features/invite`, `…/shareapp` | Screen-local; use `InviteController` / `ShareSheetController` locals | | **device-logs / lab / advanced** | Debug & internal tooling | `apps/flipcash/features/device-logs`, `…/lab`, `…/advanced` | Internal; surface trace logs and feature-flag overrides | ## Full module list -The 25 feature directories: `advanced`, `appsettings`, `appupdates`, `backupkey`, -`balance`, `bill-customization`, `cash`, `contact-verification`, `currency-creator`, -`deposit`, `device-logs`, `discovery`, `invite`, `lab`, `login`, -`menu`, `messenger`, `myaccount`, `purchase`, `scanner`, `shareapp`, `tokens`, -`transactions`, `userflags`, `withdrawal`. +The 26 feature directories: `advanced`, `appupdates`, `backupkey`, `balance`, +`bill-customization`, `cash`, `contact-verification`, `currency-creator`, `deposit`, +`device-logs`, `discovery`, `invite`, `lab`, `login`, `menu`, `messenger`, +`myaccount`, `purchase`, `scanner`, `shareapp`, `tipping`, `tokens`, `transactions`, +`user-profile`, `userflags`, `withdrawal`. > Keep this catalog representative, not exhaustive — when you add a feature, add a > row to the relevant category if it introduces a new user-facing capability or a