diff --git a/app/src/androidTest/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreenTest.kt b/app/src/androidTest/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreenTest.kt index f15e5372bd..0406dd27bf 100644 --- a/app/src/androidTest/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreenTest.kt +++ b/app/src/androidTest/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreenTest.kt @@ -28,6 +28,7 @@ import androidx.compose.ui.test.swipeRight import androidx.compose.ui.test.swipeUp import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp +import com.synonym.paykit.PaymentRequestLifecycleState import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Before @@ -36,9 +37,14 @@ import org.junit.Test import to.bitkit.models.FeeRate import to.bitkit.models.PubkyProfile import to.bitkit.repositories.PaykitPaymentRequest +import to.bitkit.repositories.PaykitRecurrenceUnit +import to.bitkit.repositories.PaykitSubscription +import to.bitkit.repositories.PaykitSubscriptionMetadata +import to.bitkit.repositories.PaykitSubscriptionRecurrence import to.bitkit.test.annotations.ComposeUi import to.bitkit.ui.components.Sheet import to.bitkit.ui.components.SheetHost +import to.bitkit.ui.screens.subscriptions.SubscriptionFirstPaymentProgress import to.bitkit.ui.shared.modifiers.sheetHeight import to.bitkit.ui.sheets.SendRoute import to.bitkit.ui.theme.AppThemeSurface @@ -47,6 +53,7 @@ import to.bitkit.viewmodels.SendMethod import to.bitkit.viewmodels.SendUiState import kotlin.test.assertEquals import kotlin.test.assertTrue +import kotlin.time.Clock import kotlin.time.ExperimentalTime @HiltAndroidTest @@ -155,7 +162,65 @@ class SendConfirmScreenTest { } @Test - fun initialOnchainSubscriptionShowsFeeBeforeConfirmation() { + fun initialOnchainSubscriptionKeepsReviewLayoutWhilePaying() { + val startsAt = Clock.System.now() + val subscription = PaykitSubscription( + paymentRequestId = "subscription-id", + counterparty = "payee", + amountValue = "0.00003", + amountSats = 3_000uL, + note = "Journey Sub", + createdAt = startsAt, + proposalExpiresAt = null, + recurrence = PaykitSubscriptionRecurrence( + every = 1, + unit = PaykitRecurrenceUnit.Month, + startsAt = startsAt, + anchor = startsAt, + endsAt = null, + ), + metadata = PaykitSubscriptionMetadata(description = null, benefits = emptyList()), + acceptedPaymentEndpointIdentifiers = listOf("btc-regtest-p2wpkh"), + lifecycleState = PaymentRequestLifecycleState.ACTIVE_RECURRING, + paidPeriods = emptyList(), + ) + val state = SendUiState( + amount = 3_000u, + payMethod = SendMethod.ONCHAIN, + isAmountInputValid = true, + isInitialSubscriptionPayment = true, + initialSubscriptionPaymentAutoStartPending = true, + onchainFeeUi = OnchainFeeUi(rate = FeeRate.NORMAL, sats = 422), + ) + composeTestRule.setContent { + AppThemeSurface { + CompositionLocalProvider(LocalInspectionMode provides true) { + SendConfirmContent( + uiState = state, + isNodeRunning = true, + isLoading = false, + showBiometrics = false, + autoPayContent = { + SubscriptionFirstPaymentProgress( + subscription = subscription, + contact = PubkyProfile.placeholder("payee"), + ) + }, + ) + } + } + } + + composeTestRule.onNodeWithText("Journey Sub").assertIsDisplayed() + composeTestRule.onNodeWithTag("GRAB").assertIsDisplayed() + composeTestRule.onNodeWithTag("GRAB").performTouchInput { swipeRight() } + composeTestRule.onNodeWithText("Journey Sub").assertIsDisplayed() + composeTestRule.onNodeWithTag("SendConfirmToggleDetails").assertDoesNotExist() + composeTestRule.onNodeWithText("Swipe To Subscribe & Pay").assertDoesNotExist() + } + + @Test + fun initialOnchainSubscriptionShowsProgressWhenSubscriptionIsMissing() { val state = SendUiState( amount = 3_000u, payMethod = SendMethod.ONCHAIN, @@ -172,16 +237,13 @@ class SendConfirmScreenTest { isNodeRunning = true, isLoading = false, showBiometrics = false, - initialShowDetails = true, ) } } } - composeTestRule.onNodeWithTag("SendConfirmAssetButton").assertIsDisplayed() - composeTestRule.onNodeWithText("422", substring = true).assertIsDisplayed() - composeTestRule.onNodeWithTag("SendConfirmToggleDetails").assertIsDisplayed() - composeTestRule.onNodeWithText("Swipe To Subscribe & Pay").assertIsDisplayed() + composeTestRule.onNodeWithTag("GRAB").assertDoesNotExist() + composeTestRule.onNodeWithText("Swipe To Subscribe & Pay").assertDoesNotExist() } @Test diff --git a/app/src/main/java/to/bitkit/ui/components/SwipeToConfirm.kt b/app/src/main/java/to/bitkit/ui/components/SwipeToConfirm.kt index 1db7b12dd4..582c0b926e 100644 --- a/app/src/main/java/to/bitkit/ui/components/SwipeToConfirm.kt +++ b/app/src/main/java/to/bitkit/ui/components/SwipeToConfirm.kt @@ -79,6 +79,7 @@ fun SwipeToConfirm( val maxPanX = if (swiperWidth == 0f) 1f else swiperWidth - with(LocalDensity.current) { CircleSize.toPx() } val panX = remember { Animatable(0f) } + val startedConfirmed = remember { confirmed } val loadingOpacity = remember { Animatable(0f) } val contentAlpha = if (enabled || loading) 1f else 0.5f @@ -90,8 +91,13 @@ fun SwipeToConfirm( } LaunchedEffect(confirmed, maxPanX) { + val target = if (confirmed) maxPanX else 0f + if (startedConfirmed && confirmed) { + panX.snapTo(target) + return@LaunchedEffect + } panX.animateTo( - targetValue = if (confirmed) maxPanX else 0f, + targetValue = target, animationSpec = spring() ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/subscriptions/SubscriptionsScreen.kt b/app/src/main/java/to/bitkit/ui/screens/subscriptions/SubscriptionsScreen.kt index 0fed380841..ee8847c07d 100644 --- a/app/src/main/java/to/bitkit/ui/screens/subscriptions/SubscriptionsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/subscriptions/SubscriptionsScreen.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity @@ -637,6 +638,11 @@ private fun SubscriptionDetailFooter( } } +/** Figma draws the review clock at 288dp inside its 256dp slot, rotated 15 degrees and shifted 7dp left. */ +private const val CLOCK_SCALE = 288f / 256f +private const val CLOCK_ROTATION_DEGREES = 15f +private val CLOCK_OFFSET_X = (-7).dp + @Composable fun SubscriptionSheet(appViewModel: AppViewModel, initialRoute: SubscriptionRoute) { var route by remember(initialRoute) { mutableStateOf(initialRoute) } @@ -672,11 +678,9 @@ fun SubscriptionSheet(appViewModel: AppViewModel, initialRoute: SubscriptionRout VerticalSpacer(16.dp) } } else { - val payOnAcceptance = subscription.paymentDueOnAcceptance(now, Clock.System.now()) != null when (route) { is SubscriptionRoute.Review -> SubscriptionReview( subscription = subscription, - payOnAcceptance = payOnAcceptance, now = now, contact = contacts.contactFor(subscription), onDetails = { @@ -746,19 +750,38 @@ private val SubscriptionRoute.id: PaykitSubscriptionId is SubscriptionRoute.Cancel -> id } +/** Keeps the Review & Subscribe layout on screen while the first payment of an accepted subscription is sent. */ +@Composable +fun SubscriptionFirstPaymentProgress( + subscription: PaykitSubscription, + contact: PubkyProfile, + modifier: Modifier = Modifier, +) { + SubscriptionReview( + subscription = subscription, + contact = contact, + now = rememberSubscriptionNow(persistentListOf(subscription)), + onDetails = null, + onSubscribe = { true }, + isPaying = true, + modifier = modifier + ) +} + @Composable private fun SubscriptionReview( subscription: PaykitSubscription, contact: PubkyProfile, - payOnAcceptance: Boolean, now: Instant, - onDetails: () -> Unit, + onDetails: (() -> Unit)?, onSubscribe: suspend () -> Boolean, + modifier: Modifier = Modifier, + isPaying: Boolean = false, ) { - var loading by remember { mutableStateOf(false) } + var loading by remember { mutableStateOf(isPaying) } val scope = rememberCoroutineScope() Column( - modifier = Modifier + modifier = modifier .fillMaxSize() .navigationBarsPadding() .padding(horizontal = 16.dp) @@ -769,18 +792,7 @@ private fun SubscriptionReview( } MoneyDisplay(sats = subscription.displaySats, showSymbol = true) VerticalSpacer(24.dp) - SubscriptionProviderCard(subscription, contact, onClick = onDetails) - subscription.paymentDueOnAcceptance(now, Clock.System.now())?.billingPeriod?.let { period -> - VerticalSpacer(16.dp) - BodyS( - text = stringResource( - R.string.subscriptions__first_period_ends, - dateTimeFormatterOf("MMM d, yyyy, HH:mm") - .format(java.time.Instant.ofEpochMilli(period.endsAt.toEpochMilliseconds())), - ), - color = Colors.White64, - ) - } + SubscriptionProviderCard(subscription, contact, onClick = onDetails, showAsCard = true) if (!subscription.recurrence.unit.isSupported) { VerticalSpacer(16.dp) BodyM(text = stringResource(R.string.subscriptions__unsupported_description), color = Colors.White64) @@ -795,20 +807,23 @@ private fun SubscriptionReview( Image( painter = painterResource(R.drawable.subscription_clock), contentDescription = null, - modifier = Modifier.size(256.dp).align(Alignment.CenterHorizontally) + modifier = Modifier + .size(256.dp) + .align(Alignment.CenterHorizontally) + .graphicsLayer { + scaleX = CLOCK_SCALE + scaleY = CLOCK_SCALE + rotationZ = CLOCK_ROTATION_DEGREES + translationX = CLOCK_OFFSET_X.toPx() + } ) FillHeight() - if (subscription.isProposalActionable(now)) { + if (loading || subscription.isProposalActionable(now)) { SwipeToConfirm( - text = stringResource( - if (payOnAcceptance) { - R.string.subscriptions__swipe_to_subscribe_and_pay - } else { - R.string.subscriptions__swipe_to_subscribe - } - ), + text = stringResource(R.string.subscriptions__swipe_to_subscribe), color = Colors.Brand, loading = loading, + confirmed = isPaying, onConfirm = { loading = true scope.launch { @@ -827,16 +842,17 @@ private fun SubscriptionProviderCard( contact: PubkyProfile, subtitle: String? = null, onClick: (() -> Unit)? = null, + showAsCard: Boolean = onClick != null, ) { val displayedSubtitle = subtitle ?: subscription.subscriptionFrequencyText() - val cardModifier = if (onClick == null) { + val cardModifier = if (!showAsCard) { Modifier.fillMaxWidth() } else { Modifier .fillMaxWidth() .clip(RoundedCornerShape(16.dp)) .background(Colors.Gray6) - .clickable(onClick = onClick) + .then(onClick?.let { Modifier.clickable(onClick = it) } ?: Modifier) .padding(16.dp) } Row( @@ -857,7 +873,7 @@ private fun SubscriptionProviderCard( overflow = TextOverflow.Ellipsis, ) } - if (onClick != null) { + if (showAsCard) { Icon(painterResource(R.drawable.ic_chevron_right), contentDescription = null, tint = Colors.White64) } } @@ -1002,7 +1018,7 @@ private fun SubscriptionCancel( } } -private fun List.contactFor(subscription: PaykitSubscription): PubkyProfile = +internal fun List.contactFor(subscription: PaykitSubscription): PubkyProfile = firstOrNull { PubkyPublicKeyFormat.matches(it.publicKey, subscription.counterparty) } ?: PubkyProfile.placeholder(subscription.counterparty) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt index e39418165b..42d0abf701 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt @@ -126,6 +126,7 @@ fun SendConfirmScreen( onClickAddTag: () -> Unit, onClickTag: (String) -> Unit, onNavigateToPin: () -> Unit, + autoPayContent: (@Composable () -> Unit)? = null, ) { val scope = rememberCoroutineScope() var isLoading by rememberSaveable { mutableStateOf(false) } @@ -188,6 +189,7 @@ fun SendConfirmScreen( showBiometrics = showBiometrics, canGoBack = canGoBack, initialShowDetails = uiState.isInitialSubscriptionPayment && !uiState.shouldAutomaticallyPay, + autoPayContent = autoPayContent, onBack = onBack, onEvent = onEvent, onClickAddTag = onClickAddTag, @@ -212,6 +214,21 @@ fun SendConfirmScreen( ) } +@Composable +private fun AutoPayOverlay( + isVisible: Boolean, + content: (@Composable () -> Unit)?, +) { + if (!isVisible || content == null) return + Box( + modifier = Modifier + .fillMaxSize() + .gradientBackground() + ) { + content() + } +} + @Composable @Suppress("CyclomaticComplexMethod") internal fun SendConfirmContent( @@ -224,6 +241,7 @@ internal fun SendConfirmContent( preparingContact: PubkyProfile? = null, canGoBack: Boolean = true, initialShowDetails: Boolean = false, + autoPayContent: (@Composable () -> Unit)? = null, onBack: () -> Unit = {}, onEvent: (SendEvent) -> Unit = {}, onClickAddTag: () -> Unit = {}, @@ -289,6 +307,8 @@ internal fun SendConfirmContent( } } + AutoPayOverlay(isVisible = !isPreparing && isAutomaticPaymentLoading, content = autoPayContent) + if (showBiometrics && !isPreparing) { BiometricsView( onSuccess = onBiometricsSuccess, diff --git a/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt index 87d1361153..8833fd49c2 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt @@ -49,11 +49,14 @@ import to.bitkit.models.PubkyPublicKeyFormat import to.bitkit.models.SendFailureDetails import to.bitkit.repositories.ConnectivityState import to.bitkit.repositories.PaykitPaymentRequest +import to.bitkit.repositories.PaykitSubscriptionId import to.bitkit.ui.components.ConnectionIssuesView import to.bitkit.ui.components.SyncNodeView import to.bitkit.ui.navigateTo import to.bitkit.ui.screens.scanner.QrScanningScreen +import to.bitkit.ui.screens.subscriptions.SubscriptionFirstPaymentProgress import to.bitkit.ui.screens.subscriptions.SubscriptionSuccess +import to.bitkit.ui.screens.subscriptions.contactFor import to.bitkit.ui.screens.wallets.send.AddTagScreen import to.bitkit.ui.screens.wallets.send.HwSendSignScreen import to.bitkit.ui.screens.wallets.send.HwSendViewModel @@ -339,6 +342,12 @@ fun SendSheet( } val uiState by appViewModel.sendUiState.collectAsStateWithLifecycle() val lightningState by walletViewModel.lightningState.collectAsStateWithLifecycle() + val initialSubscriptionId = uiState.incomingPaymentRequestId + ?.takeIf { uiState.isInitialSubscriptionPayment } + ?.let { id -> PaykitSubscriptionId(id.paymentRequestId, id.counterparty) } + val initialSubscription = remember(initialSubscriptionId) { + initialSubscriptionId?.let(appViewModel::subscription) + } SendConfirmScreen( savedStateHandle = it.savedStateHandle, @@ -357,6 +366,14 @@ fun SendSheet( onClickAddTag = { navController.navigateTo(SendRoute.AddTag) }, onClickTag = { tag -> appViewModel.removeTag(tag) }, onNavigateToPin = { navController.navigateTo(SendRoute.PinCheck) }, + autoPayContent = initialSubscription?.let { subscription -> + { + SubscriptionFirstPaymentProgress( + subscription = subscription, + contact = contacts.contactFor(subscription), + ) + } + }, ) } composableWithDefaultTransitions { diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 9bb6e638ee..c2b8695038 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -6534,7 +6534,7 @@ data class SendUiState( val incomingPaymentRequestId: PaykitPaymentRequestId? = null, ) { val shouldAutomaticallyPay: Boolean - get() = isInitialSubscriptionPayment && payMethod == SendMethod.LIGHTNING && hardwareWalletId == null + get() = isInitialSubscriptionPayment && hardwareWalletId == null } @Immutable diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 93142cb8a2..020e27cdca 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1125,7 +1125,6 @@ Expires %1$s First Payment Failed You’re subscribed, but your first payment wasn’t sent. - First billing period ends %1$s. Each period is charged in full. Frequency Could not load this image. Choose another image and try again. Monthly diff --git a/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt b/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt index 83192292e1..65541819d4 100644 --- a/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt +++ b/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt @@ -8655,7 +8655,7 @@ class AppViewModelSendFlowTest : BaseUnitTest() { } @Test - fun `initial onchain subscription payment requires confirmation`() = test { + fun `initial onchain subscription payment starts automatically`() = test { balanceState.value = BalanceState(maxSendOnchainSats = 100_000u, maxSendLightningSats = 100_000u) setSendState( SendUiState( @@ -8670,14 +8670,65 @@ class AppViewModelSendFlowTest : BaseUnitTest() { sut.setSendEvent(SendEvent.StartInitialSubscriptionPayment) advanceUntilIdle() - assertFalse(sut.sendUiState.value.shouldAutomaticallyPay) + assertTrue(sut.sendUiState.value.shouldAutomaticallyPay) assertFalse(sut.sendUiState.value.initialSubscriptionPaymentAutoStartPending) + assertTrue(sut.sendUiState.value.shouldConfirmPay) + } + + @Test + fun `initial onchain subscription payment stops at the fee warning`() = test { + balanceState.value = BalanceState(maxSendOnchainSats = 100_000u, maxSendLightningSats = 100_000u) + whenever { lightningRepo.calculateTotalFee(any(), anyOrNull(), any(), anyOrNull(), anyOrNull()) } + .thenReturn(Result.success(600uL)) + whenever(currencyRepo.convertSatsToFiat(any(), anyOrNull())).thenReturn( + Result.success( + ConvertedAmount( + value = BigDecimal.ZERO, + formatted = "0.00", + symbol = "$", + currency = USD, + flag = "", + sats = 0, + ) + ) + ) + setSendState( + SendUiState( + address = REGTEST_ADDRESS, + amount = 1_000u, + payMethod = SendMethod.ONCHAIN, + isAmountInputValid = true, + isInitialSubscriptionPayment = true, + initialSubscriptionPaymentAutoStartPending = true, + ) + ) + + sut.setSendEvent(SendEvent.StartInitialSubscriptionPayment) + advanceUntilIdle() + + assertEquals(SanityWarning.FEE_OVER_HALF_VALUE, sut.sendUiState.value.showSanityWarningDialog) assertFalse(sut.sendUiState.value.shouldConfirmPay) + } + + @Test + fun `initial hardware subscription payment requires confirmation`() = test { + balanceState.value = BalanceState(maxSendOnchainSats = 100_000u, maxSendLightningSats = 100_000u) + setSendState( + SendUiState( + amount = 1_000u, + payMethod = SendMethod.ONCHAIN, + isAmountInputValid = true, + hardwareWalletId = HARDWARE_WALLET_ID, + isInitialSubscriptionPayment = true, + initialSubscriptionPaymentAutoStartPending = true, + ) + ) - setSendState(sut.sendUiState.value.copy(payMethod = SendMethod.LIGHTNING)) sut.setSendEvent(SendEvent.StartInitialSubscriptionPayment) advanceUntilIdle() + assertFalse(sut.sendUiState.value.shouldAutomaticallyPay) + assertFalse(sut.sendUiState.value.initialSubscriptionPaymentAutoStartPending) assertFalse(sut.sendUiState.value.shouldConfirmPay) } diff --git a/changelog.d/next/1422.changed.md b/changelog.d/next/1422.changed.md new file mode 100644 index 0000000000..a227b3d205 --- /dev/null +++ b/changelog.d/next/1422.changed.md @@ -0,0 +1 @@ +Accepting a subscription now sends the first payment in the same swipe when it is paid on-chain, as it already did over Lightning. diff --git a/journeys/subscriptions/cancellation-during-confirmation.xml b/journeys/subscriptions/cancellation-during-confirmation.xml index 2b5a6fa5f1..ab503f7633 100644 --- a/journeys/subscriptions/cancellation-during-confirmation.xml +++ b/journeys/subscriptions/cancellation-during-confirmation.xml @@ -1,16 +1,21 @@ - Requires a linked regtest issuer and a payer funded for a 5,000-sat on-chain payment. + Requires a linked regtest issuer and a payer funded for two 5,000-sat on-chain payments. The issuer can cancel an accepted recurring request with a fixed P2WPKH endpoint and no - payment deadline. Verifies cancellation while confirmation is open. Cancellation after payment - preparation or during an authorization lookup is covered by unit tests. + payment deadline. Verifies cancellation while confirmation is open. The first period is paid by + the acceptance swipe itself, so the open confirmation is reached on the next period by advancing + the subscription clock offset. Cancellation after payment preparation or during an authorization + lookup is covered by unit tests. Have the issuer propose a monthly subscription for 5,000 sats with no payment deadline and an explicit regtest P2WPKH endpoint - Accept the proposal in Bitkit and open its unpaid period to the send confirmation screen without confirming payment + Accept the proposal in Bitkit and wait for the first period's payment to send and the Subscribed confirmation to appear, then close it + Advance the subscription clock offset to the next monthly period in Dev Settings, then refresh Subscriptions + Open the next unpaid period to the send confirmation screen without confirming payment Have the issuer cancel the subscription and keep Bitkit foregrounded until the payer's request state or logs confirm receipt of the cancellation If the send confirmation is still open, try to confirm payment and verify it is refused - Verify no transaction to the request's receiving address was broadcast and no payment proof was created for the period + Verify no transaction for that period was broadcast to the request's receiving address and no payment proof was created for the period Return to Subscriptions and verify the canceled subscription offers no action to pay its unpaid period + Reset the subscription clock offset in Dev Settings diff --git a/journeys/subscriptions/review-and-subscribe.xml b/journeys/subscriptions/review-and-subscribe.xml index 6c7a50352e..c2da0e4de6 100644 --- a/journeys/subscriptions/review-and-subscribe.xml +++ b/journeys/subscriptions/review-and-subscribe.xml @@ -2,20 +2,22 @@ Payer side of create-and-propose.xml. The proposed subscription arrives, surfaces under PROPOSALS, and opens the Review and Subscribe sheet where the payer swipes to accept. Accepting does not - return to the list on its own: when the first period is due on acceptance the send flow starts and - must be completed, otherwise the Subscribed confirmation opens directly. Both paths end on Close. + return to the list on its own: when the first period is due on acceptance the first payment starts + by itself, from Lightning or on-chain savings, with no second swipe; otherwise the Subscribed + confirmation opens directly. Both paths end on Close. A hardware wallet still shows the send + confirmation for the first payment. Run this on the payer instance immediately after the creator sends the proposal. - Launch the E2E Bitkit app with Paykit UI enabled on the payer instance, with the creator saved as a linked contact and a spendable balance above 5,000 sats + Launch the E2E Bitkit app with Paykit UI enabled on the payer instance, with the creator saved as a linked contact, on-chain savings above 5,000 sats plus the network fee, and no usable Lightning balance, so a first payment is sent on-chain Have the creator instance send the "Journey Sub" proposal as described in create-and-propose.xml Open the drawer menu and tap Subscriptions Verify a row for "Journey Sub" appears under the PROPOSALS section Tap the "Journey Sub" row (testTag "SubscriptionRow-<paymentRequestId>") - Verify the Review and Subscribe sheet appears with 5,000 sats and the creator contact card - Verify the swipe control reads "Swipe To Subscribe & Pay" when the first period is due on acceptance, otherwise "Swipe To Subscribe" + Verify the Review and Subscribe sheet appears with 5,000 sats and the subscription card showing its name and cadence + Verify the swipe control reads "Swipe To Subscribe", whether or not the first period is due on acceptance Swipe the confirm control to the end - If the first period was due on acceptance: complete the send flow that opens, then verify the Subscribed confirmation appears + If the first period was due on acceptance: verify the Review and Subscribe layout stays on screen with the swipe control in its loading state and no second swipe is asked for, wait for the payment to send, then verify the Subscribed confirmation appears If no payment was due: verify the Subscribed confirmation appears directly, with no send flow Tap Close on the Subscribed confirmation If a payment was made: verify its billing period is no longer offered with Pay or Dismiss, no Payment Requests sheet opens for it, and the home payment-request bell is absent when no other requests are pending