Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,36 @@ class UsdtPaymentContentTest {
Locale.setDefault(originalLocale)
}

@Test
fun lockedReviewAmountKeepsReviewOpenWhileBackAndEditableAmountsStillNavigate() {
val editable = mutableStateOf(false)
var pageChanges = 0
var backPresses = 0
val payment = quote.copy(destination = UsdtDestination.ARBITRUM, bridgeProvider = null)
composeTestRule.setContent {
Content(
UsdtSendState(quote = payment), UsdtPage.AMOUNT,
amountEditable = editable.value,
onPageChange = { pageChanges++ },
onBack = { backPresses++ },
)
}
composeTestRule.onNodeWithText("3.00").performClick()
composeTestRule.onNodeWithTag("UsdtConfirm").assertIsDisplayed()
composeTestRule.runOnIdle {
assertEquals(0, pageChanges)
assertEquals(0, backPresses)
}
composeTestRule.onNodeWithTag("NavigationBack").performClick()
composeTestRule.runOnIdle {
assertEquals(1, backPresses)
editable.value = true
}
SystemClock.sleep(600)
composeTestRule.onNodeWithText("3.00").performClick()
composeTestRule.runOnIdle { assertEquals(1, pageChanges) }
}

@Test
fun decimalKeyUsesDotInCommaRegionsWithoutChangingInputEncoding() {
Locale.setDefault(Locale.GERMANY)
Expand Down Expand Up @@ -317,6 +347,9 @@ class UsdtPaymentContentTest {
state: UsdtSendState,
page: UsdtPage,
wallet: UsdtWalletState = UsdtWalletState(),
amountEditable: Boolean = true,
onPageChange: (UsdtPage) -> Unit = {},
onBack: () -> Unit = {},
onDetails: () -> Unit = {},
) {
AppThemeSurface {
Expand All @@ -332,12 +365,13 @@ class UsdtPaymentContentTest {
onRecipientChange = {},
onAmountChange = {},
onRecipientContinue = {},
onPageChange = {},
onBack = {},
onPageChange = onPageChange,
onBack = onBack,
onReview = {},
onConfirm = {},
onDone = {},
onDetails = onDetails,
amountEditable = amountEditable,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1372,23 +1372,16 @@ class PaykitPaymentRequestRepo @Inject constructor(
.flatten()
.filter {
it.lifecycleState != PaymentRequestLifecycleState.PROOF_SUBMITTED &&
!it.isExpired(now) &&
it.id !in locallyCompletedRequestIds &&
it.id !in locallyInFlightRequestIds &&
it.id !in updatedDismissedPaymentIds
}
val recurringHistory = allSubscriptions.filter { it.isPayer }.flatMap { subscription ->
updatedSubscriptionAcceptedAt[subscription.id]
val requests = updatedSubscriptionAcceptedAt[subscription.id]
?.let { subscription.requestsThrough(subscriptionNow, it) }
.orEmpty()
}.mapNotNull { request ->
when {
request.lifecycleState == PaymentRequestLifecycleState.PROOF_SUBMITTED -> request
request.id in locallyCompletedRequestIds -> request.copy(
lifecycleState = PaymentRequestLifecycleState.PROOF_SUBMITTED,
paymentProofKind = locallyCompletedProofKinds[request.id],
)
else -> null
}
subscriptionHistory(subscription, requests, now, locallyCompletedProofKinds)
}
val oneTimeIncoming = availableRecords.mapNotNull { record ->
when (val result = record.parseIncomingPaykitPaymentRequest(now)) {
Expand Down Expand Up @@ -1462,6 +1455,24 @@ class PaykitPaymentRequestRepo @Inject constructor(
return subscription.requestsThrough(now, acceptedAt)
}

private fun subscriptionHistory(
subscription: PaykitSubscription,
requests: List<PaykitPaymentRequest>,
now: Instant,
completedProofKinds: Map<PaykitPaymentRequestId, PaykitPaymentProofKind> = emptyMap(),
): List<PaykitPaymentRequest> = requests.mapNotNull { request ->
when {
request.lifecycleState == PaymentRequestLifecycleState.PROOF_SUBMITTED -> request
request.id in completedProofKinds -> request.copy(
lifecycleState = PaymentRequestLifecycleState.PROOF_SUBMITTED,
paymentProofKind = completedProofKinds[request.id],
)
subscription.lifecycleState == PaymentRequestLifecycleState.ACTIVE_RECURRING &&
request.isExpired(now) -> request
Comment thread
ben-kaufman marked this conversation as resolved.
else -> null
}
}

private fun isCurrentState(generation: Long, expectedIdentity: String?): Boolean =
stateGeneration.get() == generation && PubkyPublicKeyFormat.matches(activeIdentity, expectedIdentity)

Expand Down Expand Up @@ -1705,23 +1716,24 @@ class PaykitPaymentRequestRepo @Inject constructor(
}

val recurringRequests = requestsThroughAcceptance(subscription, now)
val paymentNow = clock.now()
val unpaidRequests = if (
subscription.lifecycleState == PaymentRequestLifecycleState.ACTIVE_RECURRING
) {
recurringRequests.filter { it.lifecycleState != PaymentRequestLifecycleState.PROOF_SUBMITTED }
recurringRequests.filter {
it.lifecycleState != PaymentRequestLifecycleState.PROOF_SUBMITTED && !it.isExpired(paymentNow)
}
} else {
emptyList()
}
val paidRequests = recurringRequests.filter {
it.lifecycleState == PaymentRequestLifecycleState.PROOF_SUBMITTED
}
val historyRequests = subscriptionHistory(subscription, recurringRequests, paymentNow)
val existingPending = _pendingRequests.value.filterNot { it.belongsTo(subscription) }
_pendingRequests.update {
(unpaidRequests + existingPending).sortedBy { it.createdAt }
}
val existingHistory = _paymentRequestHistory.value.filterNot { it.belongsTo(subscription) }
_paymentRequestHistory.update {
(paidRequests + existingHistory).sortedByDescending { it.createdAt }
(historyRequests + existingHistory).sortedByDescending { it.createdAt }
}
prunePresentedSubscriptionProposalIds(_subscriptions.value)
subscriptionNotificationScheduler.synchronize(
Expand Down Expand Up @@ -1808,9 +1820,15 @@ class PaykitPaymentRequestRepo @Inject constructor(
val now = clock.now()
val subscriptionNow = subscriptionClock.now()
val expiredProposals = expiredProposals(now, handledRequestId)
val expiredPeriods = _pendingRequests.value.filter { it.billingPeriod != null && it.isExpired(now) }
_paymentRequestHistory.update { requests ->
(requests + expiredPeriods)
.distinctBy { it.id }
.withExpiredLifecycle(now)
.sortedByDescending { it.createdAt }
}
_pendingRequests.update { requests -> requests.filterNot { it.isExpired(now) } }
if (expiredProposals.isNotEmpty()) _expiredProposals.tryEmit(expiredProposals)
_paymentRequestHistory.update { requests -> requests.withExpiredLifecycle(now) }
_subscriptions.update { subscriptions -> subscriptions.map { it.withExpiredLifecycle(subscriptionNow) } }
prunePresentedRequestIds(_pendingRequests.value)
prunePresentedSubscriptionProposalIds(_subscriptions.value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -509,33 +509,42 @@ internal fun paymentRequestStatus(
?: stringResource(R.string.wallet__payment_request_pending)
}

return paymentRequestLifecycleStatus(request, isPending)
return stringResource(paymentRequestLifecycleStatusRes(request, isPending))
}

@Composable
private fun paymentRequestLifecycleStatus(request: PaykitPaymentRequest, isPending: Boolean): String {
if (request.lifecycleState == PaymentRequestLifecycleState.PROPOSED && request.isExpired(Clock.System.now())) {
return stringResource(R.string.wallet__payment_request_status_expired)
@StringRes
internal fun paymentRequestLifecycleStatusRes(request: PaykitPaymentRequest, isPending: Boolean): Int {
val canExpire = request.lifecycleState == PaymentRequestLifecycleState.PROPOSED ||
request.lifecycleState == PaymentRequestLifecycleState.ACTIVE_RECURRING
if (canExpire && request.isExpired(Clock.System.now())) {
Comment thread
ben-kaufman marked this conversation as resolved.
return R.string.wallet__payment_request_status_expired
}

return when (request.lifecycleState) {
PaymentRequestLifecycleState.PROPOSED -> stringResource(proposedPaymentRequestStatusRes(request, isPending))
PaymentRequestLifecycleState.PROPOSED -> proposedPaymentRequestStatusRes(request, isPending)
PaymentRequestLifecycleState.PROPOSAL_EXPIRED ->
stringResource(R.string.wallet__payment_request_status_expired)
R.string.wallet__payment_request_status_expired
PaymentRequestLifecycleState.ACCEPTED ->
stringResource(R.string.wallet__payment_request_pending)
R.string.wallet__payment_request_pending
PaymentRequestLifecycleState.REJECTED ->
stringResource(R.string.wallet__payment_request_status_rejected)
R.string.wallet__payment_request_status_rejected
PaymentRequestLifecycleState.CANCELED ->
stringResource(R.string.wallet__payment_request_status_canceled)
R.string.wallet__payment_request_status_canceled
PaymentRequestLifecycleState.PROOF_SUBMITTED ->
stringResource(R.string.wallet__payment_request_status_proof_submitted)
R.string.wallet__payment_request_status_proof_submitted
PaymentRequestLifecycleState.RECOVERY_REQUIRED ->
stringResource(R.string.wallet__payment_request_status_action_required)
R.string.wallet__payment_request_status_action_required
PaymentRequestLifecycleState.ACTIVE_RECURRING ->
if (request.billingPeriod != null &&
request.direction == PaykitPaymentRequestDirection.Incoming && isPending
) {
R.string.wallet__payment_request_waiting
} else {
R.string.wallet__payment_request_status_unavailable
}
PaymentRequestLifecycleState.INVALID_CONFLICT,
PaymentRequestLifecycleState.ACTIVE_RECURRING,
PaymentRequestLifecycleState.UNKNOWN,
-> stringResource(R.string.wallet__payment_request_status_unavailable)
-> R.string.wallet__payment_request_status_unavailable
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,16 @@ import kotlinx.coroutines.launch
import to.bitkit.R
import to.bitkit.ext.dateTimeFormatterOf
import to.bitkit.ext.nowMillis
import to.bitkit.models.NewTransactionSheetDetails
import to.bitkit.models.NewTransactionSheetDirection
import to.bitkit.models.NewTransactionSheetType
import to.bitkit.models.PaykitAmount
import to.bitkit.models.PaykitAsset
import to.bitkit.models.PaykitExchangeRate
import to.bitkit.models.PubkyProfile
import to.bitkit.models.PubkyPublicKeyFormat
import to.bitkit.repositories.MethodId
import to.bitkit.repositories.PaykitPaymentRequest
import to.bitkit.repositories.PaykitPaymentRequestDeliveryStatus
import to.bitkit.repositories.PaykitPaymentRequestId
import to.bitkit.repositories.PaykitRecurrenceUnit
Expand Down Expand Up @@ -107,11 +110,13 @@ import to.bitkit.ui.scaffold.SheetTopBar
import to.bitkit.ui.scaffold.rememberChromeHazeStyle
import to.bitkit.ui.screens.paymentrequests.PaymentRequestCard
import to.bitkit.ui.screens.paymentrequests.PaymentRequestsScreen
import to.bitkit.ui.screens.paymentrequests.hasPaymentEvidence
import to.bitkit.ui.screens.paymentrequests.paymentRequestDate
import to.bitkit.ui.screens.wallets.activity.components.CustomTabRowWithSpacing
import to.bitkit.ui.screens.wallets.activity.components.TabItem
import to.bitkit.ui.shared.modifiers.sheetHeight
import to.bitkit.ui.shared.util.gradientBackground
import to.bitkit.ui.sheets.TransactionConfetti
import to.bitkit.ui.theme.Colors
import to.bitkit.ui.utils.removeAccentTags
import to.bitkit.ui.utils.withAccent
Expand Down Expand Up @@ -319,6 +324,15 @@ internal fun SubscriptionsContent(
}
}

internal fun subscriptionPayments(
subscription: PaykitSubscription,
history: List<PaykitPaymentRequest>,
): List<PaykitPaymentRequest> = if (subscription.isCreatedByUser) {
subscription.receivedPaymentRequests()
} else {
history.filter { it.belongsTo(subscription) && it.hasPaymentEvidence }
}

internal data class SubscriptionSections(
val proposals: List<PaykitSubscription>,
val active: List<PaykitSubscription>,
Expand Down Expand Up @@ -520,11 +534,7 @@ fun SubscriptionDetailScreen(
}
}
item { SubscriptionDetailsGrid(subscription, now) }
val payments = if (subscription.isCreatedByUser) {
subscription.receivedPaymentRequests()
} else {
paymentHistory.filter { it.belongsTo(subscription) }
}
val payments = subscriptionPayments(subscription, paymentHistory)
if (payments.isNotEmpty()) {
item {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
Expand Down Expand Up @@ -929,21 +939,32 @@ fun SubscriptionSuccess(
onClose: () -> Unit,
paymentType: NewTransactionSheetType?,
modifier: Modifier = Modifier,
usdtAmount: ULong? = null,
) {
Box(
modifier = modifier
.fillMaxSize()
.navigationBarsPadding()
) {
val composition by rememberLottieComposition(
LottieCompositionSpec.RawRes(subscriptionConfettiResource(paymentType))
)
LottieAnimation(
composition = composition,
contentScale = ContentScale.Crop,
iterations = 100,
modifier = Modifier.fillMaxSize()
)
if (usdtAmount != null) {
TransactionConfetti(
NewTransactionSheetDetails(
type = NewTransactionSheetType.ONCHAIN,
direction = NewTransactionSheetDirection.SENT,
usdtAmount = usdtAmount,
)
)
} else {
val composition by rememberLottieComposition(
LottieCompositionSpec.RawRes(subscriptionConfettiResource(paymentType))
)
LottieAnimation(
composition = composition,
contentScale = ContentScale.Crop,
iterations = 100,
modifier = Modifier.fillMaxSize()
)
}
Column(
modifier = Modifier
.fillMaxSize()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ fun SendConfirmScreen(
}

@Composable
private fun AutoPayOverlay(
internal fun AutoPayOverlay(
isVisible: Boolean,
content: (@Composable () -> Unit)?,
) {
Expand Down
Loading
Loading