Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
cd3904c
docs(spec): design pro DCA sell extension
vitnehasil Apr 23, 2026
71eb3d5
docs(spec): fix migration version to v20->v21
vitnehasil Apr 23, 2026
ca1bf23
docs(plan): implementacni plan pro DCA sell extension
vitnehasil Apr 23, 2026
61aa6b7
feat(sell): add TransactionSide enum + new fields to TransactionEntit…
vitnehasil Apr 23, 2026
dc5afd4
feat(sell): Faze 1 - data model rozsireni pro sell extension
vitnehasil Apr 23, 2026
db4cdf6
feat(sell): refactor ExchangeApi - OrderStatusResult + limitSell/canc…
vitnehasil Apr 23, 2026
dcf0478
feat(sell): implement CoinmateApi limitSell + cancelOrder + getOrderS…
vitnehasil Apr 23, 2026
222d69b
feat(sell): implement BinanceApi limitSell + cancelOrder + getOrderSt…
vitnehasil Apr 23, 2026
03994d3
feat(sell): extend ResolvePendingTransactionsUseCase for SELL orders …
vitnehasil Apr 23, 2026
12de419
feat(sell): add sell-extension use cases and PlanPnL model (Tasks 13-16)
vitnehasil Apr 23, 2026
7434913
feat(sell): UserPreferences trading + sell polling flags (Task 17)
vitnehasil Apr 23, 2026
d90b077
feat(sell): SellPollingWorker + Scheduler (Task 18)
vitnehasil Apr 23, 2026
ed4a532
feat(sell): poll pending sells on app foreground (Task 19)
vitnehasil Apr 23, 2026
d1287df
feat(sell): Advanced section in Settings (Task 20)
vitnehasil Apr 23, 2026
631869c
feat(sell): sell opt-in on AddPlan (Task 21)
vitnehasil Apr 23, 2026
d67194a
feat(sell): sell section + disable dialog on EditPlan (Task 22)
vitnehasil Apr 23, 2026
f37aa52
feat(sell): plan-detail P&L card + open sells (Task 23)
vitnehasil Apr 23, 2026
5378885
feat(sell): sell wizard step 1 (Task 24)
vitnehasil Apr 23, 2026
a23a207
feat(sell): sell wizard step 2 + submit (Task 25)
vitnehasil Apr 23, 2026
f0e9821
feat(sell): chart BUY/SELL markers stub (Task 26)
vitnehasil Apr 23, 2026
49aa1d8
feat(sell): History filter chips + BUY/SELL icons (Task 27)
vitnehasil Apr 23, 2026
f25ac80
feat(sell): TxDetail SELL section + cancel button (Task 28)
vitnehasil Apr 23, 2026
a38068c
feat(sell): Portfolio realized + net P&L summary (Task 29)
vitnehasil Apr 23, 2026
fbe46e4
feat(sell): Dashboard open sells card (Task 30)
vitnehasil Apr 23, 2026
b662df2
fix(history): preserve side filter when applying bottom-sheet filters
vitnehasil Apr 23, 2026
daecaa7
feat(sell): block plan delete with open orders (Task 31)
vitnehasil Apr 24, 2026
2d2350e
feat(sell): pull-to-refresh on plan-detail (Task 32)
vitnehasil Apr 24, 2026
49acde0
fix(sell): match Room schema in v20->v21 migration
vitnehasil Apr 26, 2026
36a40a8
fix(sell): namespace open-sells card key to avoid LazyColumn collision
vitnehasil Apr 26, 2026
da549bd
refactor(portfolio): rename Portfolio to Pozice (Czech) / Positions (…
vitnehasil Apr 26, 2026
ef0aa58
feat(sell): horizontal lines for open sell orders on plan chart
vitnehasil Apr 26, 2026
12ea024
feat(sell): collapsible open orders section on Pozice plan page
vitnehasil Apr 26, 2026
b519b50
feat(sell): dashed limit-order lines, y-axis range, legend entry
vitnehasil Apr 26, 2026
d9e239a
feat(sell): toggleable limit-order legend on Pozice chart
vitnehasil Apr 26, 2026
a90aeb5
feat(sell): inline open-sells badge in plan cards, unified settings c…
vitnehasil May 1, 2026
048ea1e
feat(sell): localize SellWizardBottomSheet and unify Czech terminology
vitnehasil May 1, 2026
2cbc04b
docs(sell): cost basis calculator + ladder mode design spec
vitnehasil May 9, 2026
ed0c451
docs(sell): implementation plan for cost basis + ladder mode
vitnehasil May 9, 2026
4d1d536
fix(coinmate): validate credentials without probing BTC balance
vitnehasil May 9, 2026
abeb07a
feat(sell): add RemainingInventory model for cost basis algorithm
vitnehasil May 9, 2026
1bac893
feat(sell): CalculatePlanCostBasisUseCase with timestamp-aware cheape…
vitnehasil May 9, 2026
f510a11
feat(sell): add estimatedTakerFeeRate to ExchangeApi for fee math in …
vitnehasil May 9, 2026
1a8f208
feat(sell): LossWarning in ValidateSellOrderUseCase
vitnehasil May 9, 2026
252dec8
feat(sell): SellCalculatorMath pure helper for amount/price/net field
vitnehasil May 9, 2026
1a24903
feat(sell): wire cost basis + 3-field calculator into SellWizardViewM…
vitnehasil May 9, 2026
aed6a38
feat(sell): editable avg buy + net field + rich summary + loss banner
vitnehasil May 9, 2026
a000063
feat(sell): PlaceLadderSellUseCase + LadderGenerator helper
vitnehasil May 9, 2026
589b027
feat(sell): ladder mode wizard UI + submit flow
vitnehasil May 9, 2026
df2fcb9
feat(sell): ladder confirm step shows read-only preview table
vitnehasil May 9, 2026
1bf3453
fix(sell): wizard polish - theme, label, breakeven i18n, Czech %
vitnehasil May 9, 2026
600f2ca
fix(sell): single-line chip labels, drop Breakeven chip
vitnehasil May 9, 2026
a6acca3
fix(sell): full-screen wizard, clickable label, thousand separators
vitnehasil May 9, 2026
ef4b2a9
fix(sell): hide summary when proceeds=0 or in ladder mode
vitnehasil May 9, 2026
55d7908
fix(theme): define container colors for dark color schemes
vitnehasil May 9, 2026
80a80a6
fix(sell): diacritics + per-field validation errors
vitnehasil May 9, 2026
2cf2960
fix(sell): green shade tweak, ladder error placement, IME padding
vitnehasil May 9, 2026
feb8a96
fix(sell): fiat-based min order check via MinOrderSizeRepository
vitnehasil May 9, 2026
ab2de23
fix(sell): preserve ladder range on toggle, show net field in ladder …
vitnehasil May 9, 2026
f6f649e
feat(sell): editable ladder net + amount-pct hint
vitnehasil May 9, 2026
22d4c97
feat(sell): cancellation status, cancel-all button, cs polish
vitnehasil May 9, 2026
e5e9dcf
feat(sell): smooth single<->ladder toggle preserves intent
vitnehasil May 10, 2026
0987ce2
fix(sell): localize ladder preview table headers (Profit/Net)
vitnehasil May 10, 2026
3bcb96f
refactor(sell): audit cleanup - race fix, dead code, dedup
vitnehasil May 10, 2026
3a9a83d
refactor(sell): localize errors via sealed classes, share cost basis,…
vitnehasil May 10, 2026
4b4bc17
refactor(sell): split SellInputStep, derive SellSummary in VM
vitnehasil May 10, 2026
efb2e36
feat(sell): notification on fill + chart trade markers
vitnehasil May 10, 2026
ae01f76
test: bootstrap JVM unit-test harness (Robolectric + in-memory Room)
vitnehasil Jun 5, 2026
02d6a59
fix(history): attribute imported transactions to a connection
vitnehasil Jun 5, 2026
edf5373
feat(history): filter transaction history by plan
vitnehasil Jun 5, 2026
55c3bf9
fix(dca): stop runaway duplicate buy orders after network timeout
vitnehasil Jun 5, 2026
67af1e0
fix(dca): harden buy reconciliation (review findings)
vitnehasil Jun 5, 2026
cff5973
fix(dca): close remaining duplicate-buy vectors (transport retry, for…
vitnehasil Jun 12, 2026
81aa97b
fix(exchange): Kraken market buy - replace removed 'viqc' flag with b…
vitnehasil Jun 12, 2026
19977e2
fix(security): biometric lock - close process-death bypass, re-lock a…
vitnehasil Jun 12, 2026
a34b350
refactor(chart): remove BUY/SELL trade-marker triangles from portfoli…
vitnehasil Jun 12, 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
23 changes: 23 additions & 0 deletions accbot-android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@ android {
buildConfig = true
}

testOptions {
unitTests {
isIncludeAndroidResources = true
isReturnDefaultValues = true
}
}

experimentalProperties["android.experimental.enableScreenshotTest"] = true

packaging {
Expand All @@ -83,12 +90,19 @@ android {

}

// Export Room schemas so future migrations can be tested against real prior versions
// with MigrationTestHelper. (Schemas land in app/schemas/.)
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
}

dependencies {
// Core Android
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("androidx.core:core-ktx:1.17.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.10.0")
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.10.0")
implementation("androidx.lifecycle:lifecycle-process:2.10.0")
implementation("androidx.activity:activity-compose:1.12.3")

// Jetpack Compose
Expand Down Expand Up @@ -148,6 +162,15 @@ dependencies {

// Testing
testImplementation("junit:junit:4.13.2")
// JVM unit tests: Robolectric + in-memory Room + coroutines/WorkManager test helpers
testImplementation("org.robolectric:robolectric:4.14.1")
testImplementation("androidx.test:core-ktx:1.6.1")
testImplementation("androidx.test.ext:junit-ktx:1.2.1")
testImplementation("androidx.room:room-testing:2.8.4")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2")
testImplementation("androidx.work:work-testing:2.11.0")
testImplementation("androidx.arch.core:core-testing:2.2.0")
testImplementation("com.squareup.okhttp3:mockwebserver:5.3.0")
androidTestImplementation("androidx.test.ext:junit:1.2.1")
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.3.0")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,19 @@ import android.util.Log
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
import androidx.hilt.work.HiltWorkerFactory
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.work.Configuration
import com.accbot.dca.data.local.CredentialsStore
import com.accbot.dca.data.local.DcaDatabase
import com.accbot.dca.data.local.UserPreferences
import com.accbot.dca.domain.usecase.ResolvePendingTransactionsUseCase
import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import javax.inject.Inject
Expand All @@ -31,6 +38,16 @@ class AccBotApplication : Application(), Configuration.Provider {
@Inject
lateinit var credentialsStore: CredentialsStore

@Inject
lateinit var resolvePendingTransactionsUseCase: ResolvePendingTransactionsUseCase

/**
* App-scope coroutine scope for work that outlives any single ViewModel or
* Activity (lifecycle-observer callbacks, one-off startup tasks). SupervisorJob
* so a single failure doesn't cancel the whole scope.
*/
private val appScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)

override val workManagerConfiguration: Configuration
get() = Configuration.Builder()
.setWorkerFactory(workerFactory)
Expand Down Expand Up @@ -69,6 +86,22 @@ class AccBotApplication : Application(), Configuration.Provider {
Log.e(TAG, "Failed to run CredentialsStore migration", e)
}
}

// Poll for PENDING sell-order resolutions every time the app comes to the
// foreground. This gives users near-instant updates when they open the app
// after a sell has filled, without waiting for the next SellPollingWorker
// tick. Fire-and-forget; the use case is a no-op when there are no open sells.
ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
appScope.launch {
try {
resolvePendingTransactionsUseCase()
} catch (e: Exception) {
Log.w(TAG, "Polling on app start failed", e)
}
}
}
})
}

companion object {
Expand Down
40 changes: 37 additions & 3 deletions accbot-android/app/src/main/java/com/accbot/dca/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.PowerManager
import android.os.SystemClock
import android.provider.Settings
import androidx.appcompat.app.AppCompatActivity
import androidx.activity.compose.setContent
Expand Down Expand Up @@ -71,9 +72,14 @@ import com.accbot.dca.service.NotificationService
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.lifecycleScope
import javax.inject.Inject

/** How long the app may stay in background before the biometric lock re-engages. */
private const val BIOMETRIC_RELOCK_GRACE_MS = 30_000L

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

Expand Down Expand Up @@ -112,8 +118,35 @@ class MainActivity : AppCompatActivity() {

setContent {
val isSandboxMode = userPreferences.isSandboxMode()
var isUnlocked by rememberSaveable { mutableStateOf(false) }
// Plain remember, NOT rememberSaveable: saved instance state survives process
// death (system kills the app in background, task stays in recents), so a
// saveable flag would restore isUnlocked=true and silently skip the lock.
var isUnlocked by remember { mutableStateOf(false) }
val biometricEnabled = userPreferences.isBiometricLockEnabled()

// Re-lock when the app spends longer than a short grace period in background,
// so a quick app switch doesn't re-prompt but a real walk-away does.
if (biometricEnabled) {
DisposableEffect(Unit) {
var backgroundedAt = 0L
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_STOP ->
backgroundedAt = SystemClock.elapsedRealtime()
Lifecycle.Event.ON_START -> {
if (backgroundedAt != 0L &&
SystemClock.elapsedRealtime() - backgroundedAt > BIOMETRIC_RELOCK_GRACE_MS
) {
isUnlocked = false
}
}
else -> {}
}
}
lifecycle.addObserver(observer)
onDispose { lifecycle.removeObserver(observer) }
}
}
// Theme: collect reactive flow so changes apply immediately
val appTheme by userPreferences.appThemeFlow.collectAsState()
val darkTheme = when (appTheme) {
Expand Down Expand Up @@ -433,7 +466,7 @@ fun AccBotApp(
navController.navigate(Screen.EditPlan.createRoute(planId))
},
onNavigateToHistory = { crypto, fiat ->
navController.navigate(Screen.History.createRoute(crypto, fiat))
navController.navigate(Screen.History.createRoute(crypto, fiat, planId))
},
onNavigateToTransactionDetails = { transactionId ->
navController.navigate(Screen.TransactionDetails.createRoute(transactionId))
Expand Down Expand Up @@ -503,7 +536,8 @@ fun AccBotApp(
route = Screen.History.route,
arguments = listOf(
navArgument("crypto") { type = NavType.StringType; nullable = true; defaultValue = null },
navArgument("fiat") { type = NavType.StringType; nullable = true; defaultValue = null }
navArgument("fiat") { type = NavType.StringType; nullable = true; defaultValue = null },
navArgument("planId") { type = NavType.StringType; nullable = true; defaultValue = null }
)
) {
HistoryScreen(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,9 @@ class BackupDataCollector @Inject constructor(
lastExecutedAt = lastExecutedAt?.toEpochMilli(),
nextExecutionAt = nextExecutionAt?.toEpochMilli(),
targetAmount = targetAmount?.toPlainString(),
connectionId = connectionId
connectionId = connectionId,
allowSells = allowSells,
targetProfitAmount = targetProfitAmount?.toPlainString()
)

private fun TransactionEntity.toBackup() = BackupTransaction(
Expand All @@ -149,7 +151,10 @@ class BackupDataCollector @Inject constructor(
errorMessage = errorMessage,
warningMessage = warningMessage,
executedAt = executedAt.toEpochMilli(),
connectionId = connectionId
connectionId = connectionId,
side = side.name,
limitPrice = limitPrice?.toPlainString(),
requestedCryptoAmount = requestedCryptoAmount?.toPlainString()
)

private fun NotificationEntity.toBackup() = BackupNotification(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,9 @@ class BackupDataRestorer @Inject constructor(
createdAt = Instant.ofEpochMilli(createdAt),
lastExecutedAt = lastExecutedAt?.let { Instant.ofEpochMilli(it) },
nextExecutionAt = effectiveNext,
targetAmount = targetAmount?.let { BigDecimal(it) }
targetAmount = targetAmount?.let { BigDecimal(it) },
allowSells = allowSells,
targetProfitAmount = targetProfitAmount?.let { BigDecimal(it) }
)
}

Expand All @@ -363,7 +365,10 @@ class BackupDataRestorer @Inject constructor(
exchangeOrderId = exchangeOrderId,
errorMessage = errorMessage,
warningMessage = warningMessage,
executedAt = Instant.ofEpochMilli(executedAt)
executedAt = Instant.ofEpochMilli(executedAt),
side = try { com.accbot.dca.domain.model.TransactionSide.valueOf(side) } catch (e: Exception) { com.accbot.dca.domain.model.TransactionSide.BUY },
limitPrice = limitPrice?.let { BigDecimal(it) },
requestedCryptoAmount = requestedCryptoAmount?.let { BigDecimal(it) }
)

private fun BackupWithdrawal.toEntity(remappedPlanId: Long, connectionId: Long?) = WithdrawalEntity(
Expand Down
Loading