Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
fc2cba3
feat: add Paykit subscription payments
ben-kaufman Aug 26, 2026
bbde9e9
chore: name subscription changelog fragment
ben-kaufman Aug 26, 2026
d1c25ae
fix: polish subscription payments
ben-kaufman Aug 27, 2026
16d3820
fix: streamline onchain proof recovery
ben-kaufman Aug 31, 2026
5c9d1af
fix: address subscription review
ben-kaufman Aug 31, 2026
0d9dc5d
fix: sequence subscription payment sheet
ben-kaufman Sep 1, 2026
ae76211
fix: clean up cancelled hardware payments
ben-kaufman Sep 1, 2026
56d1601
style: simplify subscription test opt-ins
ben-kaufman Sep 1, 2026
7fed8dd
style: simplify ZoneId reference
ben-kaufman Sep 1, 2026
f0eb9d8
fix: preserve payment proof behavior
ben-kaufman Sep 2, 2026
103f11c
fix: keep payment proofs best effort
ben-kaufman Sep 2, 2026
ada2e10
fix: show normalized monthly cost
ben-kaufman Sep 3, 2026
ed030da
fix: harden subscription recovery
ben-kaufman Sep 4, 2026
d7d7ab0
fix: confirm initial subscription fees
ben-kaufman Sep 4, 2026
90b6c09
fix: preserve uncertain lightning payments
ben-kaufman Sep 4, 2026
5339b96
fix: clean up subscription request handling
ben-kaufman Sep 6, 2026
ef50785
fix: preserve subscription payment outcomes and disclose first period
ben-kaufman Sep 7, 2026
60bbd45
refactor: simplify pending request dismissal guard
ben-kaufman Sep 7, 2026
f0ec557
fix: restore subscription details navigation
ben-kaufman Sep 7, 2026
5a61869
Merge branch 'master' into codex/paykit-subscriptions-android
ben-kaufman Sep 8, 2026
2311655
fix: remove duplicate payment request amount
ben-kaufman Sep 8, 2026
b0e89a4
Merge branch 'master' into codex/paykit-subscriptions-android
ben-kaufman Sep 8, 2026
e39a3d4
fix: correct payment request feedback and scrolling
ben-kaufman Sep 8, 2026
c376956
fix: finish subscription cancellation after sheet dismissal
ben-kaufman Sep 8, 2026
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 @@ -153,7 +153,7 @@ class DrawerMenuWidgetsTest {
}

@Test
fun paymentRequestsIsAvailableFromDrawerWhenPaykitIsEnabled() {
fun subscriptionsIsTheOnlyPaykitEntryInDrawerWhenPaykitIsEnabled() {
composeTestRule.setContent {
val navController = rememberNavController()
val drawerState = rememberDrawerState(DrawerValue.Open)
Expand All @@ -166,8 +166,8 @@ class DrawerMenuWidgetsTest {
composable<Routes.Home> {
Text("Home", modifier = Modifier.testTag("HomeRoute"))
}
composable<Routes.PaymentRequests> {
Text("Payment Requests", modifier = Modifier.testTag("PaymentRequestsRoute"))
composable<Routes.Subscriptions> {
Text("Subscriptions", modifier = Modifier.testTag("SubscriptionsRoute"))
}
}
DrawerMenu(
Expand All @@ -182,14 +182,13 @@ class DrawerMenuWidgetsTest {
}
}

composeTestRule.onNodeWithText("REQUESTS").assertIsDisplayed()
composeTestRule.onNodeWithTag("DrawerPaymentRequests").performClick()
composeTestRule.onNodeWithTag("DrawerSubscriptions").performClick()

composeTestRule.onNodeWithTag("PaymentRequestsRoute").assertIsDisplayed()
composeTestRule.onNodeWithTag("SubscriptionsRoute").assertIsDisplayed()
}

@Test
fun paymentRequestsIsHiddenFromDrawerWhenPaykitIsDisabled() {
fun paykitEntriesAreHiddenFromDrawerWhenPaykitIsDisabled() {
composeTestRule.setContent {
val navController = rememberNavController()
val drawerState = rememberDrawerState(DrawerValue.Open)
Expand All @@ -207,7 +206,7 @@ class DrawerMenuWidgetsTest {
}
}

composeTestRule.onNodeWithTag("DrawerPaymentRequests").assertDoesNotExist()
composeTestRule.onNodeWithTag("DrawerSubscriptions").assertDoesNotExist()
}
}

Expand Down
36 changes: 36 additions & 0 deletions app/src/androidTest/java/to/bitkit/ui/components/SheetHostTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.platform.testTag
Expand Down Expand Up @@ -84,4 +85,39 @@ class SheetHostTest {
assertEquals(0, dismissCount)
assertEquals(0, backgroundClickCount)
}

@Test
fun programmaticHideDoesNotInvokeDismissalCallback() {
val shouldExpand = mutableStateOf(true)
val visibilityKey = mutableStateOf<Any?>("subscription")
var dismissCount = 0
composeTestRule.setContent {
AppThemeSurface {
SheetHost(
shouldExpand = shouldExpand.value,
onDismiss = { dismissCount++ },
visibilityKey = visibilityKey.value,
sheets = {
Box(
modifier = Modifier
.fillMaxWidth()
.height(320.dp)
.testTag("ProgrammaticSheet")
)
},
content = { Box(Modifier.fillMaxSize()) },
)
}
}
composeTestRule.onNodeWithTag("ProgrammaticSheet").assertIsDisplayed()

composeTestRule.runOnIdle {
shouldExpand.value = false
visibilityKey.value = null
}
composeTestRule.mainClock.advanceTimeBy(1_000)
composeTestRule.waitForIdle()

assertEquals(0, dismissCount)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,77 +2,124 @@

package to.bitkit.ui.screens.paymentrequests

import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.v2.createComposeRule
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput
import kotlinx.collections.immutable.persistentListOf
import org.junit.Rule
import org.junit.Test
import to.bitkit.models.BITCOIN_SYMBOL
import to.bitkit.models.PrimaryDisplay
import to.bitkit.models.PubkyProfile
import to.bitkit.models.USD_SYMBOL
import to.bitkit.repositories.AmountInputHandler
import to.bitkit.repositories.CurrencyState
import to.bitkit.repositories.PaykitPaymentRequest
import to.bitkit.repositories.PaykitPaymentRequestDeliveryStatus
import to.bitkit.repositories.PaykitPaymentRequestDraft
import to.bitkit.repositories.PaykitPaymentRequestTarget
import to.bitkit.test.annotations.ComposeUi
import to.bitkit.ui.LocalCurrencies
import to.bitkit.ui.theme.AppThemeSurface
import to.bitkit.viewmodels.AmountInputViewModel
import kotlin.time.ExperimentalTime
import kotlin.time.Instant
import kotlin.test.assertEquals

@ComposeUi
class CreatePaymentRequestScreenTest {
@get:Rule
val composeTestRule = createComposeRule()

@Test
fun detailsShowsAmountNoteExpiryAndContinue() {
fun detailsShowsAmountNoteExpiryAndSend() {
composeTestRule.setContent {
AppThemeSurface {
PaymentRequestDetailsContent(
amountInputViewModel = AmountInputViewModel(AmountInputHandler.stub()),
initialDraft = draft,
contact = PubkyProfile.placeholder(target.publicKey),
isCreating = false,
onBack = {},
onContinue = {},
onEditAmount = {},
onSend = {},
)
}
}

composeTestRule.onNodeWithTag("PaymentRequestAmountField").assertIsDisplayed()
composeTestRule.onNodeWithTag("PaymentRequestNote").assertIsDisplayed()
composeTestRule.onNodeWithTag("PaymentRequestExpiryWeek").assertIsDisplayed()
composeTestRule.onNodeWithTag("PaymentRequestAmountContinue").assertIsDisplayed()
composeTestRule.onNodeWithTag("PaymentRequestSend").assertIsDisplayed()
composeTestRule.onNodeWithTag("PaymentRequestNumberPad").assertDoesNotExist()
}

composeTestRule.onNodeWithTag("PaymentRequestEditAmount").performClick()
@Test
fun amountShowsNumberPadAndContinue() {
var currencies by mutableStateOf(CurrencyState())
val amountInputHandler = object : AmountInputHandler by AmountInputHandler.stub() {
override suspend fun switchUnit(unit: PrimaryDisplay) = unit.not().also {
currencies = currencies.copy(primaryDisplay = it)
}
}
val amountInputViewModel = AmountInputViewModel(amountInputHandler)
composeTestRule.setContent {
AppThemeSurface {
CompositionLocalProvider(
LocalInspectionMode provides true,
LocalCurrencies provides currencies,
) {
PaymentRequestAmountContent(
amountInputViewModel = amountInputViewModel,
initialDraft = draft,
contact = PubkyProfile.placeholder(target.publicKey),
onBack = {},
onContinue = {},
)
}
}
}

composeTestRule.onNodeWithTag("PaymentRequestAmountField").assertIsDisplayed()
composeTestRule.onNodeWithTag("PaymentRequestNumberPad").assertIsDisplayed()
composeTestRule.onNodeWithTag("PaymentRequestAmountContinue").assertIsDisplayed()
composeTestRule.onNodeWithTag("PaymentRequestNote").assertDoesNotExist()
composeTestRule.onAllNodesWithText(USD_SYMBOL, substring = true, useUnmergedTree = true).assertCountEquals(1)

composeTestRule.onNodeWithTag("PaymentRequestAmountUnit").performClick()

composeTestRule.onAllNodesWithText(BITCOIN_SYMBOL, substring = true, useUnmergedTree = true).assertCountEquals(1)
}

@Test
fun recipientShowsEligibleContactAndSendAction() {
fun recipientShowsEligibleContactAndAdvancesOnSelection() {
var selectedTarget: PaykitPaymentRequestTarget? = null
composeTestRule.setContent {
AppThemeSurface {
PaymentRequestRecipientContent(
targets = persistentListOf(target),
contacts = persistentListOf(PubkyProfile.placeholder(target.publicKey)),
isCreating = false,
onEditExpiration = {},
onBack = {},
onPaste = { target.publicKey },
onSend = {},
onSelected = { selectedTarget = it },
)
}
}

composeTestRule.onNodeWithTag("PaymentRequestContact${target.publicKey}").assertIsDisplayed()
composeTestRule.onNodeWithTag("PaymentRequestRecipientSearch").assertIsDisplayed()
composeTestRule.onNodeWithTag("PaymentRequestEditExpiration").assertIsDisplayed()
composeTestRule.onNodeWithTag("PaymentRequestRecipientPaste", useUnmergedTree = true).assertIsDisplayed()
composeTestRule.onNodeWithTag("PaymentRequestSend").assertIsDisplayed()
composeTestRule.onNodeWithTag("PaymentRequestSend").assertDoesNotExist()
composeTestRule.onNodeWithTag("PaymentRequestContact${target.publicKey}").performClick()
assertEquals(target, selectedTarget)

composeTestRule.onNodeWithTag("PaymentRequestRecipientSearch").performTextInput("not this contact")

Expand All @@ -82,11 +129,13 @@ class CreatePaymentRequestScreenTest {

@Test
fun sentShowsSuccessSurface() {
val contact = PubkyProfile.forDisplay(target.publicKey, "Anna", imageUrl = null)
var deliveryStatus by mutableStateOf(PaykitPaymentRequestDeliveryStatus.Queued)
composeTestRule.setContent {
AppThemeSurface {
PaymentRequestSentContent(
request = request.copy(deliveryStatus = PaykitPaymentRequestDeliveryStatus.Sent),
contact = PubkyProfile.placeholder(target.publicKey),
request = request.copy(deliveryStatus = deliveryStatus),
contact = contact,
onDone = {},
)
}
Expand All @@ -95,7 +144,15 @@ class CreatePaymentRequestScreenTest {
composeTestRule.onNodeWithTag("PaymentRequestSent").assertIsDisplayed()
composeTestRule.onNodeWithTag("PaymentRequestSentCheck").assertIsDisplayed()
composeTestRule.onNodeWithText("PAYMENT REQUESTED").assertIsDisplayed()
composeTestRule.onNodeWithText("Waiting for payment").assertIsDisplayed()
composeTestRule.onNodeWithText("Anna").assertIsDisplayed()
composeTestRule.onNodeWithText("Dinner").assertIsDisplayed()
composeTestRule.onNodeWithText("Your payment request is queued and will send automatically").assertIsDisplayed()
composeTestRule.onNodeWithText("You have sent a payment request").assertDoesNotExist()

composeTestRule.runOnIdle { deliveryStatus = PaykitPaymentRequestDeliveryStatus.Sent }

composeTestRule.onNodeWithText("You have sent a payment request").assertIsDisplayed()
composeTestRule.onNodeWithText("Your payment request is queued and will send automatically").assertDoesNotExist()
}

private val draft = PaykitPaymentRequestDraft(
Expand Down
Loading
Loading