From 38fe659f0e89f6e9f78e9fcf66b63c4937f10717 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 23 Jul 2026 19:39:35 -0400 Subject: [PATCH] =?UTF-8?q?refactor(payments):=20add=20DM-transfer=20deleg?= =?UTF-8?q?ates=20module;=20rename=20old=20payments=E2=86=92funding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce shared:payments holding ContactPaymentDelegate and TipPaymentDelegate — the phone-number- and user-id-keyed cores for sending cash inside a DM (resolve recipient → attach chat-type metadata → directTransfer → debit local balance → sync feed). TippingCoordinator.confirmTip now delegates its transfer to TipPaymentDelegate; the out-of-chat tip flow is unchanged. Rename the existing shared:payments (add-money / purchase methods) to shared:funding so the "payments" name is free for the transfer module. Updates all consumers, Gradle paths, Android namespaces, Kotlin packages, and generated-R imports. Add an in-memory cache to ResolverController (an identity's on-chain address is effectively immutable), so a pre-send resolve and the delegate's send-time resolve collapse to a single network round-trip. Foundation for tip-chat sends in the messenger, which rebases on top of this. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_0197MAdYdxVWvdqRU6TEqBwh --- apps/flipcash/app/build.gradle.kts | 2 +- .../features/balance/build.gradle.kts | 2 +- .../app/balance/internal/BalanceViewModel.kt | 2 +- .../currency-creator/build.gradle.kts | 2 +- .../internal/CurrencyCreatorViewModel.kt | 2 +- .../features/direct-send/build.gradle.kts | 2 +- apps/flipcash/features/menu/build.gradle.kts | 2 +- .../app/menu/internal/MenuScreenViewModel.kt | 2 +- .../features/messenger/build.gradle.kts | 2 +- .../app/messenger/internal/ChatViewModel.kt | 2 +- .../shared/{payments => funding}/.gitignore | 0 apps/flipcash/shared/funding/build.gradle.kts | 16 ++++ .../flipcash/app/funding}/PurchaseMethod.kt | 2 +- .../app/funding}/PurchaseMethodController.kt | 2 +- .../app/funding}/PurchaseMethodState.kt | 2 +- .../flipcash/app/funding}/internal/Buttons.kt | 12 +-- .../InternalPurchaseMethodController.kt | 18 ++-- .../app/funding}/PurchaseMethodStateTest.kt | 2 +- .../flipcash/shared/payments/build.gradle.kts | 14 ++-- .../shared/payments/ContactPaymentDelegate.kt | 63 ++++++++++++++ .../shared/payments/TipPaymentDelegate.kt | 64 +++++++++++++++ apps/flipcash/shared/session/build.gradle.kts | 2 +- .../internal/delegates/DepositDelegate.kt | 2 +- apps/flipcash/shared/tipping/build.gradle.kts | 1 + .../shared/tipping/TippingCoordinator.kt | 82 ++++++------------- .../shared/tipping/TippingCoordinatorTest.kt | 11 +-- apps/flipcash/shared/tokens/build.gradle.kts | 4 +- .../app/tokens/inject/PurchaseMethodModule.kt | 4 +- .../flipcash/app/tokens/ui/SwapViewModel.kt | 6 +- .../app/tokens/ui/TokenInfoViewModel.kt | 2 +- .../app/tokens/ui/SwapViewModelErrorTest.kt | 2 +- .../controllers/ResolverController.kt | 12 +++ settings.gradle.kts | 3 +- 33 files changed, 235 insertions(+), 111 deletions(-) rename apps/flipcash/shared/{payments => funding}/.gitignore (100%) create mode 100644 apps/flipcash/shared/funding/build.gradle.kts rename apps/flipcash/shared/{payments/src/main/kotlin/com/flipcash/app/payments => funding/src/main/kotlin/com/flipcash/app/funding}/PurchaseMethod.kt (96%) rename apps/flipcash/shared/{payments/src/main/kotlin/com/flipcash/app/payments => funding/src/main/kotlin/com/flipcash/app/funding}/PurchaseMethodController.kt (93%) rename apps/flipcash/shared/{payments/src/main/kotlin/com/flipcash/app/payments => funding/src/main/kotlin/com/flipcash/app/funding}/PurchaseMethodState.kt (96%) rename apps/flipcash/shared/{payments/src/main/kotlin/com/flipcash/app/payments => funding/src/main/kotlin/com/flipcash/app/funding}/internal/Buttons.kt (95%) rename apps/flipcash/shared/{payments/src/main/kotlin/com/flipcash/app/payments => funding/src/main/kotlin/com/flipcash/app/funding}/internal/InternalPurchaseMethodController.kt (94%) rename apps/flipcash/shared/{payments/src/test/kotlin/com/flipcash/app/payments => funding/src/test/kotlin/com/flipcash/app/funding}/PurchaseMethodStateTest.kt (98%) create mode 100644 apps/flipcash/shared/payments/src/main/kotlin/com/flipcash/shared/payments/ContactPaymentDelegate.kt create mode 100644 apps/flipcash/shared/payments/src/main/kotlin/com/flipcash/shared/payments/TipPaymentDelegate.kt diff --git a/apps/flipcash/app/build.gradle.kts b/apps/flipcash/app/build.gradle.kts index fed6dd7cd1..a1f002fa5c 100644 --- a/apps/flipcash/app/build.gradle.kts +++ b/apps/flipcash/app/build.gradle.kts @@ -197,7 +197,7 @@ dependencies { implementation(libs.phantom.connect) { exclude(group = "com.ionspin.kotlin", module = "multiplatform-crypto-libsodium-bindings-android-debug") } - implementation(project(":apps:flipcash:shared:payments")) + implementation(project(":apps:flipcash:shared:funding")) implementation(project(":apps:flipcash:shared:permissions")) implementation(project(":apps:flipcash:shared:phone")) implementation(project(":apps:flipcash:shared:shareable")) diff --git a/apps/flipcash/features/balance/build.gradle.kts b/apps/flipcash/features/balance/build.gradle.kts index 799c2f7952..d249c016e1 100644 --- a/apps/flipcash/features/balance/build.gradle.kts +++ b/apps/flipcash/features/balance/build.gradle.kts @@ -13,7 +13,7 @@ dependencies { implementation(project(":apps:flipcash:shared:analytics")) implementation(project(":apps:flipcash:shared:featureflags")) - implementation(project(":apps:flipcash:shared:payments")) + implementation(project(":apps:flipcash:shared:funding")) implementation(project(":apps:flipcash:shared:tokens")) implementation(project(":apps:flipcash:shared:userflags")) implementation(project(":libs:datetime")) diff --git a/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/BalanceViewModel.kt b/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/BalanceViewModel.kt index bcc885a997..2a29fe792d 100644 --- a/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/BalanceViewModel.kt +++ b/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/BalanceViewModel.kt @@ -6,7 +6,7 @@ import com.flipcash.app.analytics.FlipcashAnalyticsService import com.flipcash.app.core.AppRoute import com.flipcash.app.featureflags.FeatureFlag import com.flipcash.app.featureflags.FeatureFlagController -import com.flipcash.app.payments.PurchaseMethodController +import com.flipcash.app.funding.PurchaseMethodController import com.flipcash.app.userflags.UserFlagsCoordinator import com.flipcash.services.internal.model.thirdparty.OnRampProvider import com.flipcash.services.user.AuthState diff --git a/apps/flipcash/features/currency-creator/build.gradle.kts b/apps/flipcash/features/currency-creator/build.gradle.kts index e8a5852129..8730cd6264 100644 --- a/apps/flipcash/features/currency-creator/build.gradle.kts +++ b/apps/flipcash/features/currency-creator/build.gradle.kts @@ -13,7 +13,7 @@ dependencies { implementation(project(":apps:flipcash:shared:featureflags")) implementation(project(":apps:flipcash:shared:currency-creator")) implementation(project(":apps:flipcash:shared:onramp:deeplinks")) - implementation(project(":apps:flipcash:shared:payments")) + implementation(project(":apps:flipcash:shared:funding")) implementation(project(":apps:flipcash:shared:session")) implementation(project(":apps:flipcash:shared:tokens")) implementation(project(":apps:flipcash:shared:userflags")) diff --git a/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/CurrencyCreatorViewModel.kt b/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/CurrencyCreatorViewModel.kt index 5d9cff40fd..3410090440 100644 --- a/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/CurrencyCreatorViewModel.kt +++ b/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/CurrencyCreatorViewModel.kt @@ -11,7 +11,7 @@ import com.flipcash.app.core.extensions.onResult import com.flipcash.app.core.tokens.CurrencyCreatorDraft import com.flipcash.app.core.tokens.CurrencyCreatorStep import com.flipcash.app.currencycreator.CurrencyCreatorCoordinator -import com.flipcash.app.payments.PurchaseMethodController +import com.flipcash.app.funding.PurchaseMethodController import com.flipcash.app.tokens.BalancePoller import com.flipcash.app.tokens.TokenCoordinator import com.flipcash.app.userflags.UserFlagsCoordinator diff --git a/apps/flipcash/features/direct-send/build.gradle.kts b/apps/flipcash/features/direct-send/build.gradle.kts index 3d9dab5279..a3c55dd240 100644 --- a/apps/flipcash/features/direct-send/build.gradle.kts +++ b/apps/flipcash/features/direct-send/build.gradle.kts @@ -19,7 +19,7 @@ dependencies { implementation(project(":libs:messaging")) implementation(project(":libs:permissions:bindings")) implementation(project(":apps:flipcash:shared:featureflags")) - implementation(project(":apps:flipcash:shared:payments")) + 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")) diff --git a/apps/flipcash/features/menu/build.gradle.kts b/apps/flipcash/features/menu/build.gradle.kts index ea18332708..e745bcafdd 100644 --- a/apps/flipcash/features/menu/build.gradle.kts +++ b/apps/flipcash/features/menu/build.gradle.kts @@ -12,7 +12,7 @@ dependencies { implementation(project(":apps:flipcash:shared:authentication")) implementation(project(":apps:flipcash:shared:featureflags")) implementation(project(":apps:flipcash:shared:menu")) - implementation(project(":apps:flipcash:shared:payments")) + implementation(project(":apps:flipcash:shared:funding")) implementation(project(":apps:flipcash:shared:userflags")) implementation(project(":libs:datetime")) diff --git a/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/MenuScreenViewModel.kt b/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/MenuScreenViewModel.kt index fe0d5f0831..91d6aeeada 100644 --- a/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/MenuScreenViewModel.kt +++ b/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/MenuScreenViewModel.kt @@ -12,7 +12,7 @@ import com.flipcash.app.featureflags.FeatureFlag import com.flipcash.app.core.toast.SystemToastController import com.flipcash.app.featureflags.FeatureFlagController import com.flipcash.app.menu.MenuItem -import com.flipcash.app.payments.PurchaseMethodController +import com.flipcash.app.funding.PurchaseMethodController import com.flipcash.app.updates.ReleaseStage import com.flipcash.app.updates.ReleaseStageProvider import com.flipcash.app.userflags.UserFlagsCoordinator diff --git a/apps/flipcash/features/messenger/build.gradle.kts b/apps/flipcash/features/messenger/build.gradle.kts index 23d0661c6b..f3d07f6fb5 100644 --- a/apps/flipcash/features/messenger/build.gradle.kts +++ b/apps/flipcash/features/messenger/build.gradle.kts @@ -13,7 +13,7 @@ dependencies { implementation(project(":apps:flipcash:shared:amount-entry")) implementation(project(":apps:flipcash:shared:contacts")) implementation(project(":apps:flipcash:shared:featureflags")) - implementation(project(":apps:flipcash:shared:payments")) + implementation(project(":apps:flipcash:shared:funding")) implementation(project(":apps:flipcash:shared:tokens")) implementation(project(":libs:vibrator:bindings")) implementation(project(":libs:messaging")) diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatViewModel.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatViewModel.kt index e35b5219eb..e41021ef61 100644 --- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatViewModel.kt +++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatViewModel.kt @@ -20,7 +20,7 @@ import com.flipcash.app.featureflags.FeatureFlagController import com.flipcash.shared.chat.models.ChatListItem import com.flipcash.shared.chat.models.ReceiptStatus import com.flipcash.shared.chat.models.SeparatorConfig -import com.flipcash.app.payments.PurchaseMethodController +import com.flipcash.app.funding.PurchaseMethodController import com.flipcash.app.tokens.TokenCoordinator import com.flipcash.features.messenger.R import com.flipcash.services.models.buildDmPaymentMetadata diff --git a/apps/flipcash/shared/payments/.gitignore b/apps/flipcash/shared/funding/.gitignore similarity index 100% rename from apps/flipcash/shared/payments/.gitignore rename to apps/flipcash/shared/funding/.gitignore diff --git a/apps/flipcash/shared/funding/build.gradle.kts b/apps/flipcash/shared/funding/build.gradle.kts new file mode 100644 index 0000000000..178401e7f9 --- /dev/null +++ b/apps/flipcash/shared/funding/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + alias(libs.plugins.flipcash.android.feature) +} + +android { + namespace = "${Gradle.flipcashNamespace}.shared.funding" +} + +dependencies { + implementation(project(":apps:flipcash:shared:analytics")) + implementation(project(":apps:flipcash:shared:featureflags")) + implementation(project(":apps:flipcash:shared:tokens:core")) + implementation(project(":apps:flipcash:shared:userflags")) + implementation(project(":libs:messaging")) + implementation(project(":services:flipcash")) +} diff --git a/apps/flipcash/shared/payments/src/main/kotlin/com/flipcash/app/payments/PurchaseMethod.kt b/apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/PurchaseMethod.kt similarity index 96% rename from apps/flipcash/shared/payments/src/main/kotlin/com/flipcash/app/payments/PurchaseMethod.kt rename to apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/PurchaseMethod.kt index 038ae6987b..1aed798b17 100644 --- a/apps/flipcash/shared/payments/src/main/kotlin/com/flipcash/app/payments/PurchaseMethod.kt +++ b/apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/PurchaseMethod.kt @@ -1,4 +1,4 @@ -package com.flipcash.app.payments +package com.flipcash.app.funding import com.getcode.opencode.model.financial.Fiat import com.getcode.opencode.model.financial.LocalFiat diff --git a/apps/flipcash/shared/payments/src/main/kotlin/com/flipcash/app/payments/PurchaseMethodController.kt b/apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/PurchaseMethodController.kt similarity index 93% rename from apps/flipcash/shared/payments/src/main/kotlin/com/flipcash/app/payments/PurchaseMethodController.kt rename to apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/PurchaseMethodController.kt index 4b287a6de3..1aedec1c10 100644 --- a/apps/flipcash/shared/payments/src/main/kotlin/com/flipcash/app/payments/PurchaseMethodController.kt +++ b/apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/PurchaseMethodController.kt @@ -1,4 +1,4 @@ -package com.flipcash.app.payments +package com.flipcash.app.funding import com.flipcash.app.core.AppRoute import kotlinx.coroutines.flow.Flow diff --git a/apps/flipcash/shared/payments/src/main/kotlin/com/flipcash/app/payments/PurchaseMethodState.kt b/apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/PurchaseMethodState.kt similarity index 96% rename from apps/flipcash/shared/payments/src/main/kotlin/com/flipcash/app/payments/PurchaseMethodState.kt rename to apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/PurchaseMethodState.kt index 190ab74e9c..4edd7da99a 100644 --- a/apps/flipcash/shared/payments/src/main/kotlin/com/flipcash/app/payments/PurchaseMethodState.kt +++ b/apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/PurchaseMethodState.kt @@ -1,4 +1,4 @@ -package com.flipcash.app.payments +package com.flipcash.app.funding import com.flipcash.services.internal.model.thirdparty.OnRampProvider import com.getcode.opencode.model.financial.LocalFiat diff --git a/apps/flipcash/shared/payments/src/main/kotlin/com/flipcash/app/payments/internal/Buttons.kt b/apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/internal/Buttons.kt similarity index 95% rename from apps/flipcash/shared/payments/src/main/kotlin/com/flipcash/app/payments/internal/Buttons.kt rename to apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/internal/Buttons.kt index a0b43829a9..a132bc0cbe 100644 --- a/apps/flipcash/shared/payments/src/main/kotlin/com/flipcash/app/payments/internal/Buttons.kt +++ b/apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/internal/Buttons.kt @@ -1,4 +1,4 @@ -package com.flipcash.app.payments.internal +package com.flipcash.app.funding.internal import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box @@ -19,11 +19,11 @@ import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.flipcash.app.core.money.formatted -import com.flipcash.app.payments.PaymentAction -import com.flipcash.app.payments.PurchaseMethod -import com.flipcash.app.payments.PurchaseMethodMetadata -import com.flipcash.app.payments.PurchaseMethodState -import com.flipcash.shared.payments.R +import com.flipcash.app.funding.PaymentAction +import com.flipcash.app.funding.PurchaseMethod +import com.flipcash.app.funding.PurchaseMethodMetadata +import com.flipcash.app.funding.PurchaseMethodState +import com.flipcash.shared.funding.R import com.getcode.manager.BottomBarAction import com.getcode.manager.BottomBarManager import com.getcode.opencode.model.financial.Fiat diff --git a/apps/flipcash/shared/payments/src/main/kotlin/com/flipcash/app/payments/internal/InternalPurchaseMethodController.kt b/apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/internal/InternalPurchaseMethodController.kt similarity index 94% rename from apps/flipcash/shared/payments/src/main/kotlin/com/flipcash/app/payments/internal/InternalPurchaseMethodController.kt rename to apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/internal/InternalPurchaseMethodController.kt index 795a938dc7..814751a646 100644 --- a/apps/flipcash/shared/payments/src/main/kotlin/com/flipcash/app/payments/internal/InternalPurchaseMethodController.kt +++ b/apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/internal/InternalPurchaseMethodController.kt @@ -1,4 +1,4 @@ -package com.flipcash.app.payments.internal +package com.flipcash.app.funding.internal import com.flipcash.app.analytics.Analytics import com.flipcash.app.analytics.FlipcashAnalyticsService @@ -7,19 +7,19 @@ import com.flipcash.app.core.tokens.FundingSource import com.flipcash.app.core.tokens.SwapPurpose import com.flipcash.app.featureflags.FeatureFlag import com.flipcash.app.featureflags.FeatureFlagController -import com.flipcash.app.payments.PaymentAction -import com.flipcash.app.payments.PurchaseMethod -import com.flipcash.app.payments.PurchaseMethodController -import com.flipcash.app.payments.PurchaseMethodMetadata -import com.flipcash.app.payments.PurchaseMethodSelection -import com.flipcash.app.payments.PurchaseMethodState -import com.flipcash.app.payments.PurchasePurpose +import com.flipcash.app.funding.PaymentAction +import com.flipcash.app.funding.PurchaseMethod +import com.flipcash.app.funding.PurchaseMethodController +import com.flipcash.app.funding.PurchaseMethodMetadata +import com.flipcash.app.funding.PurchaseMethodSelection +import com.flipcash.app.funding.PurchaseMethodState +import com.flipcash.app.funding.PurchasePurpose import com.flipcash.app.tokens.core.ReservesBalanceProvider import com.flipcash.app.userflags.UserFlagsCoordinator import com.flipcash.services.internal.model.thirdparty.OnRampProvider import com.flipcash.services.internal.model.thirdparty.OnRampType import com.flipcash.services.user.UserManager -import com.flipcash.shared.payments.R +import com.flipcash.shared.funding.R import com.getcode.manager.BottomBarManager import com.getcode.opencode.exchange.Exchange import com.getcode.opencode.model.financial.LocalFiat diff --git a/apps/flipcash/shared/payments/src/test/kotlin/com/flipcash/app/payments/PurchaseMethodStateTest.kt b/apps/flipcash/shared/funding/src/test/kotlin/com/flipcash/app/funding/PurchaseMethodStateTest.kt similarity index 98% rename from apps/flipcash/shared/payments/src/test/kotlin/com/flipcash/app/payments/PurchaseMethodStateTest.kt rename to apps/flipcash/shared/funding/src/test/kotlin/com/flipcash/app/funding/PurchaseMethodStateTest.kt index ad739e6d8f..3867692723 100644 --- a/apps/flipcash/shared/payments/src/test/kotlin/com/flipcash/app/payments/PurchaseMethodStateTest.kt +++ b/apps/flipcash/shared/funding/src/test/kotlin/com/flipcash/app/funding/PurchaseMethodStateTest.kt @@ -1,4 +1,4 @@ -package com.flipcash.app.payments +package com.flipcash.app.funding import kotlin.test.Test import kotlin.test.assertEquals diff --git a/apps/flipcash/shared/payments/build.gradle.kts b/apps/flipcash/shared/payments/build.gradle.kts index 2fb1f6948f..c2ade13a23 100644 --- a/apps/flipcash/shared/payments/build.gradle.kts +++ b/apps/flipcash/shared/payments/build.gradle.kts @@ -7,10 +7,14 @@ android { } dependencies { - implementation(project(":apps:flipcash:shared:analytics")) - implementation(project(":apps:flipcash:shared:featureflags")) - implementation(project(":apps:flipcash:shared:tokens:core")) - implementation(project(":apps:flipcash:shared:userflags")) - implementation(project(":libs:messaging")) + testImplementation(kotlin("test")) + testImplementation(libs.bundles.unit.testing) + testImplementation(libs.robolectric) + + implementation(project(":apps:flipcash:core")) + implementation(project(":apps:flipcash:shared:chat")) + implementation(project(":apps:flipcash:shared:contacts")) + implementation(project(":apps:flipcash:shared:tokens")) implementation(project(":services:flipcash")) + implementation(project(":services:opencode")) } diff --git a/apps/flipcash/shared/payments/src/main/kotlin/com/flipcash/shared/payments/ContactPaymentDelegate.kt b/apps/flipcash/shared/payments/src/main/kotlin/com/flipcash/shared/payments/ContactPaymentDelegate.kt new file mode 100644 index 0000000000..3997b30f5d --- /dev/null +++ b/apps/flipcash/shared/payments/src/main/kotlin/com/flipcash/shared/payments/ContactPaymentDelegate.kt @@ -0,0 +1,63 @@ +package com.flipcash.shared.payments + +import com.flipcash.app.contacts.ContactCoordinator +import com.flipcash.app.core.contacts.DeviceContact +import com.flipcash.app.tokens.TokenCoordinator +import com.flipcash.services.models.buildDmPaymentMetadata +import com.flipcash.services.models.chat.ChatId +import com.flipcash.shared.chat.ChatCoordinator +import com.getcode.opencode.controllers.TransactionController +import com.getcode.opencode.exchange.VerifiedFiat +import com.getcode.opencode.model.accounts.AccountCluster +import com.getcode.opencode.model.financial.Token +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Sends cash within a contact (phone-number) DM: resolves the contact's on-chain owner, attaches + * contact-DM app metadata (source + destination phone number), transfers, debits the local + * balance, and syncs the chat feed. + * + * The phone-based counterpart to [TipPaymentDelegate] (keyed by a user id, attaching tip-DM + * metadata). Performs no UI or navigation — the caller owns send state and error presentation. + */ +@Singleton +class ContactPaymentDelegate @Inject constructor( + private val contactCoordinator: ContactCoordinator, + private val transactionController: TransactionController, + private val tokenCoordinator: TokenCoordinator, + private val chatCoordinator: ChatCoordinator, +) { + suspend fun send( + contact: DeviceContact, + chatId: ChatId?, + verifiedFiat: VerifiedFiat, + token: Token, + source: AccountCluster, + ): Result { + val appMetadataBytes = buildDmPaymentMetadata( + chatId = chatId, + sourcePhone = contactCoordinator.selfPhone, + destinationPhone = contact.e164, + ) + + return contactCoordinator.resolve(contact.e164) + .mapCatching { destination -> + transactionController.directTransfer( + amount = verifiedFiat, + token = token, + source = source, + destinationOwner = destination, + appMetadata = appMetadataBytes, + ).getOrThrow() + tokenCoordinator.subtract(token, verifiedFiat.localFiat) + if (chatId != null) { + chatCoordinator.loadMessages(chatId) + } else { + // New conversation — server just created the DM chat. + // Sync the feed so it appears in the contact list. + chatCoordinator.refreshFeed() + } + } + } +} diff --git a/apps/flipcash/shared/payments/src/main/kotlin/com/flipcash/shared/payments/TipPaymentDelegate.kt b/apps/flipcash/shared/payments/src/main/kotlin/com/flipcash/shared/payments/TipPaymentDelegate.kt new file mode 100644 index 0000000000..de09ad1ca8 --- /dev/null +++ b/apps/flipcash/shared/payments/src/main/kotlin/com/flipcash/shared/payments/TipPaymentDelegate.kt @@ -0,0 +1,64 @@ +package com.flipcash.shared.payments + +import com.flipcash.app.tokens.TokenCoordinator +import com.flipcash.services.controllers.ResolverController +import com.flipcash.services.models.buildTipDmPaymentMetadata +import com.flipcash.services.models.chat.ChatId +import com.flipcash.shared.chat.ChatCoordinator +import com.getcode.opencode.controllers.TransactionController +import com.getcode.opencode.exchange.VerifiedFiat +import com.getcode.opencode.model.accounts.AccountCluster +import com.getcode.opencode.model.core.ID +import com.getcode.opencode.model.financial.Token +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Sends cash within a tip DM: derives the canonical tip chat for [ID], resolves the recipient's + * on-chain owner, attaches tip-DM app metadata, transfers, debits the local balance, and syncs the + * chat feed. Returns the canonical tip [ChatId] (for message reload / navigation), or null if it + * couldn't be derived. + * + * The user-id counterpart to [ContactPaymentDelegate] (keyed by a phone number, attaching + * contact-DM metadata). Shared by the out-of-chat tip flow + * ([com.flipcash.shared.tipping.TippingCoordinator]) and the in-chat send (a tip DM opened in the + * messenger). Performs no UI or navigation — the caller owns send state, error presentation, and + * any post-send navigation. + */ +@Singleton +class TipPaymentDelegate @Inject constructor( + private val resolverController: ResolverController, + private val transactionController: TransactionController, + private val tokenCoordinator: TokenCoordinator, + private val chatCoordinator: ChatCoordinator, +) { + suspend fun send( + userId: ID, + verifiedFiat: VerifiedFiat, + token: Token, + source: AccountCluster, + ): Result { + val canonicalChatId = chatCoordinator.generateChatId(userId = userId).getOrNull() + val appMetadataBytes = buildTipDmPaymentMetadata(chatId = canonicalChatId) + + return resolverController.resolve(userId = userId) + .mapCatching { destination -> + transactionController.directTransfer( + amount = verifiedFiat, + token = token, + source = source, + destinationOwner = destination, + appMetadata = appMetadataBytes, + ).getOrThrow() + tokenCoordinator.subtract(token, verifiedFiat.localFiat) + if (canonicalChatId != null) { + chatCoordinator.loadMessages(canonicalChatId) + } else { + // New conversation — server just created the DM chat. + // Sync the feed so it appears in the contact list. + chatCoordinator.refreshFeed() + } + canonicalChatId + } + } +} diff --git a/apps/flipcash/shared/session/build.gradle.kts b/apps/flipcash/shared/session/build.gradle.kts index 49d318eacd..f0b4689ff9 100644 --- a/apps/flipcash/shared/session/build.gradle.kts +++ b/apps/flipcash/shared/session/build.gradle.kts @@ -20,7 +20,7 @@ dependencies { implementation(project(":apps:flipcash:shared:appsettings")) implementation(project(":apps:flipcash:shared:google-play-billing")) implementation(project(":apps:flipcash:shared:featureflags")) - implementation(project(":apps:flipcash:shared:payments")) + implementation(project(":apps:flipcash:shared:funding")) implementation(project(":apps:flipcash:shared:shareable")) implementation(project(":apps:flipcash:shared:tipping")) implementation(project(":apps:flipcash:shared:tokens")) diff --git a/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/delegates/DepositDelegate.kt b/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/delegates/DepositDelegate.kt index c7738f0568..0c55471c54 100644 --- a/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/delegates/DepositDelegate.kt +++ b/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/delegates/DepositDelegate.kt @@ -5,7 +5,7 @@ import com.flipcash.app.analytics.FlipcashAnalyticsService import com.flipcash.app.core.AppRoute import com.flipcash.app.featureflags.FeatureFlag import com.flipcash.app.featureflags.FeatureFlagController -import com.flipcash.app.payments.PurchaseMethodController +import com.flipcash.app.funding.PurchaseMethodController import com.flipcash.app.session.DepositOperations import com.flipcash.app.session.internal.SessionStateHolder import com.flipcash.app.tokens.UsdcDepositSweep diff --git a/apps/flipcash/shared/tipping/build.gradle.kts b/apps/flipcash/shared/tipping/build.gradle.kts index a8f7e2925e..2276a8fa10 100644 --- a/apps/flipcash/shared/tipping/build.gradle.kts +++ b/apps/flipcash/shared/tipping/build.gradle.kts @@ -14,6 +14,7 @@ dependencies { implementation(project(":apps:flipcash:shared:analytics")) implementation(project(":apps:flipcash:shared:chat")) implementation(project(":apps:flipcash:shared:payments")) + implementation(project(":apps:flipcash:shared:funding")) implementation(project(":apps:flipcash:shared:region-selection:core")) implementation(project(":apps:flipcash:shared:tokens")) implementation(project(":apps:flipcash:shared:userflags")) diff --git a/apps/flipcash/shared/tipping/src/main/kotlin/com/flipcash/shared/tipping/TippingCoordinator.kt b/apps/flipcash/shared/tipping/src/main/kotlin/com/flipcash/shared/tipping/TippingCoordinator.kt index f4159074a0..0f0fdc20a2 100644 --- a/apps/flipcash/shared/tipping/src/main/kotlin/com/flipcash/shared/tipping/TippingCoordinator.kt +++ b/apps/flipcash/shared/tipping/src/main/kotlin/com/flipcash/shared/tipping/TippingCoordinator.kt @@ -7,16 +7,13 @@ import com.flipcash.app.core.tipping.TipEvent import com.flipcash.app.core.tipping.TipSelectionHolder import com.flipcash.app.core.tipping.TipSelectionState import com.flipcash.app.currency.PreferredCurrencyController -import com.flipcash.app.payments.PurchaseMethodController +import com.flipcash.app.funding.PurchaseMethodController import com.flipcash.app.tokens.TokenCoordinator import com.flipcash.app.userflags.UserFlagsCoordinator import com.flipcash.services.controllers.ProfileController -import com.flipcash.services.controllers.ResolverController import com.flipcash.services.models.UserProfile -import com.flipcash.services.models.buildDmPaymentMetadata -import com.flipcash.services.models.buildTipDmPaymentMetadata import com.flipcash.services.user.UserManager -import com.flipcash.shared.chat.ChatCoordinator +import com.flipcash.shared.payments.TipPaymentDelegate import com.getcode.manager.BottomBarAction import com.getcode.manager.BottomBarManager import com.getcode.opencode.controllers.TransactionController @@ -67,15 +64,13 @@ class TippingCoordinator @Inject constructor( userFlags: UserFlagsCoordinator, private val profileController: ProfileController, private val userManager: UserManager, - private val resolverController: ResolverController, + private val tipPaymentDelegate: TipPaymentDelegate, private val exchange: Exchange, private val tokenCoordinator: TokenCoordinator, private val transactionController: TransactionController, private val verifiedFiatCalculator: VerifiedFiatCalculator, private val resources: ResourceHelper, - private val chatCoordinator: ChatCoordinator, private val purchaseMethodController: PurchaseMethodController, - ) : TipSelectionHolder { /** The signed-in user's id ([UserManager.accountId]), or null if unavailable. */ val currentUserId: ID? @@ -245,67 +240,38 @@ class TippingCoordinator @Inject constructor( return@launch } - val canonicalChatId = chatCoordinator.generateChatId(userId = userForTip).getOrNull() - - val appMetadataBytes = buildTipDmPaymentMetadata( - chatId = canonicalChatId, - ) - - resolverController.resolve(userId = userForTip) - .fold( - onSuccess = { destination -> - transactionController.directTransfer( - amount = verifiedFiat, - token = token, - source = source, - destinationOwner = destination, - appMetadata = appMetadataBytes, - ) - }, - onFailure = { - Result.failure(it) - } - ).fold( - onSuccess = { - tokenCoordinator.subtract(token, verifiedFiat.localFiat) - Result.success(verifiedFiat) - }, - onFailure = { Result.failure(it) } - ).onSuccess { amount -> - setSendState(LoadingSuccessState(success = true)) - if (canonicalChatId != null) { - chatCoordinator.loadMessages(canonicalChatId) - } else { - // New conversation — server just created the DM chat. - // Sync the feed so it appears in the contact list. - chatCoordinator.refreshFeed() - } - delay(400.milliseconds) - setSendState(LoadingSuccessState()) + tipPaymentDelegate.send( + userId = userForTip, + verifiedFiat = verifiedFiat, + token = token, + source = source, + ).onSuccess { canonicalChatId -> + setSendState(LoadingSuccessState(success = true)) + delay(400.milliseconds) + setSendState(LoadingSuccessState()) // analytics.transfer( // event = Analytics.Transfer.SentCash, // amount = verifiedFiat.localFiat, // successful = true, // ) - // Hand off to the tipped user's chat via the tips flow, so backing out of the - // chat lands on the tips list. - canonicalChatId?.let { - _events.emit(TipEvent.LaunchChat(ChatIdentifier.ByChatId(it))) - } - - }.onFailure { cause -> - setSendState(LoadingSuccessState()) + // Hand off to the tipped user's chat via the tips flow, so backing out of the + // chat lands on the tips list. + canonicalChatId?.let { + _events.emit(TipEvent.LaunchChat(ChatIdentifier.ByChatId(it))) + } + }.onFailure { + setSendState(LoadingSuccessState()) // analytics.transfer( // event = Analytics.Transfer.SentCash, // amount = verifiedFiat.localFiat, // error = cause, // ) - BottomBarManager.showError( - title = resources.getString(R.string.error_title_cashFailedToSend), - message = resources.getString(R.string.error_description_cashFailedToSend), - ) - } + BottomBarManager.showError( + title = resources.getString(R.string.error_title_cashFailedToSend), + message = resources.getString(R.string.error_description_cashFailedToSend), + ) + } } } diff --git a/apps/flipcash/shared/tipping/src/test/kotlin/com/flipcash/shared/tipping/TippingCoordinatorTest.kt b/apps/flipcash/shared/tipping/src/test/kotlin/com/flipcash/shared/tipping/TippingCoordinatorTest.kt index 119178d5cf..1bd2e957e2 100644 --- a/apps/flipcash/shared/tipping/src/test/kotlin/com/flipcash/shared/tipping/TippingCoordinatorTest.kt +++ b/apps/flipcash/shared/tipping/src/test/kotlin/com/flipcash/shared/tipping/TippingCoordinatorTest.kt @@ -1,14 +1,13 @@ package com.flipcash.shared.tipping import com.flipcash.app.currency.PreferredCurrencyController -import com.flipcash.app.payments.PurchaseMethodController +import com.flipcash.app.funding.PurchaseMethodController import com.flipcash.app.tokens.TokenCoordinator import com.flipcash.app.userflags.UserFlagsCoordinator import com.flipcash.services.controllers.ProfileController -import com.flipcash.services.controllers.ResolverController import com.flipcash.services.models.UserProfile import com.flipcash.services.user.UserManager -import com.flipcash.shared.chat.ChatCoordinator +import com.flipcash.shared.payments.TipPaymentDelegate import com.getcode.opencode.controllers.TransactionController import com.getcode.opencode.exchange.Exchange import com.getcode.opencode.exchange.VerifiedFiatCalculator @@ -36,7 +35,7 @@ class TippingCoordinatorTest { private val profileController = mockk() private val userManager = mockk() - private val resolverController = mockk() + private val tipPaymentDelegate = mockk(relaxed = true) private val userFlags = mockk(relaxed = true) private val preferredCurrency = mockk(relaxed = true) private val exchange = mockk(relaxed = true) @@ -44,7 +43,6 @@ class TippingCoordinatorTest { private val transactionController = mockk(relaxed = true) private val verifiedFiatCalculator = mockk(relaxed = true) private val resources = mockk(relaxed = true) - private val chatCoordinator = mockk(relaxed = true) private val purchaseMethodController = mockk(relaxed = true) // Per-transaction send limit of $100 for USD; no limit for any other currency. @@ -58,13 +56,12 @@ class TippingCoordinatorTest { userFlags, profileController, userManager, - resolverController, + tipPaymentDelegate, exchange, tokenCoordinator, transactionController, verifiedFiatCalculator, resources, - chatCoordinator, purchaseMethodController, ) diff --git a/apps/flipcash/shared/tokens/build.gradle.kts b/apps/flipcash/shared/tokens/build.gradle.kts index a107d66cf2..9b516fe608 100644 --- a/apps/flipcash/shared/tokens/build.gradle.kts +++ b/apps/flipcash/shared/tokens/build.gradle.kts @@ -19,13 +19,13 @@ dependencies { implementation(libs.androidx.lifecycle.process) implementation(project(":apps:flipcash:shared:amount-entry")) - implementation(project(":apps:flipcash:shared:payments")) + implementation(project(":apps:flipcash:shared:funding")) implementation(project(":apps:flipcash:shared:activityfeed")) implementation(project(":apps:flipcash:shared:analytics")) implementation(project(":apps:flipcash:shared:featureflags")) implementation(project(":apps:flipcash:shared:onramp:coinbase")) implementation(project(":apps:flipcash:shared:onramp:deeplinks")) - implementation(project(":apps:flipcash:shared:payments")) + implementation(project(":apps:flipcash:shared:funding")) implementation(project(":apps:flipcash:shared:persistence:sources")) implementation(project(":apps:flipcash:shared:shareable")) implementation(project(":apps:flipcash:shared:userflags")) diff --git a/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/inject/PurchaseMethodModule.kt b/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/inject/PurchaseMethodModule.kt index b59efa6e89..5045681288 100644 --- a/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/inject/PurchaseMethodModule.kt +++ b/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/inject/PurchaseMethodModule.kt @@ -1,7 +1,7 @@ package com.flipcash.app.tokens.inject -import com.flipcash.app.payments.PurchaseMethodController -import com.flipcash.app.payments.internal.InternalPurchaseMethodController +import com.flipcash.app.funding.PurchaseMethodController +import com.flipcash.app.funding.internal.InternalPurchaseMethodController import dagger.Binds import dagger.Module import dagger.hilt.InstallIn diff --git a/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/SwapViewModel.kt b/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/SwapViewModel.kt index 101afe5cb5..718e8d7425 100644 --- a/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/SwapViewModel.kt +++ b/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/SwapViewModel.kt @@ -25,9 +25,9 @@ import com.flipcash.app.onramp.PurchaseGate import com.flipcash.app.onramp.isAlert import com.flipcash.app.onramp.isNetworkCause import com.flipcash.app.onramp.messaging -import com.flipcash.app.payments.PurchaseMethod -import com.flipcash.app.payments.PurchaseMethodController -import com.flipcash.app.payments.PurchaseMethodMetadata +import com.flipcash.app.funding.PurchaseMethod +import com.flipcash.app.funding.PurchaseMethodController +import com.flipcash.app.funding.PurchaseMethodMetadata import com.flipcash.app.tokens.TokenCoordinator import com.flipcash.app.tokens.UsdcDepositSweep import com.flipcash.app.userflags.UserFlagsCoordinator diff --git a/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/TokenInfoViewModel.kt b/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/TokenInfoViewModel.kt index 67161b9c36..5df23ab7f0 100644 --- a/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/TokenInfoViewModel.kt +++ b/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/TokenInfoViewModel.kt @@ -7,7 +7,7 @@ import com.flipcash.app.core.data.isLoaded import com.flipcash.app.core.tokens.SwapPurpose import com.flipcash.app.featureflags.FeatureFlag import com.flipcash.app.featureflags.FeatureFlagController -import com.flipcash.app.payments.PurchaseMethodController +import com.flipcash.app.funding.PurchaseMethodController import com.flipcash.app.shareable.ShareSheetController import com.flipcash.app.shareable.Shareable diff --git a/apps/flipcash/shared/tokens/src/test/kotlin/com/flipcash/app/tokens/ui/SwapViewModelErrorTest.kt b/apps/flipcash/shared/tokens/src/test/kotlin/com/flipcash/app/tokens/ui/SwapViewModelErrorTest.kt index b39d31918c..093dee0284 100644 --- a/apps/flipcash/shared/tokens/src/test/kotlin/com/flipcash/app/tokens/ui/SwapViewModelErrorTest.kt +++ b/apps/flipcash/shared/tokens/src/test/kotlin/com/flipcash/app/tokens/ui/SwapViewModelErrorTest.kt @@ -5,7 +5,7 @@ import com.flipcash.app.analytics.FlipcashAnalyticsService import com.flipcash.app.core.tokens.SwapPurpose import com.flipcash.app.featureflags.FeatureFlagController import com.flipcash.app.onramp.CoinbaseOnRampController -import com.flipcash.app.payments.PurchaseMethodController +import com.flipcash.app.funding.PurchaseMethodController import com.flipcash.app.tokens.TokenCoordinator import com.flipcash.app.tokens.UsdcDepositSweep import com.flipcash.services.user.UserManager diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/ResolverController.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/ResolverController.kt index 3740fd76f7..702ce71715 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/ResolverController.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/ResolverController.kt @@ -6,12 +6,22 @@ import com.flipcash.services.repository.ResolverRepository import com.flipcash.services.user.UserManager import com.getcode.opencode.model.core.ID import com.getcode.solana.keys.PublicKey +import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject +import javax.inject.Singleton +@Singleton class ResolverController @Inject constructor( private val repository: ResolverRepository, private val userManager: UserManager, ) { + // Memoizes successful resolutions for the process lifetime. An identity's on-chain address is + // effectively immutable, and callers commonly resolve the same identity twice in quick + // succession (e.g. a chat pre-resolves the recipient to gate the send button, then the payment + // delegate resolves it again at send time) — the cache turns the second into a no-op. Failures + // are never cached, so an unresolvable identity is retried. + private val cache = ConcurrentHashMap() + /** Resolves a phone number to its on-chain address. */ suspend fun resolve(phone: ContactMethod.Phone): Result = resolve(ResolveIdentifier.Phone(phone)) @@ -21,8 +31,10 @@ class ResolverController @Inject constructor( resolve(ResolveIdentifier.UserId(userId)) private suspend fun resolve(identifier: ResolveIdentifier): Result { + cache[identifier]?.let { return Result.success(it) } val owner = userManager.accountCluster?.authority?.keyPair ?: return Result.failure(Throwable("No account cluster in UserManager")) return repository.resolve(owner, identifier) + .onSuccess { cache[identifier] = it } } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 33d3091fc5..872d6660a2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -58,6 +58,7 @@ include( ":apps:flipcash:shared:common-ui", ":apps:flipcash:shared:contacts", ":apps:flipcash:shared:currency-creator", + ":apps:flipcash:shared:payments", ":apps:flipcash:shared:onramp:coinbase", ":apps:flipcash:shared:onramp:deeplinks", ":apps:flipcash:shared:appupdates", @@ -68,7 +69,7 @@ include( ":apps:flipcash:shared:ksp", ":apps:flipcash:shared:menu", ":apps:flipcash:shared:notifications", - ":apps:flipcash:shared:payments", + ":apps:flipcash:shared:funding", ":apps:flipcash:shared:permissions", ":apps:flipcash:shared:push", ":apps:flipcash:shared:persistence:db",