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 @@ -340,4 +340,40 @@ class SendConfirmScreenTest {
assertEquals(amountBounds, amount.getUnclippedBoundsInRoot())
assertEquals(swipeBounds, swipe.getUnclippedBoundsInRoot())
}

@Test
fun merchantRequestLabelsThePayeeAsTo() {
val seller = PubkyProfile.placeholder("seller").copy(name = "Corner Shop")
val state = mutableStateOf(
SendUiState(
amount = 25_000u,
address = "bcrt1qkgfgyxyqhvkdqh04sklnzxphmcds6vft6y7h0r",
isAmountInputValid = true,
payMethod = SendMethod.ONCHAIN,
isPaymentRequest = true,
isMerchantPaymentRequest = true,
contactPaymentProfile = seller,
paymentRequestNote = "Pubky Shop order 1a2b3c4d",
)
)
composeTestRule.setContent {
AppThemeSurface {
SendConfirmContent(
uiState = state.value,
isNodeRunning = true,
isLoading = false,
showBiometrics = false,
modifier = Modifier.sheetHeight(),
)
}
}

composeTestRule.onNodeWithTag("SendConfirmToggleDetails").performClick()
composeTestRule.onNodeWithText("To", ignoreCase = true).performScrollTo().assertIsDisplayed()
composeTestRule.onNodeWithText("Contact", ignoreCase = true).assertDoesNotExist()

composeTestRule.runOnIdle { state.value = state.value.copy(isMerchantPaymentRequest = false) }
composeTestRule.onNodeWithText("Contact", ignoreCase = true).performScrollTo().assertIsDisplayed()
composeTestRule.onNodeWithText("To", ignoreCase = true).assertDoesNotExist()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ data class PaykitPaymentRequest(
val paymentDeadlineAt: Instant? = null,
val isPaymentInFlight: Boolean = false,
val proposalStreamItemId: ULong? = null,
/** An incoming request a Paykit server created for a merchant, such as a Pubky Market order. */
val isMerchantRequest: Boolean = false,
) {
val amountValue: String get() = amount.value

Expand Down Expand Up @@ -2011,8 +2013,12 @@ private fun PaymentRequestRecord.toPaykitPaymentRequest(
paymentProofKind = paymentProofs.lastOrNull()?.let {
PaykitPaymentProofKind.fromPaymentEndpointIdentifier(it.paymentEndpointIdentifier)
},
isMerchantRequest = expectedRole == PaymentRequestLocalRole.PAYER && proposalAppId == PAYKIT_SERVER_APP_ID,
)

/** The app id a Paykit server stamps on requests it creates for merchants. */
private const val PAYKIT_SERVER_APP_ID = "paykit-server"

internal fun PaymentRequestRecord.toPaykitPaymentRequestHistory(now: Instant): PaykitPaymentRequest? {
val role = localRole ?: return null
if (role == PaymentRequestLocalRole.UNKNOWN) return null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,16 @@ internal fun IncomingPaymentRequestDetailsContent(
)
}
VerticalSpacer(20.dp)
Caption13Up(text = stringResource(R.string.wallet__payment_request_contact), color = Colors.White64)
Caption13Up(
text = stringResource(
if (request.isMerchantRequest) {
R.string.wallet__send_to
} else {
R.string.wallet__payment_request_contact
}
),
color = Colors.White64,
)
VerticalSpacer(8.dp)
Row(
verticalAlignment = Alignment.CenterVertically,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -602,7 +602,7 @@ private fun OnChainDetails(
}
SendCell(
caption = stringResource(
if (uiState.isOneOffPaymentRequest) {
if (uiState.isOneOffPaymentRequest && !uiState.isMerchantPaymentRequest) {
R.string.wallet__payment_request_contact
} else {
R.string.wallet__send_to
Expand Down Expand Up @@ -748,7 +748,7 @@ private fun LightningDetails(
}
SendCell(
caption = stringResource(
if (uiState.isOneOffPaymentRequest) {
if (uiState.isOneOffPaymentRequest && !uiState.isMerchantPaymentRequest) {
R.string.wallet__payment_request_contact
} else {
R.string.wallet__send_to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ fun PaykitUsdtReview(
amountEditable = false,
isPaymentRequest = uiState.isPaymentRequest,
paymentRequestNote = uiState.paymentRequestNote,
isMerchantRequest = uiState.isMerchantPaymentRequest,
contact = uiState.contactPaymentProfile,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ internal fun UsdtPaymentContent(
amountEditable: Boolean = true,
isPaymentRequest: Boolean = false,
paymentRequestNote: String? = null,
isMerchantRequest: Boolean = false,
contact: PubkyProfile? = null,
onDestinationChange: ((UsdtDestination) -> Unit)? = null,
destinations: ImmutableList<UsdtDestination> = Env.usdtDestinations.toImmutableList(),
Expand Down Expand Up @@ -209,6 +210,7 @@ internal fun UsdtPaymentContent(
state,
isPaymentRequest,
paymentRequestNote,
isMerchantRequest,
contact,
onConfirm,
completed,
Expand Down Expand Up @@ -536,6 +538,7 @@ private fun UsdtConfirmation(
state: UsdtSendState,
isPaymentRequest: Boolean,
paymentRequestNote: String?,
isMerchantRequest: Boolean,
contact: PubkyProfile?,
onConfirm: () -> Unit,
completed: Boolean,
Expand All @@ -557,6 +560,7 @@ private fun UsdtConfirmation(
contact,
note,
isPaymentRequest,
isMerchantRequest,
showDetails,
) {
if (!state.busy && !completed) onEditAmount()
Expand Down Expand Up @@ -640,6 +644,7 @@ private fun UsdtReviewHeader(
contact: PubkyProfile?,
note: String?,
isPaymentRequest: Boolean,
isMerchantRequest: Boolean,
showDetails: Boolean,
modifier: Modifier = Modifier,
onEditAmount: () -> Unit,
Expand All @@ -652,7 +657,7 @@ private fun UsdtReviewHeader(
)
VerticalSpacer(24.dp)
if (showDetails) {
UsdtQuoteDetails(quote, contact)
UsdtQuoteDetails(quote, contact, isMerchantRequest)
note?.let {
VerticalSpacer(16.dp)
PaymentRequestInvoiceNote(it)
Expand All @@ -665,7 +670,7 @@ private fun UsdtReviewHeader(
}

@Composable
private fun UsdtQuoteDetails(quote: UsdtQuote, contact: PubkyProfile?) {
private fun UsdtQuoteDetails(quote: UsdtQuote, contact: PubkyProfile?, isMerchantRequest: Boolean) {
var showFullAddress by rememberSaveable(quote.recipient) { mutableStateOf(false) }
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
UsdtDetailField(caption = stringResource(R.string.usdt__pay_from), modifier = Modifier.weight(1f)) {
Expand Down Expand Up @@ -720,7 +725,15 @@ private fun UsdtQuoteDetails(quote: UsdtQuote, contact: PubkyProfile?) {
VerticalSpacer(16.dp)
}
if (contact != null) {
SendCell(caption = stringResource(R.string.wallet__payment_request_contact)) {
SendCell(
caption = stringResource(
if (isMerchantRequest) {
R.string.wallet__send_to
} else {
R.string.wallet__payment_request_contact
}
)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
PubkyContactAvatar(profile = contact, size = 20.dp)
HorizontalSpacer(4.dp)
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1623,6 +1623,7 @@ class AppViewModel @Inject constructor(
resetSendState(
isPaymentRequest = true,
paymentRequestNote = request.note,
isMerchantPaymentRequest = request.isMerchantRequest,
hardwareWalletId = proof.onchainWalletId,
incomingPaymentRequestId = request.id,
selectedTags = requestedPaymentRequestTags,
Expand Down Expand Up @@ -4246,6 +4247,7 @@ class AppViewModel @Inject constructor(
contactPaymentProfile = contactPaymentProfile,
isPaymentRequest = isPaymentRequest,
paymentRequestNote = incomingPaymentRequest?.note,
isMerchantPaymentRequest = incomingPaymentRequest?.isMerchantRequest == true,
isSubscriptionPayment = incomingPaymentRequest?.billingPeriod != null,
isInitialSubscriptionPayment = synchronized(contactPaymentContextLock) {
activeContactPaymentContext?.isInitialSubscriptionPayment == true
Expand Down Expand Up @@ -6601,6 +6603,7 @@ class AppViewModel @Inject constructor(
contactPaymentProfile: PubkyProfile? = null,
isPaymentRequest: Boolean = false,
paymentRequestNote: String? = null,
isMerchantPaymentRequest: Boolean = false,
hardwareWalletId: String? = activeHardwareWalletId,
isSubscriptionPayment: Boolean = false,
isInitialSubscriptionPayment: Boolean = false,
Expand All @@ -6627,6 +6630,7 @@ class AppViewModel @Inject constructor(
contactPaymentProfile = contactPaymentProfile,
isPaymentRequest = isPaymentRequest,
paymentRequestNote = paymentRequestNote,
isMerchantPaymentRequest = isMerchantPaymentRequest,
hardwareWalletId = hardwareWalletId,
hardwareWalletName = hardwareWalletId?.let { walletId ->
hwWalletRepo.wallets.value.find { it.id == walletId }?.name
Expand Down Expand Up @@ -8088,6 +8092,7 @@ data class SendUiState(
val contactPaymentProfile: PubkyProfile? = null,
val isPaymentRequest: Boolean = false,
val paymentRequestNote: String? = null,
val isMerchantPaymentRequest: Boolean = false,
val paykitUsesUsdt: Boolean = false,
val paykitRateUnavailable: Boolean = false,
val paykitAmount: String = "",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -984,6 +984,18 @@ class PaykitPaymentRequestRepoTest : BaseUnitTest(StandardTestDispatcher()) {
assertEquals(listOf(MethodId.Bolt11.rawValue), request.acceptedPaymentEndpointIdentifiers)
}

@Test
fun `requests created by a paykit server are merchant requests`() {
fun parse(record: PaymentRequestRecord) =
(record.parseIncomingPaykitPaymentRequest(START_TIME) as PaykitPaymentRequestParseResult.Parsed).request

assertFalse(parse(paymentRequestRecord()).isMerchantRequest)
assertTrue(parse(paymentRequestRecord().copy(proposalAppId = "paykit-server")).isMerchantRequest)
assertFalse(parse(paymentRequestRecord().copy(proposalAppId = "marketplace")).isMerchantRequest)
val outgoing = paymentRequestRecord(role = PaymentRequestLocalRole.PAYEE).copy(proposalAppId = "paykit-server")
assertFalse(requireNotNull(outgoing.toPaykitPaymentRequestHistory(START_TIME)).isMerchantRequest)
}

@Test
fun `absolute payment deadlines preserve fractional seconds and include the deadline instant`() {
val deadline = START_TIME + 123.milliseconds
Expand Down
1 change: 1 addition & 0 deletions changelog.d/next/1625.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Payment requests from a shop now show the payee under To instead of Contact.
3 changes: 2 additions & 1 deletion journeys/payment-requests/issuer-interoperability.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<action>Verify the one-time request row (testTag "PaymentRequestRow-71300000-0000-4000-8000-000000000001") shows 100,000 sats</action>
<action>Tap the one-time request row (testTag "PaymentRequestRow-71300000-0000-4000-8000-000000000001")</action>
<action>Verify Payment Request details (testTag "PaymentRequestDetailsScreen") shows an unsigned 100,000 sats amount (testTag "PaymentRequestDetailsAmount") and status "Waiting for payment" (testTag "PaymentRequestDetailsStatus")</action>
<action>Verify the requester heading on Payment Request details reads "To"</action>
<action>Tap Back (testTag "NavigationBack")</action>
<action>Tap the pending Payment Requests bell (testTag "PaymentRequestsBell")</action>
<action>Verify the incoming Payment Requests sheet (testTag "PaymentRequestsSheet") appears</action>
Expand All @@ -21,6 +22,6 @@
<action>Verify the Payment Request confirmation screen (testTag "PaymentRequestConfirm") shows 100,000 sats</action>
<action>Wait for fresh validation and wallet preparation to finish; verify the same sheet stays open and its payment swipe control stops showing progress</action>
<action>Tap Show details (testTag "SendConfirmToggleDetails")</action>
<action>Verify the recipient (testTag "ReviewContactRecipient") is the saved fixture issuer contact</action>
<action>Verify the recipient (testTag "ReviewContactRecipient") under To is the saved fixture issuer contact; the fixture is a Paykit server request, so the label reads "To" rather than "Contact"</action>
</actions>
</journey>
Loading