feat(wallet): implement /pay fallback UX for recipients without linked wallets
- Add ManualAddressChanged event for manual address entry - Add manualAddressInput and manualAddressError fields to PaymentEntryState - Add resolvedAddress field to track the final Cardano address - Update PaymentEntryPresenter to handle manual address entry flow - Add ManualAddressEntryCard component with embedded text field - Validate manual addresses (addr1/addr_test1, length 58-108) - Update PaymentEntryNode to pass resolvedAddress to confirmation screen Flow B: When recipient has no linked wallet, show warning banner and editable address field for manual entry. Continue button enables when valid address is entered.
This commit is contained in:
parent
c35289a3bd
commit
2b93236229
5 changed files with 222 additions and 29 deletions
|
|
@ -58,7 +58,8 @@ class PaymentEntryNode(
|
|||
PaymentEntryView(
|
||||
state = state,
|
||||
onContinue = {
|
||||
val recipientAddress = state.recipientInput
|
||||
// Use the resolved Cardano address (from lookup or manual entry)
|
||||
val recipientAddress = state.resolvedAddress ?: return@PaymentEntryView
|
||||
val amount = state.parsedAmountLovelace ?: return@PaymentEntryView
|
||||
callback.onContinue(recipientAddress, amount)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -80,16 +80,19 @@ class PaymentEntryPresenter @AssistedInject constructor(
|
|||
isCheckingWallet = false,
|
||||
amountInput = "",
|
||||
recipientInput = "",
|
||||
manualAddressInput = "",
|
||||
prefillAmount = null,
|
||||
prefillRecipient = null,
|
||||
parsedAmountLovelace = null,
|
||||
isValidRecipient = false,
|
||||
recipientResolutionState = RecipientResolutionState.NotNeeded,
|
||||
resolvedAddress = null,
|
||||
senderAddress = null,
|
||||
senderBalanceAda = null,
|
||||
isTestnet = CardanoNetworkConfig.NETWORK == CardanoNetwork.TESTNET,
|
||||
amountError = null,
|
||||
recipientError = null,
|
||||
manualAddressError = null,
|
||||
canContinue = false,
|
||||
eventSink = {},
|
||||
)
|
||||
|
|
@ -102,6 +105,7 @@ class PaymentEntryPresenter @AssistedInject constructor(
|
|||
|
||||
var amountInput by remember { mutableStateOf(prefillAmount?.let { formatLovelaceInput(it) } ?: "") }
|
||||
var recipientInput by remember { mutableStateOf(prefillRecipient ?: "") }
|
||||
var manualAddressInput by remember { mutableStateOf("") }
|
||||
var senderAddress by remember { mutableStateOf<String?>(null) }
|
||||
var senderBalanceLovelace by remember { mutableStateOf<Lovelace?>(null) }
|
||||
var recipientResolutionState by remember { mutableStateOf<RecipientResolutionState>(RecipientResolutionState.NotNeeded) }
|
||||
|
|
@ -133,6 +137,8 @@ class PaymentEntryPresenter @AssistedInject constructor(
|
|||
isCardanoAddress -> {
|
||||
recipientResolutionState = RecipientResolutionState.NotNeeded
|
||||
resolvedCardanoAddress = recipientInput
|
||||
// Clear manual entry when direct address is entered
|
||||
manualAddressInput = ""
|
||||
}
|
||||
isMatrixUser -> {
|
||||
// Start lookup
|
||||
|
|
@ -157,6 +163,7 @@ class PaymentEntryPresenter @AssistedInject constructor(
|
|||
matrixUserId = recipientInput,
|
||||
displayName = null
|
||||
)
|
||||
// Don't set resolvedCardanoAddress - user must enter manually
|
||||
}
|
||||
}
|
||||
.onFailure { e ->
|
||||
|
|
@ -174,6 +181,20 @@ class PaymentEntryPresenter @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
// When in manual entry mode, validate and use the manual address
|
||||
val needsManualEntry = recipientResolutionState is RecipientResolutionState.NeedsManualEntry
|
||||
val manualAddressError = if (needsManualEntry && manualAddressInput.isNotBlank()) {
|
||||
validateManualAddress(manualAddressInput)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
// If manual address is valid, use it as the resolved address
|
||||
val finalResolvedAddress = when {
|
||||
needsManualEntry && manualAddressInput.isNotBlank() && manualAddressError == null -> manualAddressInput
|
||||
else -> resolvedCardanoAddress
|
||||
}
|
||||
|
||||
val parsedAmountLovelace = parseAmountInput(amountInput)
|
||||
val amountError = validateAmount(parsedAmountLovelace, amountInput)
|
||||
|
||||
|
|
@ -181,21 +202,25 @@ class PaymentEntryPresenter @AssistedInject constructor(
|
|||
val isMatrixUser = MATRIX_USER_REGEX.matches(recipientInput)
|
||||
val recipientError = validateRecipient(recipientInput, isCardanoAddress, isMatrixUser, recipientResolutionState)
|
||||
|
||||
// Recipient is valid if we have a direct Cardano address or a resolved one from Matrix lookup
|
||||
val isValidRecipient = isCardanoAddress || resolvedCardanoAddress != null
|
||||
// Recipient is valid if we have a final resolved address
|
||||
val isValidRecipient = finalResolvedAddress != null
|
||||
val canContinue = parsedAmountLovelace != null &&
|
||||
parsedAmountLovelace >= MIN_AMOUNT_LOVELACE &&
|
||||
amountError == null &&
|
||||
isValidRecipient &&
|
||||
recipientError == null
|
||||
(recipientError == null || needsManualEntry) // Allow continue in manual entry mode if address is valid
|
||||
|
||||
fun handleEvent(event: PaymentFlowEvents) {
|
||||
when (event) {
|
||||
is PaymentFlowEvents.AmountChanged -> amountInput = event.amount
|
||||
is PaymentFlowEvents.RecipientChanged -> {
|
||||
recipientInput = event.recipient
|
||||
// Clear resolved address when input changes
|
||||
// Clear resolved address and manual entry when input changes
|
||||
resolvedCardanoAddress = null
|
||||
manualAddressInput = ""
|
||||
}
|
||||
is PaymentFlowEvents.ManualAddressChanged -> {
|
||||
manualAddressInput = event.address
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
|
|
@ -210,16 +235,19 @@ class PaymentEntryPresenter @AssistedInject constructor(
|
|||
isCheckingWallet = false,
|
||||
amountInput = amountInput,
|
||||
recipientInput = recipientInput,
|
||||
manualAddressInput = manualAddressInput,
|
||||
prefillAmount = prefillAmount,
|
||||
prefillRecipient = prefillRecipient,
|
||||
parsedAmountLovelace = parsedAmountLovelace,
|
||||
isValidRecipient = isValidRecipient,
|
||||
recipientResolutionState = recipientResolutionState,
|
||||
resolvedAddress = finalResolvedAddress,
|
||||
senderAddress = senderAddress,
|
||||
senderBalanceAda = senderBalanceAda,
|
||||
isTestnet = CardanoNetworkConfig.NETWORK == CardanoNetwork.TESTNET,
|
||||
amountError = amountError,
|
||||
recipientError = recipientError,
|
||||
recipientError = if (needsManualEntry) null else recipientError, // Hide error in manual entry mode
|
||||
manualAddressError = manualAddressError,
|
||||
canContinue = canContinue,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
|
|
@ -272,7 +300,7 @@ class PaymentEntryPresenter @AssistedInject constructor(
|
|||
return when (resolutionState) {
|
||||
is RecipientResolutionState.Resolving -> null // Still looking up
|
||||
is RecipientResolutionState.Found -> null // Found address
|
||||
is RecipientResolutionState.NeedsManualEntry -> "${resolutionState.matrixUserId} hasn't linked a Cardano wallet"
|
||||
is RecipientResolutionState.NeedsManualEntry -> null // Will use manual entry field
|
||||
is RecipientResolutionState.Error -> resolutionState.message
|
||||
else -> null
|
||||
}
|
||||
|
|
@ -286,4 +314,31 @@ class PaymentEntryPresenter @AssistedInject constructor(
|
|||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun validateManualAddress(input: String): String? {
|
||||
if (input.isBlank()) return null
|
||||
|
||||
// Must start with addr_test1 (preprod) or addr1 (mainnet)
|
||||
val isTestnet = input.startsWith("addr_test1")
|
||||
val isMainnet = input.startsWith("addr1") && !input.startsWith("addr_test1")
|
||||
|
||||
if (!isTestnet && !isMainnet) {
|
||||
return "Address must start with addr1 or addr_test1"
|
||||
}
|
||||
|
||||
// Length check: Cardano addresses are typically 58-108 characters
|
||||
if (input.length < 58) {
|
||||
return "Address too short"
|
||||
}
|
||||
if (input.length > 108) {
|
||||
return "Address too long"
|
||||
}
|
||||
|
||||
// Basic character validation
|
||||
if (!CARDANO_ADDRESS_REGEX.matches(input)) {
|
||||
return "Invalid Cardano address format"
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,16 +18,22 @@ data class PaymentEntryState(
|
|||
val isCheckingWallet: Boolean,
|
||||
val amountInput: String,
|
||||
val recipientInput: String,
|
||||
/** Manual address entry field - shown when recipient has no linked wallet. */
|
||||
val manualAddressInput: String,
|
||||
val prefillAmount: Lovelace?,
|
||||
val prefillRecipient: String?,
|
||||
val parsedAmountLovelace: Lovelace?,
|
||||
val isValidRecipient: Boolean,
|
||||
val recipientResolutionState: RecipientResolutionState,
|
||||
/** The final resolved Cardano address to use for the transaction. */
|
||||
val resolvedAddress: String?,
|
||||
val senderAddress: String?,
|
||||
val senderBalanceAda: String?,
|
||||
val isTestnet: Boolean,
|
||||
val amountError: String?,
|
||||
val recipientError: String?,
|
||||
/** Validation error for manual address entry field. */
|
||||
val manualAddressError: String?,
|
||||
val canContinue: Boolean,
|
||||
val eventSink: (PaymentFlowEvents) -> Unit,
|
||||
) {
|
||||
|
|
@ -37,6 +43,10 @@ data class PaymentEntryState(
|
|||
String.format("%.6f", ada).trimEnd('0').trimEnd('.')
|
||||
}
|
||||
|
||||
/** True when the user must manually enter an address for the recipient. */
|
||||
val needsManualAddressEntry: Boolean
|
||||
get() = recipientResolutionState is RecipientResolutionState.NeedsManualEntry
|
||||
|
||||
companion object {
|
||||
/** Initial loading state while checking wallet. */
|
||||
val Loading = PaymentEntryState(
|
||||
|
|
@ -44,16 +54,19 @@ data class PaymentEntryState(
|
|||
isCheckingWallet = true,
|
||||
amountInput = "",
|
||||
recipientInput = "",
|
||||
manualAddressInput = "",
|
||||
prefillAmount = null,
|
||||
prefillRecipient = null,
|
||||
parsedAmountLovelace = null,
|
||||
isValidRecipient = false,
|
||||
recipientResolutionState = RecipientResolutionState.NotNeeded,
|
||||
resolvedAddress = null,
|
||||
senderAddress = null,
|
||||
senderBalanceAda = null,
|
||||
isTestnet = false,
|
||||
amountError = null,
|
||||
recipientError = null,
|
||||
manualAddressError = null,
|
||||
canContinue = false,
|
||||
eventSink = {},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -218,13 +218,20 @@ private fun PaymentFormContent(
|
|||
)
|
||||
}
|
||||
is RecipientResolutionState.NeedsManualEntry -> {
|
||||
MatrixUserNeedsAddressCard(
|
||||
ManualAddressEntryCard(
|
||||
matrixUserId = resolution.matrixUserId,
|
||||
displayName = resolution.displayName,
|
||||
manualAddressInput = state.manualAddressInput,
|
||||
manualAddressError = state.manualAddressError,
|
||||
onManualAddressChanged = { state.eventSink(PaymentFlowEvents.ManualAddressChanged(it)) },
|
||||
)
|
||||
}
|
||||
is RecipientResolutionState.Error -> {
|
||||
Text(resolution.message, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall)
|
||||
Text(
|
||||
resolution.message,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
|
|
@ -255,7 +262,11 @@ private fun TestnetWarningCard(modifier: Modifier = Modifier) {
|
|||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text("⚠️", style = MaterialTheme.typography.titleMedium)
|
||||
Text("Testnet transaction — no real ADA", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onTertiaryContainer)
|
||||
Text(
|
||||
"Testnet transaction — no real ADA",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onTertiaryContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -271,8 +282,16 @@ private fun BalanceInfoCard(balanceAda: String, modifier: Modifier = Modifier) {
|
|||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text("Available balance", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
Text("$balanceAda ADA", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
Text(
|
||||
"Available balance",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
"$balanceAda ADA",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -337,24 +356,88 @@ private fun AddressFoundCard(matrixUserId: String, address: String, modifier: Mo
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Card shown when the Matrix user has no linked Cardano wallet.
|
||||
* Includes a text field for manual address entry.
|
||||
*/
|
||||
@Composable
|
||||
private fun MatrixUserNeedsAddressCard(matrixUserId: String, displayName: String?, modifier: Modifier = Modifier) {
|
||||
private fun ManualAddressEntryCard(
|
||||
matrixUserId: String,
|
||||
displayName: String?,
|
||||
manualAddressInput: String,
|
||||
manualAddressError: String?,
|
||||
onManualAddressChanged: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.tertiaryContainer),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
val name = displayName ?: matrixUserId.substringBefore(":").removePrefix("@")
|
||||
Text("$name hasn't linked a wallet yet", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onErrorContainer)
|
||||
Text("Enter their Cardano address manually above", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onErrorContainer.copy(alpha = 0.7f))
|
||||
Column(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
// Warning header
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text("⚠️", style = MaterialTheme.typography.titleMedium)
|
||||
val name = displayName ?: matrixUserId.substringBefore(":").removePrefix("@")
|
||||
Text(
|
||||
text = "$name hasn't linked a Cardano wallet",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onTertiaryContainer,
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "Enter their Cardano address manually:",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onTertiaryContainer.copy(alpha = 0.8f),
|
||||
)
|
||||
|
||||
// Manual address entry field
|
||||
OutlinedTextField(
|
||||
value = manualAddressInput,
|
||||
onValueChange = onManualAddressChanged,
|
||||
placeholder = { Text("addr1... or addr_test1...") },
|
||||
isError = manualAddressError != null,
|
||||
supportingText = if (manualAddressError != null) {
|
||||
{ Text(manualAddressError, color = MaterialTheme.colorScheme.error) }
|
||||
} else if (manualAddressInput.isNotBlank()) {
|
||||
{
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Check(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(14.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
Text("Valid address", color = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
},
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun PaymentEntryViewPreview(@PreviewParameter(PaymentEntryStateProvider::class) state: PaymentEntryState) {
|
||||
ElementPreview { PaymentEntryView(state = state, onContinue = {}, onCancel = {}, onOpenWalletSettings = {}) }
|
||||
internal fun PaymentEntryViewPreview(
|
||||
@PreviewParameter(PaymentEntryStateProvider::class) state: PaymentEntryState
|
||||
) {
|
||||
ElementPreview {
|
||||
PaymentEntryView(state = state, onContinue = {}, onCancel = {}, onOpenWalletSettings = {})
|
||||
}
|
||||
}
|
||||
|
||||
internal class PaymentEntryStateProvider : PreviewParameterProvider<PaymentEntryState> {
|
||||
|
|
@ -362,27 +445,66 @@ internal class PaymentEntryStateProvider : PreviewParameterProvider<PaymentEntry
|
|||
// Normal state with wallet
|
||||
PaymentEntryState(
|
||||
noWalletSetup = false, isCheckingWallet = false,
|
||||
amountInput = "", recipientInput = "", prefillAmount = null, prefillRecipient = null,
|
||||
parsedAmountLovelace = null, isValidRecipient = false, recipientResolutionState = RecipientResolutionState.NotNeeded,
|
||||
amountInput = "", recipientInput = "", manualAddressInput = "",
|
||||
prefillAmount = null, prefillRecipient = null,
|
||||
parsedAmountLovelace = null, isValidRecipient = false,
|
||||
recipientResolutionState = RecipientResolutionState.NotNeeded,
|
||||
resolvedAddress = null,
|
||||
senderAddress = "addr_test1qp2fg...", senderBalanceAda = "100.5", isTestnet = true,
|
||||
amountError = null, recipientError = null, canContinue = false, eventSink = {},
|
||||
amountError = null, recipientError = null, manualAddressError = null,
|
||||
canContinue = false, eventSink = {},
|
||||
),
|
||||
// Address found from Matrix
|
||||
PaymentEntryState(
|
||||
noWalletSetup = false, isCheckingWallet = false,
|
||||
amountInput = "10", recipientInput = "@alice:matrix.org", prefillAmount = null, prefillRecipient = null,
|
||||
amountInput = "10", recipientInput = "@alice:matrix.org", manualAddressInput = "",
|
||||
prefillAmount = null, prefillRecipient = null,
|
||||
parsedAmountLovelace = 10_000_000L, isValidRecipient = true,
|
||||
recipientResolutionState = RecipientResolutionState.Found("@alice:matrix.org", "addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer"),
|
||||
recipientResolutionState = RecipientResolutionState.Found(
|
||||
"@alice:matrix.org",
|
||||
"addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer"
|
||||
),
|
||||
resolvedAddress = "addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer",
|
||||
senderAddress = "addr_test1qp2fg...", senderBalanceAda = "100.5", isTestnet = true,
|
||||
amountError = null, recipientError = null, canContinue = true, eventSink = {},
|
||||
amountError = null, recipientError = null, manualAddressError = null,
|
||||
canContinue = true, eventSink = {},
|
||||
),
|
||||
// Manual entry needed - empty
|
||||
PaymentEntryState(
|
||||
noWalletSetup = false, isCheckingWallet = false,
|
||||
amountInput = "10", recipientInput = "@bob:matrix.org", manualAddressInput = "",
|
||||
prefillAmount = null, prefillRecipient = null,
|
||||
parsedAmountLovelace = 10_000_000L, isValidRecipient = false,
|
||||
recipientResolutionState = RecipientResolutionState.NeedsManualEntry("@bob:matrix.org", "Bob"),
|
||||
resolvedAddress = null,
|
||||
senderAddress = "addr_test1qp2fg...", senderBalanceAda = "100.5", isTestnet = true,
|
||||
amountError = null, recipientError = null, manualAddressError = null,
|
||||
canContinue = false, eventSink = {},
|
||||
),
|
||||
// Manual entry with valid address
|
||||
PaymentEntryState(
|
||||
noWalletSetup = false, isCheckingWallet = false,
|
||||
amountInput = "10", recipientInput = "@bob:matrix.org",
|
||||
manualAddressInput = "addr_test1qp2fg770ddmqxxduasjsas8rgimrhknmqjn43mj74g7ta2",
|
||||
prefillAmount = null, prefillRecipient = null,
|
||||
parsedAmountLovelace = 10_000_000L, isValidRecipient = true,
|
||||
recipientResolutionState = RecipientResolutionState.NeedsManualEntry("@bob:matrix.org", "Bob"),
|
||||
resolvedAddress = "addr_test1qp2fg770ddmqxxduasjsas8rgimrhknmqjn43mj74g7ta2",
|
||||
senderAddress = "addr_test1qp2fg...", senderBalanceAda = "100.5", isTestnet = true,
|
||||
amountError = null, recipientError = null, manualAddressError = null,
|
||||
canContinue = true, eventSink = {},
|
||||
),
|
||||
// No wallet state
|
||||
PaymentEntryState(
|
||||
noWalletSetup = true, isCheckingWallet = false,
|
||||
amountInput = "", recipientInput = "", prefillAmount = null, prefillRecipient = null,
|
||||
parsedAmountLovelace = null, isValidRecipient = false, recipientResolutionState = RecipientResolutionState.NotNeeded,
|
||||
amountInput = "", recipientInput = "", manualAddressInput = "",
|
||||
prefillAmount = null, prefillRecipient = null,
|
||||
parsedAmountLovelace = null, isValidRecipient = false,
|
||||
recipientResolutionState = RecipientResolutionState.NotNeeded,
|
||||
resolvedAddress = null,
|
||||
senderAddress = null, senderBalanceAda = null, isTestnet = false,
|
||||
amountError = null, recipientError = null, canContinue = false, eventSink = {},
|
||||
amountError = null, recipientError = null, manualAddressError = null,
|
||||
canContinue = false, eventSink = {},
|
||||
),
|
||||
// Loading state
|
||||
PaymentEntryState.Loading,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ sealed interface PaymentFlowEvents {
|
|||
// Entry screen events
|
||||
data class AmountChanged(val amount: String) : PaymentFlowEvents
|
||||
data class RecipientChanged(val recipient: String) : PaymentFlowEvents
|
||||
/** Manual address entry when recipient has no linked wallet. */
|
||||
data class ManualAddressChanged(val address: String) : PaymentFlowEvents
|
||||
data object Continue : PaymentFlowEvents
|
||||
data object Cancel : PaymentFlowEvents
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue