This commit is contained in:
callebtc
2025-07-15 17:48:09 +02:00
parent 7fbc0fdb86
commit c15792c3e5
10 changed files with 1258 additions and 773 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
+37
View File
@@ -0,0 +1,37 @@
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "com.bitchat.android",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 3,
"versionName": "0.7",
"outputFile": "app-release.apk"
}
],
"elementType": "File",
"baselineProfiles": [
{
"minApi": 28,
"maxApi": 30,
"baselineProfiles": [
"baselineProfiles/1/app-release.dm"
]
},
{
"minApi": 31,
"maxApi": 2147483647,
"baselineProfiles": [
"baselineProfiles/0/app-release.dm"
]
}
],
"minSdkVersionForDexing": 26
}
@@ -0,0 +1,265 @@
package com.bitchat.android.wallet.viewmodel
import android.util.Log
import androidx.lifecycle.MutableLiveData
import com.bitchat.android.wallet.data.*
import com.bitchat.android.wallet.repository.WalletRepository
import com.bitchat.android.wallet.service.CashuService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import java.util.*
/**
* Manages all lightning-related operations for the wallet
*/
class LightningManager(
private val repository: WalletRepository,
private val cashuService: CashuService,
private val coroutineScope: CoroutineScope
) {
companion object {
private const val TAG = "LightningManager"
}
// Observable data
private val _currentMintQuote = MutableLiveData<MintQuote?>(null)
val currentMintQuote: androidx.lifecycle.LiveData<MintQuote?> = _currentMintQuote
private val _currentMeltQuote = MutableLiveData<MeltQuote?>(null)
val currentMeltQuote: androidx.lifecycle.LiveData<MeltQuote?> = _currentMeltQuote
private val _pendingMintQuotes = MutableLiveData<List<MintQuote>>(emptyList())
val pendingMintQuotes: androidx.lifecycle.LiveData<List<MintQuote>> = _pendingMintQuotes
private val _pendingMeltQuotes = MutableLiveData<List<MeltQuote>>(emptyList())
val pendingMeltQuotes: androidx.lifecycle.LiveData<List<MeltQuote>> = _pendingMeltQuotes
private val _isLoading = MutableLiveData<Boolean>(false)
val isLoading: androidx.lifecycle.LiveData<Boolean> = _isLoading
private val _errorMessage = MutableLiveData<String?>(null)
val errorMessage: androidx.lifecycle.LiveData<String?> = _errorMessage
/**
* Create Lightning mint quote (for receiving)
*/
fun createMintQuote(amount: Long, description: String? = null) {
coroutineScope.launch {
try {
_isLoading.value = true
cashuService.createMintQuote(amount, description).onSuccess { quote ->
_currentMintQuote.value = quote
// Save quote for tracking
repository.saveMintQuote(quote).onSuccess {
loadPendingQuotes()
}.onFailure { error ->
Log.e(TAG, "Failed to save mint quote", error)
_errorMessage.value = "Failed to save invoice: ${error.message}"
}
}.onFailure { error ->
_errorMessage.value = "Failed to create invoice: ${error.message}"
}
} finally {
_isLoading.value = false
}
}
}
/**
* Create Lightning melt quote (for sending)
*/
fun createMeltQuote(invoice: String, onQuoteCreated: () -> Unit = {}) {
coroutineScope.launch {
try {
_isLoading.value = true
cashuService.createMeltQuote(invoice).onSuccess { quote ->
_currentMeltQuote.value = quote
// Save quote for tracking
repository.saveMeltQuote(quote).onSuccess {
loadPendingQuotes()
onQuoteCreated()
}.onFailure { error ->
Log.e(TAG, "Failed to save melt quote", error)
_errorMessage.value = "Failed to save quote: ${error.message}"
}
}.onFailure { error ->
_errorMessage.value = "Failed to process invoice: ${error.message}"
}
} finally {
_isLoading.value = false
}
}
}
/**
* Pay Lightning invoice (melt)
*/
fun payLightningInvoice(
quoteId: String,
onTransactionSaved: () -> Unit,
onBalanceRefresh: () -> Unit,
onPaymentComplete: () -> Unit
) {
coroutineScope.launch {
try {
_isLoading.value = true
cashuService.payInvoice(quoteId).onSuccess { success ->
if (success) {
// Update quote status and add transaction
val quote = _currentMeltQuote.value
if (quote != null) {
val updatedQuote = quote.copy(paid = true, state = MeltQuoteState.PAID)
repository.saveMeltQuote(updatedQuote).onSuccess {
val transaction = WalletTransaction(
id = UUID.randomUUID().toString(),
type = TransactionType.LIGHTNING_SEND,
amount = quote.amount,
unit = quote.unit,
status = TransactionStatus.CONFIRMED,
timestamp = Date(),
description = "Lightning payment sent",
quote = quote.id,
fee = quote.feeReserve
)
repository.saveTransaction(transaction).onSuccess {
onTransactionSaved()
onBalanceRefresh()
onPaymentComplete()
}.onFailure { error ->
Log.e(TAG, "Failed to save transaction", error)
_errorMessage.value = "Failed to save transaction: ${error.message}"
}
}.onFailure { error ->
Log.e(TAG, "Failed to save melt quote", error)
_errorMessage.value = "Failed to save quote: ${error.message}"
}
}
} else {
_errorMessage.value = "Payment failed"
}
}.onFailure { error ->
_errorMessage.value = "Payment failed: ${error.message}"
}
} finally {
_isLoading.value = false
}
}
}
/**
* Check pending quotes for updates
*/
suspend fun checkPendingQuotes(
onTransactionSaved: () -> Unit,
onBalanceRefresh: () -> Unit
) {
// Check mint quotes
repository.getMintQuotes().onSuccess { quotes ->
val unpaidQuotes = quotes.filter { !it.paid }
for (quote in unpaidQuotes) {
cashuService.checkAndMintQuote(quote.id).onSuccess { success ->
if (success) {
// Update quote status and add transaction
val updatedQuote = quote.copy(paid = true, state = MintQuoteState.PAID)
repository.saveMintQuote(updatedQuote).onSuccess {
// Update currentMintQuote if it's the same quote
if (_currentMintQuote.value?.id == quote.id) {
_currentMintQuote.value = updatedQuote
}
val transaction = WalletTransaction(
id = UUID.randomUUID().toString(),
type = TransactionType.LIGHTNING_RECEIVE,
amount = quote.amount,
unit = quote.unit,
status = TransactionStatus.CONFIRMED,
timestamp = Date(),
description = "Lightning payment received",
quote = quote.id
)
repository.saveTransaction(transaction).onSuccess {
onTransactionSaved()
onBalanceRefresh()
Log.d(TAG, "Mint quote ${quote.id} was paid and minted")
}.onFailure { error ->
Log.e(TAG, "Failed to save transaction for mint quote ${quote.id}", error)
}
}.onFailure { error ->
Log.e(TAG, "Failed to save updated mint quote ${quote.id}", error)
}
}
}
}
_pendingMintQuotes.value = unpaidQuotes
}
// Note: Melt quotes typically don't need polling as they're immediately processed
repository.getMeltQuotes().onSuccess { quotes ->
_pendingMeltQuotes.value = quotes.filter { it.state != MeltQuoteState.PAID }
}
}
/**
* Load pending quotes from repository
*/
fun loadPendingQuotes() {
coroutineScope.launch {
repository.getMintQuotes().onSuccess { quotes ->
_pendingMintQuotes.value = quotes.filter { !it.paid }
}
repository.getMeltQuotes().onSuccess { quotes ->
_pendingMeltQuotes.value = quotes.filter { it.state != MeltQuoteState.PAID }
}
}
}
/**
* Set a specific mint quote as current (for reopening from transaction list)
*/
fun setCurrentMintQuote(quoteId: String, onQuoteFound: () -> Unit) {
coroutineScope.launch {
repository.getMintQuotes().onSuccess { quotes ->
val quote = quotes.find { it.id == quoteId }
if (quote != null) {
_currentMintQuote.value = quote
onQuoteFound()
}
}
}
}
/**
* Clear current mint quote
*/
fun clearCurrentMintQuote() {
_currentMintQuote.value = null
}
/**
* Clear current melt quote
*/
fun clearCurrentMeltQuote() {
_currentMeltQuote.value = null
}
/**
* Clear error message
*/
fun clearError() {
_errorMessage.value = null
}
/**
* Get current mint quote
*/
fun getCurrentMintQuote(): MintQuote? = _currentMintQuote.value
/**
* Get current melt quote
*/
fun getCurrentMeltQuote(): MeltQuote? = _currentMeltQuote.value
}
@@ -0,0 +1,289 @@
package com.bitchat.android.wallet.viewmodel
import android.util.Log
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.bitchat.android.wallet.data.Mint
import com.bitchat.android.wallet.repository.WalletRepository
import com.bitchat.android.wallet.service.CashuService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import java.util.*
/**
* Manages all mint-related operations for the wallet
*/
class MintManager(
private val repository: WalletRepository,
private val cashuService: CashuService,
private val coroutineScope: CoroutineScope
) {
companion object {
private const val TAG = "MintManager"
}
// Observable data
private val _mints = MutableLiveData<List<Mint>>(emptyList())
val mints: androidx.lifecycle.LiveData<List<Mint>> = _mints
private val _activeMint = MutableLiveData<String?>(null)
val activeMint: androidx.lifecycle.LiveData<String?> = _activeMint
private val _isLoading = MutableLiveData<Boolean>(false)
val isLoading: androidx.lifecycle.LiveData<Boolean> = _isLoading
private val _errorMessage = MutableLiveData<String?>(null)
val errorMessage: androidx.lifecycle.LiveData<String?> = _errorMessage
/**
* Initialize with default mint if no mints are configured
*/
fun initializeDefaultWallet(onWalletInitialized: () -> Unit) {
coroutineScope.launch {
try {
// Check if we have mints configured
repository.getMints().onSuccess { mintList ->
if (mintList.isEmpty()) {
Log.d(TAG, "No mints found, initializing with default mint")
// Add default mint
addDefaultMint(onWalletInitialized)
} else {
_mints.value = mintList
}
}
// Ensure we have an active mint
repository.getActiveMint().onSuccess { activeMintUrl ->
if (activeMintUrl.isNullOrEmpty()) {
Log.d(TAG, "No active mint, setting default")
// Use default mint URL from CashuService
cashuService.initializeWallet("https://testnut.cashu.space").onSuccess {
onWalletInitialized()
}
} else {
_activeMint.value = activeMintUrl
initializeWalletWithMint(activeMintUrl, onWalletInitialized)
}
}
} catch (e: Exception) {
Log.e(TAG, "Error initializing default wallet", e)
_errorMessage.value = "Failed to initialize wallet: ${e.message}"
}
}
}
/**
* Add default mint for testing
*/
private fun addDefaultMint(onWalletInitialized: () -> Unit) {
coroutineScope.launch {
val defaultMintUrl = "https://mint.minibits.cash/Bitcoin"
val defaultMintNickname = "Minibits"
try {
cashuService.getMintInfo(defaultMintUrl).onSuccess { mintInfo ->
val mint = Mint(
url = defaultMintUrl,
nickname = defaultMintNickname,
info = mintInfo,
keysets = emptyList(),
active = true,
dateAdded = Date()
)
repository.saveMint(mint).onSuccess {
repository.setActiveMint(defaultMintUrl)
_activeMint.value = defaultMintUrl
repository.getMints().onSuccess { mintList ->
_mints.value = mintList
}
// Initialize wallet with this mint
initializeWalletWithMint(defaultMintUrl, onWalletInitialized)
}
}.onFailure { error ->
Log.e(TAG, "Failed to add default mint", error)
// Fallback - still initialize wallet even if mint info fails
cashuService.initializeWallet(defaultMintUrl).onSuccess {
_activeMint.value = defaultMintUrl
onWalletInitialized()
}
}
} catch (e: Exception) {
Log.e(TAG, "Exception adding default mint", e)
_errorMessage.value = "Failed to add default mint: ${e.message}"
}
}
}
/**
* Initialize wallet with specific mint
*/
private suspend fun initializeWalletWithMint(mintUrl: String, onWalletInitialized: () -> Unit) {
try {
_isLoading.value = true
cashuService.initializeWallet(mintUrl).onSuccess {
onWalletInitialized()
Log.d(TAG, "Wallet initialized with mint: $mintUrl")
}.onFailure { error ->
Log.e(TAG, "Failed to initialize wallet with mint: $mintUrl", error)
_errorMessage.value = "Failed to connect to mint: ${error.message}"
}
} finally {
_isLoading.value = false
}
}
/**
* Load mints from repository
*/
fun loadMints() {
coroutineScope.launch {
repository.getMints().onSuccess { mintList ->
_mints.value = mintList
}
repository.getActiveMint().onSuccess { mintUrl ->
_activeMint.value = mintUrl
}
}
}
/**
* Add a new mint
*/
fun addMint(mintUrl: String, nickname: String, onSuccess: () -> Unit = {}) {
coroutineScope.launch {
try {
_isLoading.value = true
cashuService.getMintInfo(mintUrl).onSuccess { mintInfo ->
val mint = Mint(
url = mintUrl,
nickname = nickname.ifEmpty { mintInfo.name },
info = mintInfo,
keysets = emptyList(), // Will be populated by CDK
active = true,
dateAdded = Date()
)
repository.saveMint(mint).onSuccess {
// Reload mints
repository.getMints().onSuccess { mintList ->
_mints.value = mintList
}
// Set as active mint if it's the first one
if (_activeMint.value.isNullOrEmpty()) {
setActiveMint(mintUrl)
}
onSuccess()
}.onFailure { error ->
_errorMessage.value = "Failed to save mint: ${error.message}"
}
}.onFailure { error ->
_errorMessage.value = "Failed to connect to mint: ${error.message}"
}
} finally {
_isLoading.value = false
}
}
}
/**
* Set the active mint
*/
fun setActiveMint(mintUrl: String, onWalletInitialized: () -> Unit = {}) {
coroutineScope.launch {
try {
_isLoading.value = true
repository.setActiveMint(mintUrl).onSuccess {
_activeMint.value = mintUrl
initializeWalletWithMint(mintUrl, onWalletInitialized)
}.onFailure { error ->
_errorMessage.value = "Failed to set active mint: ${error.message}"
}
} finally {
_isLoading.value = false
}
}
}
/**
* Update mint nickname
*/
fun updateMintNickname(mintUrl: String, newNickname: String) {
coroutineScope.launch {
val currentMints = _mints.value ?: emptyList()
val mintToUpdate = currentMints.find { it.url == mintUrl }
if (mintToUpdate != null) {
val updatedMint = mintToUpdate.copy(nickname = newNickname)
repository.saveMint(updatedMint).onSuccess {
// Reload mints
repository.getMints().onSuccess { mintList ->
_mints.value = mintList
}
}.onFailure { error ->
_errorMessage.value = "Failed to update mint nickname: ${error.message}"
}
}
}
}
/**
* Sync all mints - refresh mint information and keysets
*/
fun syncAllMints() {
coroutineScope.launch {
try {
_isLoading.value = true
val currentMints = _mints.value ?: emptyList()
for (mint in currentMints) {
try {
cashuService.getMintInfo(mint.url).onSuccess { mintInfo ->
val updatedMint = mint.copy(
info = mintInfo,
lastSync = Date()
)
repository.saveMint(updatedMint).onSuccess {
// Mint updated successfully
}.onFailure { error ->
Log.e(TAG, "Failed to save updated mint ${mint.url}", error)
}
}
} catch (e: Exception) {
Log.e(TAG, "Error syncing mint ${mint.url}", e)
}
}
// Reload mints list
repository.getMints().onSuccess { mintList ->
_mints.value = mintList
}
} catch (e: Exception) {
Log.e(TAG, "Error syncing mints", e)
_errorMessage.value = "Failed to sync mints: ${e.message}"
} finally {
_isLoading.value = false
}
}
}
/**
* Clear error message
*/
fun clearError() {
_errorMessage.value = null
}
/**
* Get current active mint URL
*/
fun getCurrentActiveMint(): String? = _activeMint.value
/**
* Get current mints list
*/
fun getCurrentMints(): List<Mint> = _mints.value ?: emptyList()
}
@@ -0,0 +1,318 @@
package com.bitchat.android.wallet.viewmodel
import android.util.Log
import androidx.lifecycle.MutableLiveData
import com.bitchat.android.wallet.data.CashuToken
import com.bitchat.android.wallet.data.Mint
import com.bitchat.android.wallet.data.WalletTransaction
import com.bitchat.android.wallet.data.TransactionType
import com.bitchat.android.wallet.data.TransactionStatus
import com.bitchat.android.wallet.repository.WalletRepository
import com.bitchat.android.wallet.service.CashuService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.delay
import java.math.BigDecimal
import java.util.*
/**
* Manages all token-related operations for the wallet
*/
class TokenManager(
private val repository: WalletRepository,
private val cashuService: CashuService,
private val coroutineScope: CoroutineScope
) {
companion object {
private const val TAG = "TokenManager"
}
// Observable data
private val _generatedToken = MutableLiveData<String?>(null)
val generatedToken: androidx.lifecycle.LiveData<String?> = _generatedToken
private val _decodedToken = MutableLiveData<CashuToken?>(null)
val decodedToken: androidx.lifecycle.LiveData<CashuToken?> = _decodedToken
private val _tokenInput = MutableLiveData<String>("")
val tokenInput: androidx.lifecycle.LiveData<String> = _tokenInput
private val _isLoading = MutableLiveData<Boolean>(false)
val isLoading: androidx.lifecycle.LiveData<Boolean> = _isLoading
private val _errorMessage = MutableLiveData<String?>(null)
val errorMessage: androidx.lifecycle.LiveData<String?> = _errorMessage
/**
* Create a Cashu token
*/
fun createCashuToken(
amount: Long,
memo: String? = null,
onTransactionSaved: () -> Unit,
onBalanceRefresh: () -> Unit
) {
coroutineScope.launch {
try {
_isLoading.value = true
cashuService.createToken(amount, memo).onSuccess { token ->
_generatedToken.value = token
// Add transaction
val transaction = WalletTransaction(
id = UUID.randomUUID().toString(),
type = TransactionType.CASHU_SEND,
amount = BigDecimal(amount),
unit = "sat",
status = TransactionStatus.CONFIRMED,
timestamp = Date(),
description = memo ?: "Cashu token sent",
token = token
)
repository.saveTransaction(transaction).onSuccess {
onTransactionSaved()
onBalanceRefresh()
}.onFailure { error ->
Log.e(TAG, "Failed to save transaction", error)
_errorMessage.value = "Failed to save transaction: ${error.message}"
}
}.onFailure { error ->
_errorMessage.value = "Failed to create token: ${error.message}"
}
} finally {
_isLoading.value = false
}
}
}
/**
* Create a Cashu token for payments (with callbacks for /pay command)
*/
fun createCashuTokenForPayment(
amount: Long,
memo: String? = null,
onSuccess: (String) -> Unit,
onError: (String) -> Unit,
onTransactionSaved: () -> Unit,
onBalanceRefresh: () -> Unit
) {
coroutineScope.launch {
try {
cashuService.createToken(amount, memo).onSuccess { token ->
// Add transaction
val transaction = WalletTransaction(
id = UUID.randomUUID().toString(),
type = TransactionType.CASHU_SEND,
amount = BigDecimal(amount),
unit = "sat",
status = TransactionStatus.CONFIRMED,
timestamp = Date(),
description = memo ?: "Cashu token sent",
token = token
)
repository.saveTransaction(transaction).onSuccess {
onTransactionSaved()
onBalanceRefresh()
// Success callback with token
onSuccess(token)
}.onFailure { error ->
Log.e(TAG, "Failed to save transaction", error)
onError("Failed to save transaction: ${error.message}")
}
}.onFailure { error ->
onError("Failed to create token: ${error.message}")
}
} catch (e: Exception) {
Log.e(TAG, "Exception creating payment token", e)
onError(e.message ?: "Unknown error occurred")
}
}
}
/**
* Decode a Cashu token to show information
*/
fun decodeCashuToken(token: String) {
coroutineScope.launch {
try {
_isLoading.value = true
cashuService.decodeToken(token).onSuccess { decodedToken ->
_decodedToken.value = decodedToken
}.onFailure { error ->
_errorMessage.value = "Invalid token: ${error.message}"
}
} finally {
_isLoading.value = false
}
}
}
/**
* Receive a Cashu token
*/
fun receiveCashuToken(
token: String,
currentMints: List<Mint>,
onSuccess: (WalletViewModel.SuccessAnimationData) -> Unit,
onFailure: (WalletViewModel.FailureAnimationData) -> Unit,
onTransactionSaved: () -> Unit,
onBalanceRefresh: () -> Unit,
onMintsUpdated: () -> Unit
) {
coroutineScope.launch {
try {
_isLoading.value = true
// Get decoded token
val decodedToken = _decodedToken.value
if (decodedToken == null) {
val failureData = WalletViewModel.FailureAnimationData(
errorMessage = "Failed to decode token",
operationType = "Token Receive"
)
onFailure(failureData)
clearTokenInput()
return@launch
}
// Use the CashuService.receiveToken with mint management
cashuService.receiveToken(
token = token,
autoAdd = true, // Enable automatic mint addition
currentMints = currentMints,
decodedToken = decodedToken
).onSuccess { amount ->
// Add transaction
val transaction = WalletTransaction(
id = UUID.randomUUID().toString(),
type = TransactionType.CASHU_RECEIVE,
amount = BigDecimal(amount),
unit = "sat",
status = TransactionStatus.CONFIRMED,
timestamp = Date(),
description = "Cashu token received",
token = token
)
repository.saveTransaction(transaction).onSuccess {
onTransactionSaved()
onBalanceRefresh()
// Reload mints in case a new one was added
onMintsUpdated()
// Clear token input immediately
clearTokenInput()
// Show success animation
val animationData = WalletViewModel.SuccessAnimationData(
type = WalletViewModel.SuccessAnimationType.CASHU_RECEIVED,
amount = amount,
unit = "sat",
description = "Cashu token received successfully!"
)
onSuccess(animationData)
}.onFailure { error ->
Log.e(TAG, "Failed to save transaction", error)
// Clear token input
clearTokenInput()
// Show failure animation for transaction save error
val failureData = WalletViewModel.FailureAnimationData(
errorMessage = "Failed to save transaction: ${error.message}",
operationType = "Token Receive"
)
onFailure(failureData)
}
}.onFailure { error ->
Log.e(TAG, "Failed to receive token", error)
// Clear token input
clearTokenInput()
// Show failure animation for token receive error
val failureData = WalletViewModel.FailureAnimationData(
errorMessage = error.message ?: "Unknown error occurred",
operationType = "Token Receive"
)
onFailure(failureData)
}
} catch (e: Exception) {
Log.e(TAG, "Exception in receiveCashuToken", e)
// Clear token input
clearTokenInput()
// Show failure animation for unexpected error
val failureData = WalletViewModel.FailureAnimationData(
errorMessage = e.message ?: "Unexpected error occurred",
operationType = "Token Receive"
)
onFailure(failureData)
} finally {
_isLoading.value = false
}
}
}
/**
* Set token input and trigger decoding if it's a Cashu token
*/
fun setTokenInput(token: String) {
_tokenInput.value = token
if (token.isNotEmpty() && token.startsWith("cashu")) {
decodeCashuToken(token)
}
}
/**
* Clear token input and decoded token
*/
fun clearTokenInput() {
_tokenInput.value = ""
_decodedToken.value = null
}
/**
* Clear generated token
*/
fun clearGeneratedToken() {
_generatedToken.value = null
}
/**
* Set decoded token directly (for external integrations)
*/
fun setDecodedToken(token: CashuToken) {
_decodedToken.value = token
}
/**
* Clear error message
*/
fun clearError() {
_errorMessage.value = null
}
/**
* Get current token input
*/
fun getCurrentTokenInput(): String = _tokenInput.value ?: ""
/**
* Get current decoded token
*/
fun getCurrentDecodedToken(): CashuToken? = _decodedToken.value
/**
* Get current generated token
*/
fun getCurrentGeneratedToken(): String? = _generatedToken.value
}
@@ -0,0 +1,66 @@
package com.bitchat.android.wallet.viewmodel
import androidx.lifecycle.MutableLiveData
import com.bitchat.android.wallet.data.WalletTransaction
import com.bitchat.android.wallet.repository.WalletRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
/**
* Manages all transaction-related operations for the wallet
*/
class TransactionManager(
private val repository: WalletRepository,
private val coroutineScope: CoroutineScope
) {
companion object {
private const val TAG = "TransactionManager"
}
// Observable data
private val _transactions = MutableLiveData<List<WalletTransaction>>(emptyList())
val transactions: androidx.lifecycle.LiveData<List<WalletTransaction>> = _transactions
/**
* Load recent transaction history (last 10)
*/
fun loadTransactions() {
coroutineScope.launch {
repository.getLastTransactions(10).onSuccess { txList ->
_transactions.value = txList
}
}
}
/**
* Get full transaction history (not just last 10)
*/
fun getAllTransactions(): androidx.lifecycle.LiveData<List<WalletTransaction>> {
val allTransactions = MutableLiveData<List<WalletTransaction>>()
coroutineScope.launch {
repository.getAllTransactions().onSuccess { txList ->
allTransactions.value = txList
}
}
return allTransactions
}
/**
* Save a new transaction
*/
fun saveTransaction(transaction: WalletTransaction, onSuccess: () -> Unit, onError: (String) -> Unit) {
coroutineScope.launch {
repository.saveTransaction(transaction).onSuccess {
loadTransactions() // Refresh the transaction list
onSuccess()
}.onFailure { error ->
onError(error.message ?: "Failed to save transaction")
}
}
}
/**
* Get current transactions list
*/
fun getCurrentTransactions(): List<WalletTransaction> = _transactions.value ?: emptyList()
}
@@ -0,0 +1,142 @@
package com.bitchat.android.wallet.viewmodel
import androidx.lifecycle.MutableLiveData
/**
* Manages all UI state for the wallet
*/
class UIStateManager {
// Send/Receive dialog state
private val _showSendDialog = MutableLiveData<Boolean>(false)
val showSendDialog: androidx.lifecycle.LiveData<Boolean> = _showSendDialog
private val _showReceiveDialog = MutableLiveData<Boolean>(false)
val showReceiveDialog: androidx.lifecycle.LiveData<Boolean> = _showReceiveDialog
private val _sendType = MutableLiveData<WalletViewModel.SendType>(WalletViewModel.SendType.CASHU)
val sendType: androidx.lifecycle.LiveData<WalletViewModel.SendType> = _sendType
private val _receiveType = MutableLiveData<WalletViewModel.ReceiveType>(WalletViewModel.ReceiveType.CASHU)
val receiveType: androidx.lifecycle.LiveData<WalletViewModel.ReceiveType> = _receiveType
// Mints tab state
private val _showAddMintDialog = MutableLiveData<Boolean>(false)
val showAddMintDialog: androidx.lifecycle.LiveData<Boolean> = _showAddMintDialog
// Success animation state
private val _showSuccessAnimation = MutableLiveData<Boolean>(false)
val showSuccessAnimation: androidx.lifecycle.LiveData<Boolean> = _showSuccessAnimation
private val _successAnimationData = MutableLiveData<WalletViewModel.SuccessAnimationData?>(null)
val successAnimationData: androidx.lifecycle.LiveData<WalletViewModel.SuccessAnimationData?> = _successAnimationData
// Failure animation state
private val _showFailureAnimation = MutableLiveData<Boolean>(false)
val showFailureAnimation: androidx.lifecycle.LiveData<Boolean> = _showFailureAnimation
private val _failureAnimationData = MutableLiveData<WalletViewModel.FailureAnimationData?>(null)
val failureAnimationData: androidx.lifecycle.LiveData<WalletViewModel.FailureAnimationData?> = _failureAnimationData
// Loading and error state
private val _isLoading = MutableLiveData<Boolean>(false)
val isLoading: androidx.lifecycle.LiveData<Boolean> = _isLoading
private val _errorMessage = MutableLiveData<String?>(null)
val errorMessage: androidx.lifecycle.LiveData<String?> = _errorMessage
// Back navigation handler
private var backHandler: (() -> Boolean)? = null
// Dialog Management
fun showSendDialog() {
_showSendDialog.value = true
}
fun hideSendDialog() {
_showSendDialog.value = false
_sendType.value = WalletViewModel.SendType.CASHU
}
fun showReceiveDialog() {
_showReceiveDialog.value = true
}
fun hideReceiveDialog() {
_showReceiveDialog.value = false
_receiveType.value = WalletViewModel.ReceiveType.CASHU
}
fun setSendType(type: WalletViewModel.SendType) {
_sendType.value = type
}
fun setReceiveType(type: WalletViewModel.ReceiveType) {
_receiveType.value = type
}
fun showAddMintDialog() {
_showAddMintDialog.value = true
}
fun hideAddMintDialog() {
_showAddMintDialog.value = false
}
// Animation Management
fun showSuccessAnimation(animationData: WalletViewModel.SuccessAnimationData) {
_successAnimationData.value = animationData
_showSuccessAnimation.value = true
}
fun hideSuccessAnimation() {
_showSuccessAnimation.value = false
_successAnimationData.value = null
}
fun showFailureAnimation(animationData: WalletViewModel.FailureAnimationData) {
_failureAnimationData.value = animationData
_showFailureAnimation.value = true
}
fun hideFailureAnimation() {
_showFailureAnimation.value = false
_failureAnimationData.value = null
}
// Loading and Error Management
fun setLoading(loading: Boolean) {
_isLoading.value = loading
}
fun clearError() {
_errorMessage.value = null
}
fun setError(message: String) {
_errorMessage.value = message
}
// Back Navigation
fun setBackHandler(handler: () -> Boolean) {
backHandler = handler
}
fun handleBackPress(): Boolean {
return backHandler?.invoke() ?: false
}
// State Getters
fun getCurrentSendType(): WalletViewModel.SendType = _sendType.value ?: WalletViewModel.SendType.CASHU
fun getCurrentReceiveType(): WalletViewModel.ReceiveType = _receiveType.value ?: WalletViewModel.ReceiveType.CASHU
fun isCurrentlyLoading(): Boolean = _isLoading.value ?: false
fun getCurrentError(): String? = _errorMessage.value
}
File diff suppressed because it is too large Load Diff