Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
fdd47c6
feat: add usdt wallet and paykit payments
ben-kaufman Oct 7, 2026
b7a29d0
fix: preserve payment recovery and final authorization
ben-kaufman Oct 7, 2026
3338d04
chore: rename changelog fragment
ben-kaufman Oct 7, 2026
29c2413
feat: add usdt bridge support actions
ben-kaufman Oct 7, 2026
d7fe9c0
Make USDT sharing optional in companion authorization
ben-kaufman Oct 7, 2026
7d9c8da
feat: apply usdt wallet designs
ben-kaufman Oct 7, 2026
d6937b2
fix: align paykit pricing with selected payment methods
ben-kaufman Oct 7, 2026
c2c7c89
fix: align USDT and Paykit screens with design updates
ben-kaufman Oct 8, 2026
592b184
chore: update bitkit core to 0.8.0-rc2
ben-kaufman Oct 8, 2026
0824171
fix: preserve multi-asset paykit pricing
ben-kaufman Oct 8, 2026
cc58f52
fix: align usdt displays with design feedback
ben-kaufman Oct 9, 2026
a032c9c
Preserve payment quotes during recovery and synchronize backups
ben-kaufman Oct 12, 2026
35fa60e
fix: preserve quote deadlines through payment retries
ben-kaufman Oct 12, 2026
51a8947
fix: use usdt colors for paid payment requests
ben-kaufman Oct 12, 2026
c5c25b1
fix: dismiss prepared requests when navigating back
ben-kaufman Oct 12, 2026
eeadd52
fix: route USDT payment links to send flow
ben-kaufman Oct 12, 2026
81871b7
fix: show Lightning invoice fee estimates
ben-kaufman Oct 12, 2026
d9ecb5f
fix: clarify usdt payment status and fee failures
ben-kaufman Oct 12, 2026
917fd95
chore: use the public usdt gateway
ben-kaufman Oct 12, 2026
0b39e7d
fix: preserve payment review and contact state
ben-kaufman Oct 12, 2026
0f88104
refactor: simplify hardware payment preparation
ben-kaufman Oct 12, 2026
82b3e19
fix: preserve reviewed payment terms
ben-kaufman Oct 12, 2026
4d4165d
docs: clarify unfunded Lightning receive journey
ben-kaufman Oct 12, 2026
41959e7
fix: show completed deposit refunds accurately
ben-kaufman Oct 12, 2026
50a61e4
feat: align USDT send and activity designs
ben-kaufman Oct 12, 2026
3ecf2ac
docs: align USDT QA with current navigation
ben-kaufman Oct 12, 2026
a5e0bd4
fix: complete usdt design consistency
ben-kaufman Oct 12, 2026
20e100b
fix(usdt): retain queued sends and align source-menu checks
ben-kaufman Oct 12, 2026
0e7ad01
fix: expose send funding menu to UI automation
ben-kaufman Oct 12, 2026
4b95d8f
fix: keep usdt send review and dismissal safe
ben-kaufman Oct 12, 2026
c9bfc7a
fix: preserve usdt payment links and clarify self sends
ben-kaufman Oct 12, 2026
557e66f
fix: clarify bridge amount errors and incoming progress
ben-kaufman Oct 12, 2026
b5270f1
docs: clarify hardware manual entry journey
ben-kaufman Oct 13, 2026
6c2e1d2
fix: clarify usdt review and navigation
ben-kaufman Oct 13, 2026
82a1a9f
fix: distinguish sent and received bridge amounts
ben-kaufman Oct 13, 2026
adca467
fix: preserve usdt flows after rebase
ben-kaufman Oct 13, 2026
056273b
Implement USDT opt-in and updated Figma sheets
ben-kaufman Oct 13, 2026
3f3980c
fix: align USDT screens and payment recovery
ben-kaufman Oct 13, 2026
15fe80d
fix: respect usdt opt-in across payment flows
ben-kaufman Oct 13, 2026
de1f9dc
test: cover initial usdt subscription payments
ben-kaufman Oct 13, 2026
e6d9511
feat: align payment requests and Earn with USDT designs
ben-kaufman Oct 13, 2026
8657caf
Merge master security updates into USDT work
ben-kaufman Oct 13, 2026
87be4ac
fix: keep Earn disclosures visible on compact screens
ben-kaufman Oct 13, 2026
2de00fb
fix: align subscription totals and use core rc6
ben-kaufman Oct 13, 2026
a39ba35
fix: align earn introduction copy
ben-kaufman Oct 13, 2026
eac781d
fix: preserve usdt consent and payment navigation
ben-kaufman Oct 13, 2026
9c7f570
fix: preserve active usdt payments when reading new addresses
ben-kaufman Oct 14, 2026
b014863
fix: preserve usdt send state across recreation
ben-kaufman Oct 14, 2026
1ccb284
docs: clarify receive behavior after usdt restore
ben-kaufman Oct 14, 2026
c51f3bd
fix: preserve paykit review and recovery across assets
ben-kaufman Oct 14, 2026
b1fbfb8
fix: format usd request amounts for the device locale
ben-kaufman Oct 14, 2026
f2f766a
Match USDT bridge activity and refund designs
ben-kaufman Oct 14, 2026
d3acbb6
Align USDT bridge typography and spacing with designs
ben-kaufman Oct 14, 2026
77545b8
docs: correct the documented core version
ben-kaufman Oct 14, 2026
3f44eec
fix: match exact usdt activity searches
ben-kaufman Oct 14, 2026
4f3ab97
fix: unblock usdt contact payments
ben-kaufman Oct 14, 2026
f5dc875
docs: align subscription acceptance journey
ben-kaufman Oct 14, 2026
00a8e5d
fix: localize usdt refund amounts
ben-kaufman Oct 14, 2026
7df6209
test: verify localized usdt refund flow
ben-kaufman Oct 14, 2026
af799d0
fix: preserve refund editing during refresh
ben-kaufman Oct 14, 2026
dcbf816
perf: reuse valid usdt payment quotes
ben-kaufman Oct 15, 2026
423b74a
fix: prefer funded usdt for quoted requests
ben-kaufman Oct 15, 2026
12dfd8a
fix: preserve usdt recovery and payment details
ben-kaufman Oct 15, 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
29 changes: 27 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ android {
unitTests {
isReturnDefaultValues = true // mockito
isIncludeAndroidResources = true // robolectric
all { it.maxHeapSize = "2g" }
}
}
lint {
Expand All @@ -320,6 +321,23 @@ androidComponents {
val buildConfigFields = requireNotNull(variant.buildConfigFields) {
"buildFeatures.buildConfig must stay enabled for '${variant.name}'."
}
buildConfigFields.put(
"USDT_BUNDLER_URL",
localProp("USDT_BUNDLER_URL").orElse("https://bitkit.to/v1/usdt/rpc").stringField(),
)
buildConfigFields.put(
"USDT_BRIDGES_URL",
localProp("USDT_BRIDGES_URL").orElse("https://bitkit.to/v1/usdt/bridges").stringField(),
)
buildConfigFields.put(
"USDT_DEPOSITS_URL",
localProp("USDT_DEPOSITS_URL").orElse("https://bitkit.to/v1/usdt/deposits").stringField(),
)
buildConfigFields.put("USDT_BRIDGE_NETWORKS", localProp("USDT_BRIDGE_NETWORKS").orElse("").stringField())
buildConfigFields.put(
"USDT_RPC_URL",
localProp("USDT_RPC_URL").orElse("https://bitkit.to/v1/usdt/chain-rpc").stringField(),
)
buildConfigFields.put("E2E", e2eEnv.booleanField())
buildConfigFields.put("E2E_BACKEND", e2eBackendEnv.stringField())
buildConfigFields.put("E2E_LOCAL_HOST", e2eLocalHostEnv.stringField())
Expand Down Expand Up @@ -516,10 +534,17 @@ dependencies {
// Crypto
implementation(libs.bouncycastle.provider.jdk)
implementation(libs.ldk.node.android) { exclude(group = "net.java.dev.jna", module = "jna") }
implementation(libs.bitkit.core)
val localBitkitCoreAar = providers.gradleProperty("bitkitCoreAar").orNull
if (localBitkitCoreAar != null) {
debugImplementation(files(localBitkitCoreAar))
debugImplementation("org.jetbrains.kotlinx:atomicfu:0.23.1")
releaseImplementation(libs.bitkit.core)
} else {
implementation(libs.bitkit.core)
}
implementation(libs.paykit)
implementation(libs.vss.client)
nativeDebugSymbols(libs.bitkit.core.nativeDebugSymbolsArtifact())
if (localBitkitCoreAar == null) nativeDebugSymbols(libs.bitkit.core.nativeDebugSymbolsArtifact())
nativeDebugSymbols(libs.ldk.node.android.nativeDebugSymbolsArtifact())
nativeDebugSymbols(libs.paykit.nativeDebugSymbolsArtifact())
nativeDebugSymbols(libs.vss.client.nativeDebugSymbolsArtifact())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package to.bitkit.ui.components
import androidx.compose.foundation.layout.Column
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertHasClickAction
import androidx.compose.ui.test.assertHeightIsEqualTo
import androidx.compose.ui.test.junit4.v2.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
Expand Down Expand Up @@ -50,7 +51,7 @@ class ButtonTest {
onClick = {},
size = ButtonSize.Small,
fullWidth = false,
modifier = Modifier.testTag("SecondarySmallEnabled")
testTag = "SecondarySmallEnabled",
)
SecondaryButton(
text = "Secondary",
Expand Down Expand Up @@ -94,6 +95,7 @@ class ButtonTest {
).forEach {
composeTestRule.onNodeWithTag(it).assertHeightIsEqualTo(40.dp)
}
composeTestRule.onNodeWithTag("SecondarySmallEnabled").assertHasClickAction()
composeTestRule.onNodeWithTag("PrimaryLarge").assertHeightIsEqualTo(56.dp)
composeTestRule.onNodeWithTag("SecondaryLarge").assertHeightIsEqualTo(56.dp)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package to.bitkit.ui.components

import android.content.Context
import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import to.bitkit.ext.getClipboardText
import to.bitkit.models.NewTransactionSheetDetails
import to.bitkit.models.NewTransactionSheetDirection
import to.bitkit.models.NewTransactionSheetType
import to.bitkit.test.annotations.ComposeUi
import to.bitkit.ui.screens.wallets.usdt.UsdtReceiveContent
import to.bitkit.ui.sheets.NewTransactionSheetView
import to.bitkit.ui.theme.AppThemeSurface
import java.util.Locale
import kotlin.test.assertEquals

@HiltAndroidTest
@ComposeUi
class UsdtReceivePrivacyTest {
private val originalLocale = Locale.getDefault()

@get:Rule
val hiltRule = HiltAndroidRule(this)

@get:Rule
val composeTestRule = createAndroidComposeRule<ComponentActivity>()

@Before
fun setup() {
Locale.setDefault(Locale.US)
hiltRule.inject()
}

@After
fun teardown() {
Locale.setDefault(originalLocale)
}

@Test
fun qrCopyPreservesPaymentUriAndDetailsCopyOnlyAddress() {
val address = "0x18503bD685aF84E6A7bC289898bAa1C3f5d8527E"
val uri = "ethereum:0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9@42161/transfer?address=$address&uint256=3500000"
lateinit var context: Context
composeTestRule.setContent {
context = LocalContext.current
AppThemeSurface {
Column {
UsdtReceiveContent(address, uri, null, "Arbitrum One", {}, {})
}
}
}
composeTestRule.onNodeWithTag("ReceiveCopyQR").performClick()
composeTestRule.runOnIdle { assertEquals(uri, context.getClipboardText()) }
composeTestRule.onNodeWithTag("UsdtReceiveDetails").performClick()
composeTestRule.onNodeWithText("Copy").performClick()
composeTestRule.runOnIdle { assertEquals(address, context.getClipboardText()) }
composeTestRule.runOnIdle { composeTestRule.activity.onBackPressedDispatcher.onBackPressed() }
composeTestRule.onNodeWithTag("ReceiveCopyQR").assertIsDisplayed()
}

@Test
fun incomingPaymentHonorsHiddenBalance() {
composeTestRule.mainClock.autoAdvance = false
val hidden = mutableStateOf(true)
composeTestRule.setContent {
AppThemeSurface {
NewTransactionSheetView(
details = NewTransactionSheetDetails(
type = NewTransactionSheetType.ONCHAIN,
direction = NewTransactionSheetDirection.RECEIVED,
usdtAmount = 123_456_789uL,
),
onCloseClick = {},
onDetailClick = {},
hideBalance = hidden.value,
)
}
}
composeTestRule.mainClock.advanceTimeBy(1000)
composeTestRule.onNodeWithText("123.46").assertDoesNotExist()
composeTestRule.runOnIdle { hidden.value = false }
composeTestRule.mainClock.advanceTimeByFrame()
composeTestRule.onNodeWithText("123.46").assertIsDisplayed()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ class MnemonicInputFieldTest {
onPositionChange = {},
onBackspaceInEmpty = { backspaceInEmptyCount++ },
focusRequester = focusRequester,
index = 0,
isFocused = true,
Comment thread
ben-kaufman marked this conversation as resolved.
index = 0,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,30 @@ 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.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.assertIsNotSelected
import androidx.compose.ui.test.assertIsSelected
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.performScrollTo
import androidx.compose.ui.test.performTextInput
import com.synonym.paykit.PaymentRequestLifecycleState
import kotlinx.collections.immutable.persistentListOf
import org.junit.Rule
import org.junit.Test
import to.bitkit.models.BITCOIN_SYMBOL
import to.bitkit.models.PaykitAmount
import to.bitkit.models.PaykitAsset
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.MethodId
import to.bitkit.repositories.PaykitBillingPeriod
import to.bitkit.repositories.PaykitPaymentRequest
import to.bitkit.repositories.PaykitPaymentRequestDeliveryStatus
Expand All @@ -35,9 +43,9 @@ import to.bitkit.test.annotations.ComposeUi
import to.bitkit.ui.LocalCurrencies
import to.bitkit.ui.theme.AppThemeSurface
import to.bitkit.viewmodels.AmountInputViewModel
import kotlin.test.assertEquals
import kotlin.time.ExperimentalTime
import kotlin.time.Instant
import kotlin.test.assertEquals

@ComposeUi
class CreatePaymentRequestScreenTest {
Expand Down Expand Up @@ -65,6 +73,41 @@ class CreatePaymentRequestScreenTest {
composeTestRule.onNodeWithTag("PaymentRequestNumberPad").assertDoesNotExist()
}

@Test
fun acceptedAssetsGroupsBitcoinRailsAndRequiresASelection() {
var submitted: PaykitPaymentRequestDraft? = null
composeTestRule.setContent {
AppThemeSurface {
PaymentRequestDetailsContent(
initialDraft = draft,
availableMethods = persistentListOf(
MethodId.P2wpkh.rawValue,
MethodId.Bolt11.rawValue,
MethodId.UsdtArbitrum.rawValue
),
contact = PubkyProfile.placeholder(target.publicKey),
isCreating = false,
onBack = {},
onEditAmount = {},
onSend = { submitted = it },
)
}
}
composeTestRule.onNodeWithTag("PaymentRequestAccept-bitcoin")
.performScrollTo().assertIsSelected().performClick()
composeTestRule.onNodeWithTag("PaymentRequestAccept-usdt").assertIsSelected()
composeTestRule.onNodeWithTag("PaymentRequestSend").performClick()
assertEquals(listOf(MethodId.UsdtArbitrum.rawValue), submitted?.acceptedPaymentEndpointIdentifiers)
composeTestRule.onNodeWithTag("PaymentRequestAccept-usdt").performClick().assertIsNotSelected()
composeTestRule.onNodeWithTag("PaymentRequestSend").assertIsNotEnabled()
composeTestRule.onNodeWithTag("PaymentRequestAccept-bitcoin").performClick().assertIsSelected()
composeTestRule.onNodeWithTag("PaymentRequestSend").assertIsEnabled().performClick()
assertEquals(
setOf(MethodId.P2wpkh.rawValue, MethodId.Bolt11.rawValue),
submitted?.acceptedPaymentEndpointIdentifiers?.toSet()
)
}

@Test
fun amountShowsNumberPadAndContinue() {
var currencies by mutableStateOf(CurrencyState())
Expand Down Expand Up @@ -99,7 +142,11 @@ class CreatePaymentRequestScreenTest {

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

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

@Test
Expand Down Expand Up @@ -214,7 +261,7 @@ class CreatePaymentRequestScreenTest {
}

private val draft = PaykitPaymentRequestDraft(
amountSats = 25_000uL,
amount = PaykitAmount(PaykitAsset.BTC, 25_000uL),
note = "Dinner",
expiresAt = Instant.parse("2027-01-15T09:00:00Z"),
)
Expand All @@ -226,8 +273,8 @@ class CreatePaymentRequestScreenTest {
private val request = PaykitPaymentRequest(
paymentRequestId = "payment-request",
counterparty = target.publicKey,
amountValue = "0.00025",
amountSats = draft.amountSats,
amount = draft.amount,
paymentReference = "reference",
note = draft.note,
createdAt = Instant.parse("2027-01-15T08:00:00Z"),
expiresAt = Instant.DISTANT_FUTURE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentSetOf
import org.junit.Rule
import org.junit.Test
import to.bitkit.models.PaykitAmount
import to.bitkit.models.PaykitAsset
import to.bitkit.models.PaykitRequestPricing
import to.bitkit.models.PrimaryDisplay
import to.bitkit.models.PubkyProfile
import to.bitkit.repositories.CurrencyState
Expand Down Expand Up @@ -316,8 +319,8 @@ class PaymentRequestsScreenTest {
private fun request(id: String = "request") = PaykitPaymentRequest(
paymentRequestId = id,
counterparty = "pubky3rsduhcxpw74snwyct86m38c63j3pq8x4ycqikxg64roik8yw5xg",
amountValue = "0.00025",
amountSats = 25_000uL,
amount = PaykitAmount(PaykitAsset.BTC, 25_000uL),
paymentReference = "reference-$id",
note = "Dinner",
createdAt = Instant.parse("2027-01-15T08:00:00Z"),
expiresAt = null,
Expand All @@ -327,8 +330,9 @@ class PaymentRequestsScreenTest {
private fun subscription(note: String) = PaykitSubscription(
paymentRequestId = "subscription",
counterparty = request().counterparty,
amountValue = "0.00025",
amountSats = 25_000uL,
amount = PaykitAmount(PaykitAsset.BTC, 25_000uL),
paymentReference = "invoice",
pricing = PaykitRequestPricing(),
note = note,
createdAt = Instant.parse("2027-01-15T08:00:00Z"),
proposalExpiresAt = null,
Expand Down
Loading
Loading