Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
dbb4b40
feat: share paykit state across apps
ben-kaufman Oct 1, 2026
bfe5c7f
chore: rename changelog fragment
ben-kaufman Oct 1, 2026
7d4fcde
docs: clarify paykit integration contracts
ben-kaufman Oct 1, 2026
dd190bf
fix: tighten paykit attribution and cleanup reporting
ben-kaufman Oct 1, 2026
517114b
test: cover paykit key generation and rotation
ben-kaufman Oct 1, 2026
b5a231d
fix: guard paykit execution and cache contact backfills
ben-kaufman Oct 1, 2026
b68738e
fix: prune completed paykit acceptance records
ben-kaufman Oct 1, 2026
b62f6b2
fix: preserve paykit payment recovery and contact choices
ben-kaufman Oct 1, 2026
453bc70
refactor: simplify paykit contact backfill
ben-kaufman Oct 1, 2026
8362c93
chore: update Paykit to rc59
ben-kaufman Oct 1, 2026
967e4c0
fix: preserve private sharing settings during contact cleanup
ben-kaufman Oct 1, 2026
40aef85
test: align request presentation with shared paykit
ben-kaufman Oct 1, 2026
d68ecbb
test: align private paykit settings stub
ben-kaufman Oct 1, 2026
a371aa6
fix: reduce paykit sync overhead and retry session setup
ben-kaufman Oct 1, 2026
0665c49
fix: keep private message sync off frequent request polls
ben-kaufman Oct 2, 2026
5da2ef2
fix: preserve paykit activation and recurring payments
ben-kaufman Oct 2, 2026
8203350
fix: preserve paykit recovery and payment details
ben-kaufman Oct 2, 2026
2458eae
fix: stop redundant paykit polling
ben-kaufman Oct 2, 2026
2ca29dc
fix: guard paykit payment authorization and cleanup
ben-kaufman Oct 2, 2026
8823181
style: format paykit backfill test
ben-kaufman Oct 2, 2026
2f42424
fix: yield between paykit handshake advances
ben-kaufman Oct 2, 2026
7d3c196
fix: consolidate paykit background retries
ben-kaufman Oct 2, 2026
09c6426
fix: receive Paykit messages on each inbox poll
ben-kaufman Oct 2, 2026
10a3a64
fix: report private endpoint withdrawal failures
ben-kaufman Oct 2, 2026
eaf6a09
fix: coalesce paykit preparation and reduce inbox polling
ben-kaufman Oct 2, 2026
0fdff06
fix: coordinate private contact preparation and request discovery
ben-kaufman Oct 2, 2026
85f1963
Avoid transport maintenance when opening subscription reminders
ben-kaufman Oct 2, 2026
bb4637d
fix: exclude sdk queue waits from request discovery timeout
ben-kaufman Oct 2, 2026
75e5429
fix: reduce redundant paykit synchronization
ben-kaufman Oct 3, 2026
4951920
fix: keep contact payments responsive during background sync
ben-kaufman Oct 3, 2026
09e224c
fix: avoid duplicate private cleanup registry sync
ben-kaufman Oct 3, 2026
dc73b29
fix: integrate master profile updates and Paykit rc60
ben-kaufman Oct 3, 2026
288c7d8
perf: use rc61 and separate inbox polling from maintenance
ben-kaufman Oct 3, 2026
172cb30
chore: update Paykit to rc62
ben-kaufman Oct 3, 2026
a29e32d
perf: coalesce overlapping paykit refreshes
ben-kaufman Oct 4, 2026
e8a7686
fix: block deleted contacts without peer records
ben-kaufman Oct 4, 2026
fbe136e
Batch private Paykit list withdrawal
ben-kaufman Oct 5, 2026
b4b2141
fix: preserve Android security providers during crypto setup
ben-kaufman Oct 5, 2026
e4e5f75
Avoid redundant Paykit startup and failed-write reads
ben-kaufman Oct 5, 2026
e7aad37
Skip linked-peer handshake advancement during drains
ben-kaufman Oct 5, 2026
0eaae43
Document foreground Paykit inbox polling
ben-kaufman Oct 5, 2026
a38f515
fix: refresh Paykit requests after state changes
ben-kaufman Oct 5, 2026
b752e7d
fix: preserve Paykit cleanup and request presentation state
ben-kaufman Oct 5, 2026
cd4648b
fix: preserve Paykit presentation and queue ordering
ben-kaufman Oct 5, 2026
3266919
fix: complete active Paykit calls before cancellation
ben-kaufman Oct 5, 2026
3f61604
perf: reuse identity status during private publication
ben-kaufman Oct 5, 2026
11f6861
chore: merge master into shared paykit runtime
ben-kaufman Oct 5, 2026
b43b5d7
Merge master and preserve Paykit session guards
ben-kaufman Oct 5, 2026
d44d5bb
Refresh payment requests only for relevant proof changes
ben-kaufman Oct 5, 2026
0d21ff9
fix: keep Paykit request controls responsive during refresh
ben-kaufman Oct 5, 2026
98a1570
fix: contain identity failures during proof cleanup
ben-kaufman Oct 5, 2026
bb6cef7
fix: retry pending endpoint cleanup during maintenance
ben-kaufman Oct 5, 2026
b101e89
fix: prepare incoming payments in the send sheet
ben-kaufman Oct 6, 2026
9ffb08a
fix: prioritize incoming request work during polling
ben-kaufman Oct 6, 2026
57c9cab
fix: retain subscription reminders through refresh failures
ben-kaufman Oct 6, 2026
5310b75
perf: prioritize sharing withdrawal completion
ben-kaufman Oct 6, 2026
1a1cab3
fix: preserve payment request state through refresh retries
ben-kaufman Oct 6, 2026
de9dd1c
fix: refresh sent payment request receipts
ben-kaufman Oct 6, 2026
bde1523
perf: adopt paykit rc63 runtime updates
ben-kaufman Oct 6, 2026
117072b
Preserve subscription reminders during manual request payments
ben-kaufman Oct 6, 2026
d56c3f7
fix: avoid duplicate paykit activation after import failure
ben-kaufman Oct 6, 2026
51e26b0
fix: coalesce pubky restore and profile loads
ben-kaufman Oct 6, 2026
fe364a5
fix: batch profile deletion and use paykit rc64
ben-kaufman Oct 6, 2026
9083227
fix: restore contacts atomically with Paykit rc65
ben-kaufman Oct 6, 2026
b5c43df
Cover bulk contact assignment cleanup
ben-kaufman Oct 6, 2026
074e84d
Enforce absolute Paykit deadlines before payment submission
ben-kaufman Oct 6, 2026
12a0045
merge: integrate master security provider startup fix
ben-kaufman Oct 6, 2026
53d459a
fix: retain pending links after publication failure
ben-kaufman Oct 7, 2026
fd04581
fix: streamline paykit payment acceptance with rc66
ben-kaufman Oct 7, 2026
fb89098
fix: resume wallet backups after cancellation
ben-kaufman Oct 7, 2026
6a2b4d2
fix: coalesce background paykit link preparation
ben-kaufman Oct 7, 2026
713d4d6
chore: update paykit to rc67
ben-kaufman Oct 7, 2026
90177b5
fix: defer queued acceptance delivery during payment
ben-kaufman Oct 7, 2026
3280a4e
style: align payment callback naming and test formatting
ben-kaufman Oct 7, 2026
804e754
chore: update paykit to rc68
ben-kaufman Oct 7, 2026
6ef9021
fix: complete reconciled hardware payments
ben-kaufman Oct 7, 2026
660c397
Document hardware recovery checks and use atomic result updates
ben-kaufman Oct 7, 2026
9c87876
fix: preserve payment and backup recovery on interruption
ben-kaufman Oct 7, 2026
50811b2
fix: preserve hardware payment failure outcomes
ben-kaufman Oct 7, 2026
7bc3ccb
fix: present queued requests after closing a sheet
ben-kaufman Oct 7, 2026
fd71df5
fix: retry deferred sessions while foregrounded
ben-kaufman Oct 7, 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 @@ -21,6 +21,7 @@ import androidx.navigation.compose.rememberNavController
import androidx.test.ext.junit.runners.AndroidJUnit4
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.flow.flowOf
import org.junit.Before
import org.junit.Rule
import org.junit.Test
Expand Down Expand Up @@ -70,6 +71,7 @@ class DrawerMenuWidgetsTest {
hasSeenShopIntro = true,
onBeforeNavigate = {},
showWidgets = true,
profileIdentityExists = flowOf(false),
)
}
}
Expand Down Expand Up @@ -102,6 +104,7 @@ class DrawerMenuWidgetsTest {
hasSeenShopIntro = true,
onBeforeNavigate = {},
showWidgets = true,
profileIdentityExists = flowOf(false),
onOpenWidgetsHome = { openWidgetsHome.value = true },
)
if (openWidgetsHome.value) {
Expand Down Expand Up @@ -138,6 +141,7 @@ class DrawerMenuWidgetsTest {
hasSeenShopIntro = true,
onBeforeNavigate = {},
showWidgets = false,
profileIdentityExists = flowOf(false),
onOpenWidgetsHome = { error("Should not request home widgets page") },
onOpenWidgetsSheet = { openWidgetsSheet.value = true },
)
Expand Down Expand Up @@ -177,6 +181,7 @@ class DrawerMenuWidgetsTest {
hasSeenShopIntro = true,
onBeforeNavigate = {},
showWidgets = true,
profileIdentityExists = flowOf(true),
isPaykitEnabled = true,
)
}
Expand All @@ -201,6 +206,7 @@ class DrawerMenuWidgetsTest {
hasSeenShopIntro = true,
onBeforeNavigate = {},
showWidgets = true,
profileIdentityExists = flowOf(true),
isPaykitEnabled = false,
)
}
Expand Down
39 changes: 39 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 @@ -88,6 +88,45 @@ class SheetHostTest {
assertEquals(0, backgroundClickCount)
}

@Test
fun dismissingSheetPresentsQueuedSheet() {
val sheet = mutableStateOf<Sheet?>(Sheet.Receive())
val request = Sheet.Send()
var dismissCount = 0
var presentationCount = 0
composeTestRule.setContent {
AppThemeSurface {
SheetHost(
shouldExpand = sheet.value != null,
visibilityKey = sheet.value,
onDismiss = {
dismissCount++
sheet.value = null
sheet.value = request
},
onVisible = { presentationCount++ },
sheets = {
Box(
modifier = Modifier
.fillMaxWidth()
.height(320.dp)
.testTag(if (sheet.value === request) "PaymentRequestSheet" else "ReceiveSheet")
)
},
content = { Box(Modifier.fillMaxSize()) },
)
}
}
composeTestRule.onNodeWithTag("ReceiveSheet").assertIsDisplayed()

pressBack()
composeTestRule.waitForIdle()

composeTestRule.onNodeWithTag("PaymentRequestSheet").assertIsDisplayed()
assertEquals(1, dismissCount)
assertEquals(2, presentationCount)
}

@Test
fun programmaticHideDoesNotInvokeDismissalCallback() {
val shouldExpand = mutableStateOf(true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ 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 com.synonym.paykit.PaymentRequestLifecycleState
import kotlinx.collections.immutable.persistentListOf
import org.junit.Rule
import org.junit.Test
Expand All @@ -24,8 +25,10 @@ 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.PaykitBillingPeriod
import to.bitkit.repositories.PaykitPaymentRequest
import to.bitkit.repositories.PaykitPaymentRequestDeliveryStatus
import to.bitkit.repositories.PaykitPaymentRequestDirection
import to.bitkit.repositories.PaykitPaymentRequestDraft
import to.bitkit.repositories.PaykitPaymentRequestTarget
import to.bitkit.test.annotations.ComposeUi
Expand Down Expand Up @@ -130,11 +133,15 @@ class CreatePaymentRequestScreenTest {
@Test
fun sentShowsSuccessSurface() {
val contact = PubkyProfile.forDisplay(target.publicKey, "Anna", imageUrl = null)
var deliveryStatus by mutableStateOf(PaykitPaymentRequestDeliveryStatus.Queued)
val createdRequest = request.copy(note = null, deliveryStatus = PaykitPaymentRequestDeliveryStatus.Queued)
val sentRequest = createdRequest.copy(deliveryStatus = PaykitPaymentRequestDeliveryStatus.Sent)
val proofRequest = sentRequest.copy(lifecycleState = PaymentRequestLifecycleState.PROOF_SUBMITTED)
var history by mutableStateOf(persistentListOf<PaykitPaymentRequest>())
composeTestRule.setContent {
AppThemeSurface {
PaymentRequestSentContent(
request = request.copy(deliveryStatus = deliveryStatus),
request = createdRequest,
history = history,
contact = contact,
onDone = {},
)
Expand All @@ -145,14 +152,65 @@ class CreatePaymentRequestScreenTest {
composeTestRule.onNodeWithTag("PaymentRequestSentCheck").assertIsDisplayed()
composeTestRule.onNodeWithText("PAYMENT REQUESTED").assertIsDisplayed()
composeTestRule.onNodeWithText("Anna").assertIsDisplayed()
composeTestRule.onNodeWithText("Dinner").assertIsDisplayed()
composeTestRule.onNodeWithText("Queued for delivery").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.runOnIdle { history = persistentListOf(sentRequest) }

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

composeTestRule.runOnIdle { history = persistentListOf(proofRequest) }

composeTestRule.onNodeWithText("Proof submitted").assertIsDisplayed()
composeTestRule.onNodeWithText("Waiting for payment").assertDoesNotExist()
composeTestRule.onNodeWithText("PAYMENT REQUESTED").assertIsDisplayed()

composeTestRule.runOnIdle { history = persistentListOf(proofRequest.copy(note = "Dinner")) }

composeTestRule.onNodeWithText("Dinner").assertIsDisplayed()
composeTestRule.onNodeWithText("Proof submitted").assertDoesNotExist()

composeTestRule.runOnIdle { history = persistentListOf() }

composeTestRule.onNodeWithText("Queued for delivery").assertIsDisplayed()
composeTestRule.onNodeWithText("Dinner").assertDoesNotExist()
}

@Test
fun sentIgnoresHistoryForOtherRequests() {
val createdRequest = request.copy(note = "", deliveryStatus = PaykitPaymentRequestDeliveryStatus.Queued)
val proofRequest = createdRequest.copy(lifecycleState = PaymentRequestLifecycleState.PROOF_SUBMITTED)
var history by mutableStateOf(persistentListOf<PaykitPaymentRequest>())
composeTestRule.setContent {
AppThemeSurface {
PaymentRequestSentContent(
request = createdRequest,
history = history,
contact = null,
onDone = {},
)
}
}

listOf(
proofRequest.copy(paymentRequestId = "another-request"),
proofRequest.copy(counterparty = "another-counterparty"),
proofRequest.copy(direction = PaykitPaymentRequestDirection.Incoming),
proofRequest.copy(
billingPeriod = PaykitBillingPeriod(
startsAt = Instant.parse("2027-01-15T08:00:00Z"),
endsAt = Instant.parse("2027-02-15T08:00:00Z"),
),
),
).forEach { unrelatedRequest ->
composeTestRule.runOnIdle { history = persistentListOf(unrelatedRequest) }

composeTestRule.onNodeWithText("Queued for delivery").assertIsDisplayed()
composeTestRule.onNodeWithText("Proof submitted").assertDoesNotExist()
}
}

private val draft = PaykitPaymentRequestDraft(
Expand All @@ -163,18 +221,17 @@ class CreatePaymentRequestScreenTest {

private val target = PaykitPaymentRequestTarget(
publicKey = "pubky3rsduhcxpw74snwyct86m38c63j3pq8x4ycqikxg64roik8yw5xg",
receiverPath = "bitkit/wallet",
)

private val request = PaykitPaymentRequest(
paymentRequestId = "payment-request",
counterparty = target.publicKey,
counterpartyReceiverPath = target.receiverPath,
amountValue = "0.00025",
amountSats = draft.amountSats,
note = draft.note,
createdAt = Instant.parse("2027-01-15T08:00:00Z"),
expiresAt = draft.expiresAt,
expiresAt = Instant.DISTANT_FUTURE,
acceptedPaymentEndpointIdentifiers = listOf("btc-lightning-bolt11"),
direction = PaykitPaymentRequestDirection.Outgoing,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ package to.bitkit.ui.screens.paymentrequests

import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.assertTextContains
import androidx.compose.ui.test.assertTextEquals
Expand Down Expand Up @@ -40,6 +42,31 @@ class PaymentRequestsScreenTest {
@get:Rule
val composeTestRule = createComposeRule()

@Test
fun detailsDisablePayWhilePreparationIsPending() {
val isPreparing = mutableStateOf(false)
val request = request()
composeTestRule.setContent {
PaymentRequestsTestSurface {
IncomingPaymentRequestDetailsContent(
request = request,
contact = PubkyProfile.placeholder(request.counterparty),
isPending = true,
isPreparing = isPreparing.value,
onBack = {},
onPay = {},
onDismiss = { Result.success(Unit) },
)
}
}

composeTestRule.onNodeWithTag("PaymentRequestDetailsPay").assertIsEnabled()
composeTestRule.runOnIdle { isPreparing.value = true }
composeTestRule.onNodeWithTag("PaymentRequestDetailsPay").assertIsNotEnabled()
composeTestRule.runOnIdle { isPreparing.value = false }
composeTestRule.onNodeWithTag("PaymentRequestDetailsPay").assertIsEnabled()
}

@Test
fun queueShowsIncomingRequestAndSeeAllAction() {
val request = request(id = "incoming")
Expand Down Expand Up @@ -289,7 +316,6 @@ class PaymentRequestsScreenTest {
private fun request(id: String = "request") = PaykitPaymentRequest(
paymentRequestId = id,
counterparty = "pubky3rsduhcxpw74snwyct86m38c63j3pq8x4ycqikxg64roik8yw5xg",
counterpartyReceiverPath = "bitkit/wallet",
amountValue = "0.00025",
amountSats = 25_000uL,
note = "Dinner",
Expand All @@ -301,7 +327,6 @@ class PaymentRequestsScreenTest {
private fun subscription(note: String) = PaykitSubscription(
paymentRequestId = "subscription",
counterparty = request().counterparty,
counterpartyReceiverPath = "bitkit/wallet",
amountValue = "0.00025",
amountSats = 25_000uL,
note = note,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ class CreateSubscriptionScreenTest {

@Test
fun recipientAllowsExactlyOneSelectionAndChangesExpiry() {
val second = PaykitPaymentRequestTarget("pubky" + "z".repeat(52), "bitkit/wallet")
val second = PaykitPaymentRequestTarget("pubky" + "z".repeat(52))
var selected by mutableStateOf<PaykitPaymentRequestTarget?>(null)
var expiration by mutableStateOf(PaymentRequestExpiration.Week)
var proposedTo: PaykitPaymentRequestTarget? = null
Expand Down Expand Up @@ -158,13 +158,12 @@ class CreateSubscriptionScreenTest {
composeTestRule.onNodeWithText("OK").assertIsDisplayed()
}

private val target = PaykitPaymentRequestTarget("pubky" + "y".repeat(52), "bitkit/wallet")
private val target = PaykitPaymentRequestTarget("pubky" + "y".repeat(52))
private val contact = PubkyProfile.forDisplay(target.publicKey, "Anna", null)
private val startsAt = Instant.parse("2027-01-15T08:00:00Z")
private val subscription = PaykitSubscription(
paymentRequestId = "creator-proposal",
counterparty = target.publicKey,
counterpartyReceiverPath = target.receiverPath,
amountValue = "0.00001",
amountSats = 1000uL,
note = "Support",
Expand Down
Loading
Loading