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 @@ -82,7 +82,19 @@ enum class Exchange(
supportedCryptos = listOf("BTC", "ETH", "SOL", "ADA"),
minOrderSize = mapOf("EUR" to BigDecimal("1"), "USD" to BigDecimal("1")),
sandboxSupport = SandboxSupport.FULL
)
);

companion object {
/** Binance LOT_SIZE step sizes per crypto (from /api/v3/exchangeInfo). */
val binanceLotStepSize = mapOf(
"BTC" to "0.00001",
"ETH" to "0.0001",
"BNB" to "0.001",
"SOL" to "0.001",
"ADA" to "0.1",
"DOT" to "0.01"
)
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.accbot.dca.data.local.UserPreferences
import com.accbot.dca.domain.model.Exchange
import com.accbot.dca.domain.model.ExchangeCredentials
import com.accbot.dca.exchange.ExchangeApiFactory
import java.net.UnknownHostException
import javax.inject.Inject

/**
Expand All @@ -13,6 +14,7 @@ import javax.inject.Inject
sealed class CredentialValidationResult {
data object Success : CredentialValidationResult()
data class Error(val message: String) : CredentialValidationResult()
data object NetworkError : CredentialValidationResult()
}

/**
Expand Down Expand Up @@ -82,6 +84,10 @@ class ValidateAndSaveCredentialsUseCase @Inject constructor(
} else ""
CredentialValidationResult.Error("Invalid API credentials.$hint")
}
} catch (e: UnknownHostException) {
CredentialValidationResult.NetworkError
} catch (e: java.io.IOException) {
CredentialValidationResult.NetworkError
} catch (e: Exception) {
val isSandbox = userPreferences.isSandboxMode()
val hint = if (isSandbox) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package com.accbot.dca.presentation.credentials

import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.res.stringResource
import com.accbot.dca.R
import com.accbot.dca.data.local.CredentialsStore
import com.accbot.dca.data.local.UserPreferences
import com.accbot.dca.domain.model.Exchange
Expand Down Expand Up @@ -29,10 +33,21 @@ data class CredentialFormState(
val isValidatingCredentials: Boolean = false,
val credentialsValid: Boolean = false,
val credentialsError: String? = null,
@StringRes val credentialsErrorRes: Int = 0,
val isSandboxMode: Boolean = false,
val availableExchanges: List<Exchange> = emptyList(),
val showExperimental: Boolean = false
)
) {
val hasCredentialsError: Boolean
get() = credentialsError != null || credentialsErrorRes != 0
}

/** Resolve the credentials error to a localized string. */
val CredentialFormState.resolvedCredentialsError: String?
@Composable get() = when {
credentialsErrorRes != 0 -> stringResource(credentialsErrorRes)
else -> credentialsError
}

/**
* Shared delegate for credential form state and logic.
Expand Down Expand Up @@ -76,7 +91,7 @@ class CredentialFormDelegate(
apiSecret = credentials?.apiSecret ?: "",
passphrase = credentials?.passphrase ?: "",
clientId = credentials?.clientId ?: "",
credentialsError = null
credentialsError = null, credentialsErrorRes = 0
)
}
}
Expand All @@ -95,25 +110,25 @@ class CredentialFormDelegate(
apiKey = "",
apiSecret = "",
passphrase = "",
credentialsError = null
credentialsError = null, credentialsErrorRes = 0
)
}
}

fun setClientId(value: String) {
_state.update { it.copy(clientId = value, credentialsError = null) }
_state.update { it.copy(clientId = value, credentialsError = null, credentialsErrorRes = 0) }
}

fun setApiKey(value: String) {
_state.update { it.copy(apiKey = value, credentialsError = null) }
_state.update { it.copy(apiKey = value, credentialsError = null, credentialsErrorRes = 0) }
}

fun setApiSecret(value: String) {
_state.update { it.copy(apiSecret = value, credentialsError = null) }
_state.update { it.copy(apiSecret = value, credentialsError = null, credentialsErrorRes = 0) }
}

fun setPassphrase(value: String) {
_state.update { it.copy(passphrase = value, credentialsError = null) }
_state.update { it.copy(passphrase = value, credentialsError = null, credentialsErrorRes = 0) }
}

fun validateAndSaveCredentials(onSuccess: () -> Unit) {
Expand All @@ -123,7 +138,7 @@ class CredentialFormDelegate(
val exchange = state.selectedExchange ?: return

coroutineScope.launch {
_state.update { it.copy(isValidatingCredentials = true, credentialsError = null) }
_state.update { it.copy(isValidatingCredentials = true, credentialsError = null, credentialsErrorRes = 0) }

val result = validateAndSaveCredentialsUseCase.execute(
exchange = exchange,
Expand Down Expand Up @@ -152,10 +167,27 @@ class CredentialFormDelegate(
)
}
}
is CredentialValidationResult.NetworkError -> {
_state.update {
it.copy(
isValidatingCredentials = false,
credentialsErrorRes = R.string.error_no_internet
)
}
}
}
}
}

fun notifyNetworkError() {
_state.update {
it.copy(
isValidatingCredentials = false,
credentialsErrorRes = R.string.error_no_internet
)
}
}

fun setExperimentalExchangesEnabled(enabled: Boolean) {
userPreferences.setExperimentalExchangesEnabled(enabled)
val isSandbox = _state.value.isSandboxMode
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package com.accbot.dca.presentation.plan
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
Expand All @@ -18,6 +20,7 @@ import androidx.compose.ui.unit.dp
import com.accbot.dca.R
import com.accbot.dca.domain.model.DcaFrequency
import com.accbot.dca.domain.model.DcaStrategy
import com.accbot.dca.domain.model.Exchange
import com.accbot.dca.presentation.components.AmountInputWithPresets
import com.accbot.dca.presentation.components.ChipGroup
import com.accbot.dca.presentation.components.FrequencyDropdown
Expand Down Expand Up @@ -52,6 +55,7 @@ fun PlanFormContent(
onWithdrawalAddressChanged: (String) -> Unit,
onTargetAmountChanged: (String) -> Unit,
modifier: Modifier = Modifier,
exchange: Exchange? = null,
showCryptoFiatSelection: Boolean = true,
errorMessage: String? = null
) {
Expand Down Expand Up @@ -93,6 +97,28 @@ fun PlanFormContent(
minOrderSize = state.minOrderSize,
amountBelowMinimum = state.amountBelowMinimum
)
if (exchange == Exchange.BINANCE) {
val stepSize = Exchange.binanceLotStepSize[state.selectedCrypto]
if (stepSize != null) {
Spacer(modifier = Modifier.height(8.dp))
Row(
verticalAlignment = Alignment.Top,
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
Icon(
imageVector = Icons.Default.Info,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(16.dp)
)
Text(
text = stringResource(R.string.binance_lot_size_note, state.selectedCrypto, stepSize),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}

// Frequency selection
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ fun AddPlanScreen(
onWithdrawalEnabledChanged = viewModel.planForm::setWithdrawalEnabled,
onWithdrawalAddressChanged = viewModel.planForm::setWithdrawalAddress,
onTargetAmountChanged = viewModel.planForm::setTargetAmount,
exchange = cred.selectedExchange,
errorMessage = uiState.errorMessage
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ class AddPlanViewModel @Inject constructor(
}
return@launch
}
is CredentialValidationResult.NetworkError -> {
credentialForm.notifyNetworkError()
_localState.update { it.copy(isLoading = false) }
return@launch
}
is CredentialValidationResult.Success -> {
// Credentials validated and saved, continue with plan creation
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import com.accbot.dca.domain.usecase.ApiImportResultState
import com.accbot.dca.presentation.components.AccBotTopAppBar
import com.accbot.dca.presentation.components.ApiImportResultDialog
import com.accbot.dca.presentation.components.CredentialsInputCard
import com.accbot.dca.presentation.credentials.resolvedCredentialsError
import com.accbot.dca.presentation.components.ExchangeSelectionTile
import com.accbot.dca.presentation.components.ExperimentalExchangeDisclaimer
import com.accbot.dca.presentation.components.RequestExchangeTile
Expand Down Expand Up @@ -135,7 +136,7 @@ fun AddExchangeScreen(
apiSecret = uiState.credentialForm.apiSecret,
passphrase = uiState.credentialForm.passphrase,
isValidating = uiState.credentialForm.isValidatingCredentials,
error = uiState.credentialForm.credentialsError,
error = uiState.credentialForm.resolvedCredentialsError,
onClientIdChange = viewModel.credentialForm::setClientId,
onApiKeyChange = viewModel.credentialForm::setApiKey,
onApiSecretChange = viewModel.credentialForm::setApiSecret,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import com.accbot.dca.domain.model.supportsApiImport
import com.accbot.dca.presentation.components.AccBotTopAppBar
import com.accbot.dca.presentation.components.ApiImportResultDialog
import com.accbot.dca.presentation.components.CredentialsInputCard
import com.accbot.dca.presentation.credentials.resolvedCredentialsError
import com.accbot.dca.presentation.components.ExchangeAvatar
import com.accbot.dca.presentation.components.ImportConfigDialog
import com.accbot.dca.presentation.ui.theme.Error
Expand Down Expand Up @@ -251,7 +252,7 @@ fun ExchangeDetailScreen(
onApiKeyChange = viewModel.credentialForm::setApiKey,
onApiSecretChange = viewModel.credentialForm::setApiSecret,
onPassphraseChange = viewModel.credentialForm::setPassphrase,
errorMessage = uiState.credentialForm.credentialsError,
errorMessage = uiState.credentialForm.resolvedCredentialsError,
isValidating = uiState.credentialForm.isValidatingCredentials
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import com.accbot.dca.domain.model.Exchange
import com.accbot.dca.domain.model.isStable
import com.accbot.dca.presentation.components.AccBotTopAppBar
import com.accbot.dca.presentation.components.CredentialsInputCard
import com.accbot.dca.presentation.credentials.resolvedCredentialsError
import com.accbot.dca.presentation.components.ExchangeSelectionGrid
import com.accbot.dca.presentation.components.ExchangeInstructionsCard
import com.accbot.dca.presentation.components.ExperimentalExchangeDisclaimer
Expand Down Expand Up @@ -191,7 +192,7 @@ fun ExchangeSetupScreen(
onApiKeyChange = viewModel.credentialForm::setApiKey,
onApiSecretChange = viewModel.credentialForm::setApiSecret,
onPassphraseChange = viewModel.credentialForm::setPassphrase,
errorMessage = cred.credentialsError,
errorMessage = cred.resolvedCredentialsError,
isValidating = cred.isValidatingCredentials
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ fun FirstPlanScreen(
onWithdrawalEnabledChanged = viewModel.planForm::setWithdrawalEnabled,
onWithdrawalAddressChanged = viewModel.planForm::setWithdrawalAddress,
onTargetAmountChanged = viewModel.planForm::setTargetAmount,
exchange = uiState.credentialForm.selectedExchange,
errorMessage = uiState.error
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ fun EditPlanScreen(
onWithdrawalEnabledChanged = viewModel.planForm::setWithdrawalEnabled,
onWithdrawalAddressChanged = viewModel.planForm::setWithdrawalAddress,
onTargetAmountChanged = viewModel.planForm::setTargetAmount,
exchange = uiState.exchange,
errorMessage = if (uiState.isSaving) uiState.error else null
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.accbot.dca.data.local.DcaPlanDao
import com.accbot.dca.data.local.DcaPlanEntity
import com.accbot.dca.domain.model.DcaFrequency
import com.accbot.dca.domain.model.DcaStrategy
import com.accbot.dca.domain.model.Exchange
import com.accbot.dca.domain.usecase.CalculateMonthlyCostUseCase
import com.accbot.dca.domain.util.CronUtils
import com.accbot.dca.data.local.UserPreferences
Expand All @@ -26,6 +27,7 @@ data class EditPlanUiState(
val planId: Long = 0,
val crypto: String = "",
val fiat: String = "",
val exchange: Exchange? = null,
val exchangeName: String = "",

// Plan form (from delegate)
Expand Down Expand Up @@ -79,6 +81,7 @@ class EditPlanViewModel @Inject constructor(
planId = plan.id,
crypto = plan.crypto,
fiat = plan.fiat,
exchange = plan.exchange,
exchangeName = plan.exchange.displayName,
isLoading = false
)
Expand Down
Loading