From e7ef5d9736b07e06fa509a72508a6b6784ae6414 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Wed, 22 Jul 2026 13:18:47 -0400 Subject: [PATCH] refactor(scannable): render Scannables through the bill container - Reorganize bills: RenderedBill -> ScannableRenderer, AnimatedBill -> AnimatedScannable, composables into components/{bills,cards,receipts} (adds the TipCard renderer); BillContainerView -> ScannableContainer. - Widen BillState.bill to Scannable? and add SessionController.showTipCard so a non-payment tip card can be presented in the container (no grab/valuation); add tip SessionState fields wired to the new Tipping feature flag. - Theme: add the tipCard color token; bump the xxl corner shape. - Incidental: enable Gradle parallel tooling; small ResolverService refactor. --- .../com/flipcash/app/core/bill/BillState.kt | 11 +- .../BillCustomizationScaffold.kt | 6 +- .../screens/BillCustomizationScreen.kt | 7 +- .../internal/screens/ReviewScreen.kt | 7 +- .../flipcash/app/scanner/internal/Scanner.kt | 6 +- ...ContainerView.kt => ScannableContainer.kt} | 129 +++++++++++------- apps/flipcash/shared/bills/build.gradle.kts | 1 + .../{AnimatedBill.kt => AnimatedScannable.kt} | 14 +- .../com/flipcash/app/bills/RenderedBill.kt | 60 -------- .../flipcash/app/bills/ScannableRenderer.kt | 80 +++++++++++ .../bills/{ => components}/ScannableCode.kt | 2 +- .../{ => components/bills}/BillAmount.kt | 5 +- .../bills/{ => components/bills}/CashBill.kt | 16 +-- .../bills/{ => components/bills}/GoldBar.kt | 42 +++--- .../app/bills/components/cards/TipCard.kt | 86 ++++++++++++ .../receipts/PaymentReceipt.kt} | 3 +- .../flipcash/app/featureflags/FeatureFlag.kt | 11 ++ .../flipcash/app/session/SessionController.kt | 9 ++ .../session/internal/RealSessionController.kt | 17 ++- .../delegates/BillPresentationDelegate.kt | 8 ++ .../delegates/GiftCardSharingDelegate.kt | 2 +- .../theme/internal/FlipcashDesignSystem.kt | 1 + gradle.properties | 3 + .../network/services/ResolverService.kt | 28 ++-- .../main/kotlin/com/getcode/theme/Shape.kt | 2 +- .../main/kotlin/com/getcode/theme/Theme.kt | 6 + 26 files changed, 372 insertions(+), 190 deletions(-) rename apps/flipcash/features/scanner/src/main/kotlin/com/flipcash/app/scanner/internal/bills/{BillContainerView.kt => ScannableContainer.kt} (70%) rename apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/{AnimatedBill.kt => AnimatedScannable.kt} (85%) delete mode 100644 apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/RenderedBill.kt create mode 100644 apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/ScannableRenderer.kt rename apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/{ => components}/ScannableCode.kt (98%) rename apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/{ => components/bills}/BillAmount.kt (92%) rename apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/{ => components/bills}/CashBill.kt (97%) rename apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/{ => components/bills}/GoldBar.kt (97%) create mode 100644 apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/components/cards/TipCard.kt rename apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/{PaymentReceiptBill.kt => components/receipts/PaymentReceipt.kt} (97%) diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/bill/BillState.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/bill/BillState.kt index 55d11b06d9..20457a97de 100644 --- a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/bill/BillState.kt +++ b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/bill/BillState.kt @@ -11,7 +11,7 @@ import com.getcode.opencode.model.financial.Token import kotlin.time.Duration data class BillState( - val bill: Scannable.Payable?, + val bill: Scannable?, val showToast: Boolean, val toast: BillToast?, val valuation: Valuation?, @@ -19,10 +19,15 @@ data class BillState( val secondaryAction: Action?, ) { val canSwipeToDismiss: Boolean - get() = bill?.canSwipeToDismiss == true + get() = when (val b = bill) { + null -> false + is Scannable.Payable -> b.canSwipeToDismiss + else -> true // non-payment scannables (e.g. TipCard) are dismissable + } val confirmationDelayMillis: Int - get() = (bill?.confirmationDelay ?: Duration.ZERO).inWholeMilliseconds.toInt() + get() = ((bill as? Scannable.Payable)?.confirmationDelay ?: Duration.ZERO) + .inWholeMilliseconds.toInt() companion object { val Default = BillState( diff --git a/apps/flipcash/features/bill-customization/src/main/kotlin/com/flipcash/app/bill/customization/BillCustomizationScaffold.kt b/apps/flipcash/features/bill-customization/src/main/kotlin/com/flipcash/app/bill/customization/BillCustomizationScaffold.kt index 720d2ea37a..808faca55b 100644 --- a/apps/flipcash/features/bill-customization/src/main/kotlin/com/flipcash/app/bill/customization/BillCustomizationScaffold.kt +++ b/apps/flipcash/features/bill-customization/src/main/kotlin/com/flipcash/app/bill/customization/BillCustomizationScaffold.kt @@ -40,11 +40,9 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.flipcash.app.bill.customization.components.BillPlayground -import com.flipcash.app.bills.AnimatedBill -import com.flipcash.app.core.bill.Scannable +import com.flipcash.app.bills.AnimatedScannable import com.flipcash.features.bill.playground.R import com.getcode.theme.CodeTheme -import com.getcode.ui.components.AppBarDefaults import com.getcode.ui.core.measured import androidx.compose.foundation.clickable import com.getcode.ui.utils.AnimationUtils @@ -104,7 +102,7 @@ fun BillPlaygroundScaffold(content: @Composable () -> Unit) { Box(modifier = Modifier.fillMaxSize()) { content() - AnimatedBill( + AnimatedScannable( modifier = Modifier.fillMaxSize(), dismissState = billDismissState, dismissed = !isUsingPlayground, diff --git a/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/screens/BillCustomizationScreen.kt b/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/screens/BillCustomizationScreen.kt index 0feaa046d7..a482bba356 100644 --- a/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/screens/BillCustomizationScreen.kt +++ b/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/screens/BillCustomizationScreen.kt @@ -18,8 +18,7 @@ import com.flipcash.app.bill.customization.Event import com.flipcash.app.bill.customization.LocalBillPlaygroundController import com.flipcash.app.bill.customization.PlaygroundContext import com.flipcash.app.bill.customization.components.BillPlayground -import com.flipcash.app.bills.RenderedBill -import com.flipcash.app.core.bill.Scannable +import com.flipcash.app.bills.ScannableRenderer import com.flipcash.app.core.ui.transitions.SharedTransition import com.flipcash.app.core.ui.transitions.sharedBoundsTransition import com.flipcash.app.currencycreator.internal.CurrencyCreatorViewModel @@ -88,7 +87,7 @@ internal fun BillCustomizationContent( horizontalAlignment = Alignment.CenterHorizontally ) { augmentedBill?.let { bill -> - RenderedBill( + ScannableRenderer( modifier = Modifier .padding(top = CodeTheme.dimens.grid.x3) .fillMaxWidth() @@ -96,7 +95,7 @@ internal fun BillCustomizationContent( .sharedBoundsTransition( transition = SharedTransition.CurrencyBill ), - bill = bill, + scannable = bill, ) } diff --git a/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/screens/ReviewScreen.kt b/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/screens/ReviewScreen.kt index 88e9c1f5aa..b931601f8e 100644 --- a/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/screens/ReviewScreen.kt +++ b/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/screens/ReviewScreen.kt @@ -16,7 +16,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.flipcash.app.bills.RenderedBill +import com.flipcash.app.bills.ScannableRenderer import com.flipcash.app.core.tokens.CurrencyCreatorResult import com.flipcash.app.core.tokens.CurrencyCreatorStep import com.flipcash.app.core.ui.TokenIconWithName @@ -32,7 +32,6 @@ import com.getcode.ui.theme.CodeButton import com.getcode.ui.theme.CodeScaffold import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @Composable @@ -120,14 +119,14 @@ internal fun ReviewContent( } state.bill?.let { bill -> - RenderedBill( + ScannableRenderer( modifier = Modifier .weight(1f) .fillMaxWidth() .sharedBoundsTransition( transition = SharedTransition.CurrencyBill ), - bill = bill, + scannable = bill, ) } } diff --git a/apps/flipcash/features/scanner/src/main/kotlin/com/flipcash/app/scanner/internal/Scanner.kt b/apps/flipcash/features/scanner/src/main/kotlin/com/flipcash/app/scanner/internal/Scanner.kt index 8812d15abf..dc1bac0cb7 100644 --- a/apps/flipcash/features/scanner/src/main/kotlin/com/flipcash/app/scanner/internal/Scanner.kt +++ b/apps/flipcash/features/scanner/src/main/kotlin/com/flipcash/app/scanner/internal/Scanner.kt @@ -18,7 +18,7 @@ import com.flipcash.app.core.extensions.navigateAll import com.flipcash.app.core.extensions.openAsSheet import com.flipcash.app.core.navigation.DeeplinkType import com.flipcash.app.router.LocalRouter -import com.flipcash.app.scanner.internal.bills.BillContainer +import com.flipcash.app.scanner.internal.bills.ScannableContainer import com.flipcash.app.session.LocalSessionController import com.getcode.libs.code.detection.CodeScanResult import com.getcode.navigation.core.LocalCodeNavigator @@ -74,7 +74,7 @@ internal fun Scanner() { } @SuppressLint("LocalContextGetResourceValueCall") - BillContainer( + ScannableContainer( isPaused = isPaused, isPinching = isPinching, zoomRatio = zoomRatio, @@ -90,7 +90,7 @@ internal fun Scanner() { navigator.openAsSheet(route) } - return@BillContainer + return@ScannableContainer } } else -> Unit diff --git a/apps/flipcash/features/scanner/src/main/kotlin/com/flipcash/app/scanner/internal/bills/BillContainerView.kt b/apps/flipcash/features/scanner/src/main/kotlin/com/flipcash/app/scanner/internal/bills/ScannableContainer.kt similarity index 70% rename from apps/flipcash/features/scanner/src/main/kotlin/com/flipcash/app/scanner/internal/bills/BillContainerView.kt rename to apps/flipcash/features/scanner/src/main/kotlin/com/flipcash/app/scanner/internal/bills/ScannableContainer.kt index f4fe75ffe0..66c8818025 100644 --- a/apps/flipcash/features/scanner/src/main/kotlin/com/flipcash/app/scanner/internal/bills/BillContainerView.kt +++ b/apps/flipcash/features/scanner/src/main/kotlin/com/flipcash/app/scanner/internal/bills/ScannableContainer.kt @@ -34,9 +34,9 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.flipcash.app.bills.AnimatedBill +import com.flipcash.app.bills.AnimatedScannable import com.flipcash.app.core.android.extensions.launchAppSettings +import com.flipcash.app.core.bill.Scannable import com.flipcash.app.scanner.internal.ScannerDecorItem import com.flipcash.app.scanner.internal.ui.components.DecorView import com.flipcash.app.scanner.internal.ui.modals.ReceivedFundsConfirmation @@ -59,10 +59,11 @@ import com.getcode.ui.utils.AnimationUtils import com.getcode.util.permissions.PermissionResult import com.getcode.util.permissions.rememberCameraPermission import kotlinx.coroutines.delay +import kotlin.time.Duration.Companion.milliseconds @OptIn(ExperimentalMaterialApi::class) @Composable -internal fun BillContainer( +internal fun ScannableContainer( modifier: Modifier = Modifier, isPaused: Boolean, isPinching: Boolean = false, @@ -183,7 +184,7 @@ internal fun BillContainer( LaunchedEffect(dismissed) { if (dismissed) { - delay(500) + delay(500.milliseconds) dismissed = false } } @@ -216,7 +217,7 @@ internal fun BillContainer( } } - AnimatedBill( + AnimatedScannable( modifier = Modifier.fillMaxSize(), dismissState = billDismissState, dismissed = dismissed, @@ -240,58 +241,84 @@ internal fun BillContainer( } ) - //Bill management options - AnimatedVisibility( - modifier = Modifier - .align(BottomCenter) - .measured { managementHeight = it.height }, - visible = showManagementOptions, - enter = fadeIn(), - exit = fadeOut(tween(100)), - ) { - var canCancel by remember { - mutableStateOf(false) - } - BillManagementOptions( - modifier = Modifier - .windowInsetsPadding(WindowInsets.navigationBars), - primaryAction = updatedBillState.primaryAction, - secondaryAction = updatedBillState.secondaryAction, - isSending = updatedState.isRemoteSendLoading, - isInteractable = canCancel, - ) - - LaunchedEffect(transition.isRunning, transition.targetState) { - // wait for spring settle to enable cancel to not prematurely cancel - // the enter. doing so causing the exit of the bill to not run, or run its own dismiss animation - if (transition.targetState == EnterExitState.Visible && transition.currentState == transition.targetState) { - delay(500) - canCancel = true - } - } - - BackHandler(canCancel) { - session.dismissBill(PutInWallet) - } + // Below-bill content, folded by scannable type. `displayedScannable` retains the + // last shown scannable so an arm's modal can still animate OUT as `bill` returns to + // null on dismiss (the arm stays mounted; only `visible` flips). + var displayedScannable by remember { mutableStateOf(null) } + LaunchedEffect(updatedBillState.bill) { + updatedBillState.bill?.let { displayedScannable = it } } - //Bill Received Bottom Dialog - AnimatedVisibility( - modifier = Modifier.align(BottomCenter), - visible = updatedBillState.bill?.didReceive ?: false, - enter = AnimationUtils.modalEnter(billState.confirmationDelayMillis), - exit = AnimationUtils.modalExit, - ) { - if (updatedBillState.bill != null) { - Box( - contentAlignment = BottomCenter + when (val shown = displayedScannable) { + is Scannable.Payable -> { + //Bill management options + AnimatedVisibility( + modifier = Modifier + .align(BottomCenter) + .measured { managementHeight = it.height }, + visible = updatedBillState.bill is Scannable.Payable && showManagementOptions, + enter = fadeIn(), + exit = fadeOut(tween(100)), ) { - ReceivedFundsConfirmation( - bill = updatedBillState.bill!!, - onClaim = { session.dismissBill(PutInWallet) } + var canCancel by remember { + mutableStateOf(false) + } + BillManagementOptions( + modifier = Modifier + .windowInsetsPadding(WindowInsets.navigationBars), + primaryAction = updatedBillState.primaryAction, + secondaryAction = updatedBillState.secondaryAction, + isSending = updatedState.isRemoteSendLoading, + isInteractable = canCancel, ) + + LaunchedEffect(transition.isRunning, transition.targetState) { + // wait for spring settle to enable cancel to not prematurely cancel + // the enter. doing so causing the exit of the bill to not run, or run its own dismiss animation + if (transition.targetState == EnterExitState.Visible && transition.currentState == transition.targetState) { + delay(500) + canCancel = true + } + } + + BackHandler(canCancel) { + session.dismissBill(PutInWallet) + } + } + + //Bill Received Bottom Dialog + AnimatedVisibility( + modifier = Modifier.align(BottomCenter), + visible = (updatedBillState.bill as? Scannable.Payable)?.didReceive == true, + enter = AnimationUtils.modalEnter(billState.confirmationDelayMillis), + exit = AnimationUtils.modalExit, + ) { + Box( + contentAlignment = BottomCenter + ) { + ReceivedFundsConfirmation( + bill = shown, + onClaim = { session.dismissBill(PutInWallet) } + ) + } } } + + is Scannable.TipCard -> { + // TODO(owner): the tip card's bottom modal (analogous to ReceivedFundsConfirmation). + // Gate `visible` on the live bill so it animates out on dismiss, and use `shown` + // (the retained Scannable.TipCard) for content so it persists through the exit: + // AnimatedVisibility( + // modifier = Modifier.align(BottomCenter), + // visible = updatedBillState.bill is Scannable.TipCard, + // enter = AnimationUtils.modalEnter(0), + // exit = AnimationUtils.modalExit, + // ) { + // TipCardModal(tipCard = shown, onDone = { session.dismissBill(PutInWallet) }) + // } + } + + null -> Unit } } } diff --git a/apps/flipcash/shared/bills/build.gradle.kts b/apps/flipcash/shared/bills/build.gradle.kts index 59db6ea754..82828d6657 100644 --- a/apps/flipcash/shared/bills/build.gradle.kts +++ b/apps/flipcash/shared/bills/build.gradle.kts @@ -11,6 +11,7 @@ dependencies { implementation(libs.firebase.messaging) implementation(project(":libs:messaging")) + implementation(project(":apps:flipcash:shared:common-ui")) implementation(libs.androidx.datastore) } diff --git a/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/AnimatedBill.kt b/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/AnimatedScannable.kt similarity index 85% rename from apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/AnimatedBill.kt rename to apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/AnimatedScannable.kt index aee3aaa183..58ddcbd364 100644 --- a/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/AnimatedBill.kt +++ b/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/AnimatedScannable.kt @@ -20,7 +20,7 @@ import com.getcode.ui.theme.CustomSwipeToDismiss @OptIn(ExperimentalMaterialApi::class) @Composable -fun AnimatedBill( +fun AnimatedScannable( modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues( horizontal = CodeTheme.dimens.inset, @@ -28,9 +28,9 @@ fun AnimatedBill( ), dismissState: DismissState, dismissed: Boolean, - transitionSpec: AnimatedContentTransitionScope.() -> ContentTransform, - contentKey: (Scannable.Payable?) -> Any? = { it }, - bill: Scannable.Payable?, + transitionSpec: AnimatedContentTransitionScope.() -> ContentTransform, + contentKey: (Scannable?) -> Any? = { it }, + bill: Scannable?, ) { AnimatedContent( modifier = modifier, @@ -47,16 +47,16 @@ fun AnimatedBill( state = dismissState, dismissContent = { if (b != null && !dismissed) { - RenderedBill( + ScannableRenderer( modifier = Modifier .fillMaxWidth() .weight(1f) .padding(contentPadding), - bill = b + scannable = b ) } }, - directions = if (b?.disableGestures ?: false) { + directions = if ((b as? Scannable.Payable)?.disableGestures == true) { emptySet() } else { setOf(DismissDirection.EndToStart, DismissDirection.StartToEnd) diff --git a/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/RenderedBill.kt b/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/RenderedBill.kt deleted file mode 100644 index b7378da579..0000000000 --- a/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/RenderedBill.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.flipcash.app.bills - -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import com.flipcash.app.core.bill.Scannable -import com.getcode.opencode.model.financial.CurrencyCode -import com.getcode.opencode.model.financial.Fiat -import com.getcode.opencode.model.financial.LocalFiat -import com.getcode.opencode.model.financial.Rate -import com.getcode.opencode.model.financial.Token -import com.getcode.theme.CodeTheme -import com.getcode.theme.DesignSystem - -@Composable -fun RenderedBill( - modifier: Modifier = Modifier, - bill: Scannable.Payable, -) { - when (bill) { - is Scannable.CashBill -> CashBill( - modifier = modifier, - payloadData = bill.data, - amount = bill.amount, - token = bill.token - ) - is Scannable.GoldBar -> GoldBar( - modifier = modifier.padding(horizontal = CodeTheme.dimens.inset), - payloadData = bill.data, - amount = bill.amount.underlyingTokenAmount, - ) - } -} - -private val PREVIEW_CODE_DATA = listOf( - 0xA5, 0x3C, 0xD7, 0x8B, 0x14, 0xE9, 0x62, 0xF0, - 0x4D, 0xB6, 0x29, 0x7A, 0xC3, 0x58, 0x91, 0xDE, - 0x6F, 0x03, 0xB4, 0x87, 0x2C, 0xE5, 0x50, 0xA9, - 0x1E, 0x73, 0xC6, 0x3F, 0x98, 0x41, 0xDA, 0x65, - 0x0B, 0xF2, 0x7D, 0xAE, 0x53, 0xC0, 0x19, -).map { it.toByte() } - -@Preview -@Composable -fun Preview_CashBill() { - DesignSystem { - // $3 USD - val usdcBase = Fiat(3.00, CurrencyCode.USD) - val cadRate = Rate(1.4, CurrencyCode.CAD) - CashBill( - amount = LocalFiat( - usdf = usdcBase, - nativeAmount = usdcBase.convertingTo(cadRate), - ), - mint = "5AMAA9JV9H97YYVxx8F6FsCMmTwXSuTTQneiup4RYAUQ", - payloadData = PREVIEW_CODE_DATA, - ) - } -} \ No newline at end of file diff --git a/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/ScannableRenderer.kt b/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/ScannableRenderer.kt new file mode 100644 index 0000000000..0c002a1eae --- /dev/null +++ b/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/ScannableRenderer.kt @@ -0,0 +1,80 @@ +package com.flipcash.app.bills + +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewWrapper +import com.flipcash.app.bills.components.bills.CashBill +import com.flipcash.app.bills.components.bills.GoldBar +import com.flipcash.app.bills.components.cards.TipCard +import com.flipcash.app.core.bill.Scannable +import com.flipcash.app.theme.FlipcashThemeWrapper +import com.flipcash.services.models.UserProfile +import com.getcode.opencode.model.financial.CurrencyCode +import com.getcode.opencode.model.financial.Fiat +import com.getcode.opencode.model.financial.LocalFiat +import com.getcode.opencode.model.financial.Rate +import com.getcode.theme.CodeTheme + +@Composable +fun ScannableRenderer( + modifier: Modifier = Modifier, + scannable: Scannable, +) { + when (scannable) { + is Scannable.CashBill -> CashBill( + modifier = modifier, + payloadData = scannable.data, + amount = scannable.amount, + token = scannable.token + ) + is Scannable.GoldBar -> GoldBar( + modifier = modifier.padding(horizontal = CodeTheme.dimens.inset), + payloadData = scannable.data, + amount = scannable.amount.underlyingTokenAmount, + ) + is Scannable.TipCard -> TipCard( + modifier = modifier, + payloadData = scannable.data, + user = scannable.user + ) + } +} + +private val PREVIEW_CODE_DATA = listOf( + 0xA5, 0x3C, 0xD7, 0x8B, 0x14, 0xE9, 0x62, 0xF0, + 0x4D, 0xB6, 0x29, 0x7A, 0xC3, 0x58, 0x91, 0xDE, + 0x6F, 0x03, 0xB4, 0x87, 0x2C, 0xE5, 0x50, 0xA9, + 0x1E, 0x73, 0xC6, 0x3F, 0x98, 0x41, 0xDA, 0x65, + 0x0B, 0xF2, 0x7D, 0xAE, 0x53, 0xC0, 0x19, +).map { it.toByte() } + +@Preview +@PreviewWrapper(FlipcashThemeWrapper::class) +@Composable +fun Preview_CashBill() { + // $3 USD + val usdcBase = Fiat(3.00, CurrencyCode.USD) + val cadRate = Rate(1.4, CurrencyCode.CAD) + CashBill( + amount = LocalFiat( + usdf = usdcBase, + nativeAmount = usdcBase.convertingTo(cadRate), + ), + mint = "5AMAA9JV9H97YYVxx8F6FsCMmTwXSuTTQneiup4RYAUQ", + payloadData = PREVIEW_CODE_DATA, + ) +} + +@Preview +@PreviewWrapper(FlipcashThemeWrapper::class) +@Composable +fun Preview_TipCard() { + TipCard( + payloadData = PREVIEW_CODE_DATA, + user = UserProfile.Empty.copy( + displayName = "Flipcash User", + ) + ) +} \ No newline at end of file diff --git a/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/ScannableCode.kt b/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/components/ScannableCode.kt similarity index 98% rename from apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/ScannableCode.kt rename to apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/components/ScannableCode.kt index 2e115b20e3..b17554e132 100644 --- a/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/ScannableCode.kt +++ b/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/components/ScannableCode.kt @@ -1,4 +1,4 @@ -package com.flipcash.app.bills +package com.flipcash.app.bills.components import android.graphics.Bitmap import android.graphics.BitmapFactory diff --git a/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/BillAmount.kt b/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/components/bills/BillAmount.kt similarity index 92% rename from apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/BillAmount.kt rename to apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/components/bills/BillAmount.kt index d9c0e92b1e..11645e3080 100644 --- a/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/BillAmount.kt +++ b/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/components/bills/BillAmount.kt @@ -1,4 +1,4 @@ -package com.flipcash.app.bills +package com.flipcash.app.bills.components.bills import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row @@ -9,9 +9,6 @@ import androidx.compose.ui.draw.rotate import androidx.compose.ui.layout.layout import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.min -import androidx.compose.ui.unit.sp import com.flipcash.shared.bills.R import com.getcode.theme.CodeTheme import com.getcode.ui.utils.nonScaledSp diff --git a/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/CashBill.kt b/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/components/bills/CashBill.kt similarity index 97% rename from apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/CashBill.kt rename to apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/components/bills/CashBill.kt index 5f970098ad..45e562a608 100644 --- a/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/CashBill.kt +++ b/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/components/bills/CashBill.kt @@ -1,6 +1,5 @@ -package com.flipcash.app.bills +package com.flipcash.app.bills.components.bills -import android.R.attr.fillType import android.annotation.SuppressLint import android.graphics.Bitmap import android.graphics.BitmapFactory @@ -35,8 +34,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clipToBounds -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.draw.rotate import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect @@ -56,7 +53,6 @@ import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.graphics.drawscope.DrawScope -import androidx.compose.ui.graphics.drawscope.clipPath import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.lerp import androidx.compose.ui.layout.ContentScale @@ -77,28 +73,22 @@ import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.isSpecified -import androidx.compose.ui.zIndex +import com.flipcash.app.bills.components.ScannableCode import com.flipcash.app.core.money.formatted import com.flipcash.shared.bills.R import com.getcode.opencode.compose.LocalExchange import com.getcode.opencode.model.financial.LocalFiat import com.getcode.opencode.model.financial.Token import com.getcode.opencode.model.ui.BillBackground -import com.getcode.opencode.model.ui.BillTexture import com.getcode.opencode.model.ui.TokenBillCustomizations -import com.getcode.solana.keys.Mint import com.getcode.opencode.model.ui.BlendMode as PlaygroundBlendMode import com.getcode.solana.keys.base58 import com.getcode.theme.CodeTheme -import com.getcode.ui.core.blendMode -import com.getcode.ui.core.drawWithGradient import com.getcode.ui.core.patternBlend import com.getcode.ui.core.punchCircle import com.getcode.ui.core.punchRectangle import com.getcode.ui.utils.Geometry -import com.getcode.ui.utils.deriveTargetColor import com.getcode.ui.utils.hexToColor -import com.getcode.ui.utils.hls import com.getcode.ui.utils.nonScaledSp import kotlin.math.ceil import kotlin.math.roundToInt @@ -665,7 +655,7 @@ private class BillPunchShape( fun punchColorsFrom(billColors: List): List { return billColors.map { color -> - lerp(color, Color.Black, 0.30f) + lerp(color, Black, 0.30f) } } diff --git a/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/GoldBar.kt b/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/components/bills/GoldBar.kt similarity index 97% rename from apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/GoldBar.kt rename to apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/components/bills/GoldBar.kt index 245e0e450e..b5b7a220cd 100644 --- a/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/GoldBar.kt +++ b/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/components/bills/GoldBar.kt @@ -1,6 +1,12 @@ -package com.flipcash.app.bills +package com.flipcash.app.bills.components.bills import android.content.Context +import android.graphics.BitmapShader +import android.graphics.Matrix +import android.graphics.PorterDuff +import android.graphics.PorterDuffXfermode +import android.graphics.RuntimeShader +import android.graphics.Shader import android.hardware.Sensor import android.hardware.SensorEvent import android.hardware.SensorEventListener @@ -78,18 +84,20 @@ import androidx.compose.ui.unit.dp import androidx.core.graphics.createBitmap import androidx.core.graphics.set import androidx.compose.ui.res.stringResource +import com.flipcash.app.bills.components.ScannableCode import com.flipcash.shared.bills.R import com.getcode.opencode.model.financial.Fiat import com.getcode.solana.keys.Mint import com.getcode.solana.keys.base58 import com.getcode.theme.DesignSystem import com.getcode.ui.utils.nonScaledSp +import kotlin.math.abs import kotlin.random.Random import android.graphics.Paint as NativePaint @OptIn(ExperimentalLayoutApi::class) @Composable -fun GoldBar( +internal fun GoldBar( modifier: Modifier = Modifier, payloadData: List = emptyList(), amount: Fiat, @@ -326,20 +334,20 @@ private fun Modifier.brushedMetal( shape.createOutline(size, layoutDirection, Density(density)) ) } - val streakShader = android.graphics.BitmapShader( + val streakShader = BitmapShader( streakBitmap, - android.graphics.Shader.TileMode.REPEAT, - android.graphics.Shader.TileMode.REPEAT, + Shader.TileMode.REPEAT, + Shader.TileMode.REPEAT, ).apply { - val matrix = android.graphics.Matrix() + val matrix = Matrix() matrix.setRotate(highlightRotation) setLocalMatrix(matrix) } val streakPaint = NativePaint().apply { shader = streakShader alpha = grainAlpha - xfermode = android.graphics.PorterDuffXfermode( - android.graphics.PorterDuff.Mode.OVERLAY + xfermode = PorterDuffXfermode( + PorterDuff.Mode.OVERLAY ) } onDrawBehind { @@ -614,7 +622,7 @@ private fun Modifier.recessWithCodeCutout( val grooveCount = 50 val strokePx = 1f // exactly 1 physical pixel — crisp, no AA blur val groovePaint = NativePaint().apply { - style = android.graphics.Paint.Style.STROKE + style = NativePaint.Style.STROKE strokeWidth = strokePx isAntiAlias = false // pixel-sharp, no soft halos } @@ -723,8 +731,8 @@ private fun Modifier.recessWithCodeCutout( canvas.nativeCanvas.drawRect( 0f, 0f, size.width, size.height, NativePaint().apply { - xfermode = android.graphics.PorterDuffXfermode( - android.graphics.PorterDuff.Mode.SRC_ATOP + xfermode = PorterDuffXfermode( + PorterDuff.Mode.SRC_ATOP ) color = argb }, @@ -755,8 +763,8 @@ private fun Modifier.recessWithCodeCutout( val sc = canvas.nativeCanvas.saveLayer( null, NativePaint().apply { - xfermode = android.graphics.PorterDuffXfermode( - android.graphics.PorterDuff.Mode.DST_OUT + xfermode = PorterDuffXfermode( + PorterDuff.Mode.DST_OUT ) }, ) @@ -874,7 +882,7 @@ private fun Modifier.agslBrushedMetal( lightSource: Offset = DefaultLightSource, rotation: Float = 0f, ): Modifier = this.drawWithCache { - val shader = android.graphics.RuntimeShader(AGSL_BRUSHED_METAL) + val shader = RuntimeShader(AGSL_BRUSHED_METAL) shader.setFloatUniform("iResolution", size.width, size.height) val sl = safeLight(lightSource) shader.setFloatUniform("iTilt", sl.x, sl.y) @@ -1107,9 +1115,9 @@ private fun rememberTiltState(): State { // Dead zone — only update state if change is perceptible val prev = state.value - val dx = kotlin.math.abs(smoothX - prev.x) - val dy = kotlin.math.abs(smoothY - prev.y) - val dr = kotlin.math.abs(smoothRot - prev.rotation) + val dx = abs(smoothX - prev.x) + val dy = abs(smoothY - prev.y) + val dr = abs(smoothRot - prev.rotation) if (dx > deadZone || dy > deadZone || dr > 0.5f) { state.value = TiltState(smoothX, smoothY, smoothRot) } diff --git a/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/components/cards/TipCard.kt b/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/components/cards/TipCard.kt new file mode 100644 index 0000000000..d21b1baa37 --- /dev/null +++ b/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/components/cards/TipCard.kt @@ -0,0 +1,86 @@ +package com.flipcash.app.bills.components.cards + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsIgnoringVisibility +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import com.flipcash.app.bills.components.ScannableCode +import com.flipcash.services.models.UserProfile +import com.flipcash.shared.common.ui.ContactAvatar +import com.getcode.theme.CodeTheme +import com.getcode.theme.xxl + +@OptIn(ExperimentalLayoutApi::class) +@Composable +internal fun TipCard( + payloadData: List, + user: UserProfile, + modifier: Modifier = Modifier, +) { + BoxWithConstraints( + modifier = modifier + .windowInsetsPadding(WindowInsets.statusBarsIgnoringVisibility) + .padding(horizontal = CodeTheme.dimens.inset), + contentAlignment = Alignment.Center + ) { + val mW = this.maxWidth + val codeSize = remember { mW * 0.65f } + + Column( + modifier = Modifier.background(CodeTheme.colors.tipCardColor, shape = CodeTheme.shapes.xxl) + .padding(vertical = CodeTheme.dimens.grid.x8, horizontal = CodeTheme.dimens.grid.x7) + .heightIn(0.dp, 800.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x4) + ) { + if (payloadData.isNotEmpty()) { + ScannableCode( + modifier = Modifier.size(codeSize), + data = payloadData, + icon = null, + ) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = "Tip", + style = CodeTheme.typography.textMedium, + color = CodeTheme.colors.textMain, + ) + + ContactAvatar( + modifier = Modifier + .padding(start = CodeTheme.dimens.grid.x2, end = CodeTheme.dimens.grid.x1) + .size(CodeTheme.dimens.staticGrid.x5) + .clip(CircleShape), + userProfile = user + ) + + Text( + text = user.displayName.orEmpty(), + style = CodeTheme.typography.textMedium, + color = CodeTheme.colors.textMain, + ) + } + } + } +} \ No newline at end of file diff --git a/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/PaymentReceiptBill.kt b/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/components/receipts/PaymentReceipt.kt similarity index 97% rename from apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/PaymentReceiptBill.kt rename to apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/components/receipts/PaymentReceipt.kt index 1038e6881e..7f8999114e 100644 --- a/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/PaymentReceiptBill.kt +++ b/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/components/receipts/PaymentReceipt.kt @@ -1,4 +1,4 @@ -package com.flipcash.app.bills +package com.flipcash.app.bills.components.receipts import androidx.compose.foundation.Canvas import androidx.compose.foundation.background @@ -26,6 +26,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.flipcash.app.bills.components.ScannableCode import com.getcode.opencode.compose.LocalExchange import com.getcode.opencode.model.financial.CurrencyCode import com.getcode.opencode.model.financial.LocalFiat diff --git a/apps/flipcash/shared/featureflags/src/main/kotlin/com/flipcash/app/featureflags/FeatureFlag.kt b/apps/flipcash/shared/featureflags/src/main/kotlin/com/flipcash/app/featureflags/FeatureFlag.kt index 383ad11bd9..0199e18e58 100644 --- a/apps/flipcash/shared/featureflags/src/main/kotlin/com/flipcash/app/featureflags/FeatureFlag.kt +++ b/apps/flipcash/shared/featureflags/src/main/kotlin/com/flipcash/app/featureflags/FeatureFlag.kt @@ -257,6 +257,15 @@ sealed interface FeatureFlag { override val persistLogOut: Boolean = false } + @FeatureFlagMarker + data object Tipping: FeatureFlag { + override val key: String = "tipping_enabled" + override val default: Boolean = false + override val launched: Boolean = false + override val visible: Boolean = true + override val persistLogOut: Boolean = false + } + companion object { val entries: List> get() = FeatureFlagEntries.entries @@ -294,6 +303,7 @@ val FeatureFlag<*>.title: String FeatureFlag.GiveUsdf -> "Give/Send USDF" FeatureFlag.AddMoneyUX -> "Add Money UX" FeatureFlag.ShowNetworkState -> "Network Offline Indicator" + FeatureFlag.Tipping -> "Tipping" } val FeatureFlag<*>.message: String @@ -322,6 +332,7 @@ val FeatureFlag<*>.message: String FeatureFlag.GiveUsdf -> "When enabled, you'll gain the ability to send USDF directly and give it as cash" FeatureFlag.AddMoneyUX -> "When enabled, the user experience for getting money into the app will be focused around 'Adding Money'" FeatureFlag.ShowNetworkState -> "When enabled, you'll gain the ability to see the network state on the Scanner when offline" + FeatureFlag.Tipping -> "When enabled, you'll gain the ability to tip other users and set up your own tip card to receive tips" } diff --git a/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/SessionController.kt b/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/SessionController.kt index e0c059fdd6..04ab50fa0f 100644 --- a/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/SessionController.kt +++ b/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/SessionController.kt @@ -21,6 +21,13 @@ data object PutInWallet : BillDeterminationResult, ActedUpon interface BillOperations { val billState: StateFlow fun showBill(bill: Scannable.Payable) + + /** + * Presents a non-payment [Scannable.TipCard] in the bill container. Unlike [showBill], + * this runs no grab/await transaction and sets no valuation — the card is simply shown + * until dismissed. + */ + fun showTipCard(tipCard: Scannable.TipCard) fun dismissBill(action: BillDeterminationResult) } @@ -60,8 +67,10 @@ data class SessionState( val restrictionType: RestrictionType? = null, val isRemoteSendLoading: Boolean = false, val contactDmUnreadCount: Int = 0, + val tipsUnreadCount: Int = 0, val tokens: List = emptyList(), val isPhoneNumberSendEnabled: Boolean = false, + val isTippingEnabled: Boolean = false, val addMoneyUx: Boolean = false, ) diff --git a/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/RealSessionController.kt b/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/RealSessionController.kt index 474758471a..d8fbdaa6c5 100644 --- a/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/RealSessionController.kt +++ b/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/RealSessionController.kt @@ -9,6 +9,7 @@ import com.flipcash.app.contacts.ContactCoordinator import com.flipcash.app.blob.BlobStorageCoordinator import com.flipcash.services.models.chat.ChatType import com.flipcash.shared.chat.ChatCoordinator +import com.flipcash.app.core.bill.Scannable import com.flipcash.app.core.internal.bill.BillController import com.flipcash.app.core.internal.updater.ProfileUpdater import com.flipcash.app.featureflags.FeatureFlag @@ -202,6 +203,15 @@ class RealSessionController @Inject constructor( .onEach { count -> stateHolder.update { it.copy(contactDmUnreadCount = count) } } .launchIn(scope) + userManager.state + .map { it.authState } + .filter { it.isAtLeastRegistered } + .distinctUntilChanged() + .flatMapLatest { chatCoordinator.observeUnreadConversations(ChatType.TIP_DM) } + .distinctUntilChanged() + .onEach { count -> stateHolder.update { it.copy(tipsUnreadCount = count) } } + .launchIn(scope) + // Preload the blob upload policy once registered so profile-photo selection can filter and // validate against it without a network round-trip. Cached in the BlobStorageCoordinator. userManager.state @@ -252,6 +262,10 @@ class RealSessionController @Inject constructor( .onEach { enabled -> stateHolder.update { it.copy(isPhoneNumberSendEnabled = enabled) } } .launchIn(scope) + featureFlagController.observe(FeatureFlag.Tipping) + .onEach { enabled -> stateHolder.update { it.copy(isTippingEnabled = enabled) } } + .launchIn(scope) + // Retry updateUserFlags when network is restored networkObserver.state .map { it.connected } @@ -292,7 +306,8 @@ class RealSessionController @Inject constructor( toastController.clear() val bill = billController.state.value.bill - if (!shareSheetController.isCheckingForShare || (bill != null && !bill.didReceive)) { + if (!shareSheetController.isCheckingForShare || + (bill != null && (bill as? Scannable.Payable)?.didReceive != true)) { BottomBarManager.clear() billController.cancelAwaitForGrab() dismissBill(PutInWallet) diff --git a/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/delegates/BillPresentationDelegate.kt b/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/delegates/BillPresentationDelegate.kt index 5f4d08d534..c6bfd7686e 100644 --- a/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/delegates/BillPresentationDelegate.kt +++ b/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/delegates/BillPresentationDelegate.kt @@ -112,6 +112,14 @@ class BillPresentationDelegate @Inject constructor( } } + override fun showTipCard(tipCard: Scannable.TipCard) { + // Single bill slot — don't clobber a bill that's already presented. + if (billController.state.value.bill != null) return + // No grab/await, no valuation: just place the card in the container. + billController.update { it.copy(bill = tipCard, valuation = null) } + stateHolder.update { it.copy(billResult = PutInWallet) } + } + override fun dismissBill(action: BillDeterminationResult) { scope.launch { stateHolder.update { it.copy(billResult = action) } diff --git a/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/delegates/GiftCardSharingDelegate.kt b/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/delegates/GiftCardSharingDelegate.kt index c0a4becff9..d8090faf1b 100644 --- a/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/delegates/GiftCardSharingDelegate.kt +++ b/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/delegates/GiftCardSharingDelegate.kt @@ -140,7 +140,7 @@ class GiftCardSharingDelegate @Inject constructor( message = "Cash link not sent. Restarting awaiting grab", type = TraceType.User, ) - val currentBill = billController.state.value.bill ?: bill + val currentBill = billController.state.value.bill as? Scannable.Payable ?: bill _events.trySend(Event.RestartBillGrab(currentBill, owner)) } } diff --git a/apps/flipcash/shared/theme/src/main/kotlin/com/flipcash/app/theme/internal/FlipcashDesignSystem.kt b/apps/flipcash/shared/theme/src/main/kotlin/com/flipcash/app/theme/internal/FlipcashDesignSystem.kt index e6fc52cc61..30da635d44 100644 --- a/apps/flipcash/shared/theme/src/main/kotlin/com/flipcash/app/theme/internal/FlipcashDesignSystem.kt +++ b/apps/flipcash/shared/theme/src/main/kotlin/com/flipcash/app/theme/internal/FlipcashDesignSystem.kt @@ -118,6 +118,7 @@ private val colors = with(Flipcash2ColorSpec) { toggleUncheckedTrackColor = Color(0xFF666666), cashBill = cashBill, cashBillDecorColor = Color.White.copy(0.60f), + tipCard = Color.Black.copy(alpha = 0.36f), betaIndicator = BetaIndicator, bannerThemed = bannerThemed, bannerError = Error, diff --git a/gradle.properties b/gradle.properties index 0d37ec7fd0..42df4d3ba9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -45,3 +45,6 @@ android.dependency.useConstraints=false # enum unboxing, constant propagation). Produces smaller, faster output # and can also reduce R8's own processing time. android.enableR8.fullMode=true + +# Enabled parallel sync for Gradle 9.4+ +org.gradle.tooling.parallel=true diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/ResolverService.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/ResolverService.kt index f215521c1d..6b108116e8 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/ResolverService.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/ResolverService.kt @@ -21,24 +21,22 @@ internal class ResolverService @Inject constructor( return runCatching { api.resolve(owner, identifier) }.foldWithSuppression( - onSuccess = { it.toResult() }, + onSuccess = { response -> + when (response.result) { + RpcResolverService.ResolveResponse.Result.OK -> + Result.success(response.resolution.address.toPublicKey()) + RpcResolverService.ResolveResponse.Result.NOT_FOUND -> + Result.failure(ResolveContactError.NotFound()) + RpcResolverService.ResolveResponse.Result.DENIED -> + Result.failure(ResolveContactError.Denied()) + RpcResolverService.ResolveResponse.Result.UNRECOGNIZED -> + Result.failure(ResolveContactError.Unrecognized()) + else -> Result.failure(ResolveContactError.Other()) + } + }, onFailure = { cause -> Result.failure(cause.toValidationOrElse { ResolveContactError.Other(cause = it) }) } ) } - - private fun RpcResolverService.ResolveResponse.toResult(): Result { - return when (result) { - RpcResolverService.ResolveResponse.Result.OK -> - Result.success(resolution.address.toPublicKey()) - RpcResolverService.ResolveResponse.Result.NOT_FOUND -> - Result.failure(ResolveContactError.NotFound()) - RpcResolverService.ResolveResponse.Result.DENIED -> - Result.failure(ResolveContactError.Denied()) - RpcResolverService.ResolveResponse.Result.UNRECOGNIZED -> - Result.failure(ResolveContactError.Unrecognized()) - else -> Result.failure(ResolveContactError.Other()) - } - } } diff --git a/ui/theme/src/main/kotlin/com/getcode/theme/Shape.kt b/ui/theme/src/main/kotlin/com/getcode/theme/Shape.kt index 1dfd9cbc99..0a4168cb04 100644 --- a/ui/theme/src/main/kotlin/com/getcode/theme/Shape.kt +++ b/ui/theme/src/main/kotlin/com/getcode/theme/Shape.kt @@ -28,7 +28,7 @@ val Shapes.extraLarge: CornerBasedShape @Composable get() = RoundedCornerShape(20.dp) val Shapes.xxl: CornerBasedShape - @Composable get() = RoundedCornerShape(25.dp) + @Composable get() = RoundedCornerShape(30.dp) @Composable fun Shapes.receipt(step: Dp = CodeTheme.dimens.grid.x2) = TriangleCutShape(step) diff --git a/ui/theme/src/main/kotlin/com/getcode/theme/Theme.kt b/ui/theme/src/main/kotlin/com/getcode/theme/Theme.kt index f542514804..3decfbd3eb 100644 --- a/ui/theme/src/main/kotlin/com/getcode/theme/Theme.kt +++ b/ui/theme/src/main/kotlin/com/getcode/theme/Theme.kt @@ -52,6 +52,7 @@ internal val CodeDefaultColorScheme = ColorScheme( toggleUncheckedTrackColor = BrandToggleUncheckedTrackColor, cashBill = CashBill, cashBillDecorColor = CashBillDecor, + tipCard = Brand, betaIndicator = BetaIndicator, bannerThemed = Brand, bannerError = Error, @@ -153,6 +154,7 @@ class ColorScheme( toggleUncheckedTrackColor: Color, cashBill: Color, cashBillDecorColor: Color, + tipCard: Color, betaIndicator: Color, bannerThemed: Color, bannerError: Color, @@ -229,6 +231,8 @@ class ColorScheme( private set var cashBillDecorColor by mutableStateOf(cashBillDecorColor) private set + var tipCardColor by mutableStateOf(tipCard) + private set var betaIndicator by mutableStateOf(betaIndicator) private set @@ -285,6 +289,7 @@ class ColorScheme( toggleUncheckedTrackColor = other.toggleUncheckedTrackColor cashBillColor = other.cashBillColor cashBillDecorColor = other.cashBillDecorColor + tipCardColor = other.tipCardColor betaIndicator = other.betaIndicator bannerThemed = other.bannerThemed bannerError = other.bannerError @@ -330,6 +335,7 @@ class ColorScheme( toggleUncheckedTrackColor = toggleUncheckedTrackColor, cashBill = cashBillColor, cashBillDecorColor = cashBillDecorColor, + tipCard = tipCardColor, betaIndicator = betaIndicator, bannerThemed = bannerThemed, bannerError = bannerError,