diff --git a/.claude/settings.local.template.json b/.claude/settings.local.template.json new file mode 100644 index 000000000..bd209fde4 --- /dev/null +++ b/.claude/settings.local.template.json @@ -0,0 +1,26 @@ +{ + "permissions": { + "allow": [ + "Bash", + "Read", + "Edit", + "Write", + "WebFetch", + "mcp__ide__getDiagnostics", + "WebSearch", + "mcp__github__pull_request_read", + "mcp__github__search_pull_requests", + "mcp__github__list_pull_requests", + "mcp__github__get_me", + "mcp__github__get_file_contents" + ], + "deny": [], + "ask": [ + "Bash(rm -rf:*)", + "Bash(git commit:*)", + "Bash(git push:*)" + ], + "additionalDirectories": [ + ] + } +} diff --git a/.cursor/notes/libs.md b/.cursor/notes/libs.md new file mode 100644 index 000000000..8bf348e91 --- /dev/null +++ b/.cursor/notes/libs.md @@ -0,0 +1,162 @@ +# Android Libraries Documentation Reference + +This document provides a comprehensive reference for all libraries used in the bitkit-android project, including their documentation links and key usage patterns. + +## Core Android Libraries + +### Kotlin +- **Documentation**: https://kotlinlang.org/docs/ +- **API Reference**: https://kotlinlang.org/api/latest/jvm/stdlib/ + +### Android Core +- **Core KTX**: https://developer.android.com/kotlin/ktx +- **AppCompat**: https://developer.android.com/jetpack/androidx/releases/appcompat +- **Material Design**: https://material.io/develop/android +- **Core Splashscreen**: https://developer.android.com/develop/ui/views/launch/splash-screen + +## UI Framework + +### Jetpack Compose +- **Main Documentation**: https://developer.android.com/jetpack/compose +- **BOM Mapping**: https://developer.android.com/develop/ui/compose/bom/bom-mapping +- **Material 3**: https://developer.android.com/jetpack/compose/designsystems/material3 +- **Navigation**: https://developer.android.com/jetpack/compose/navigation +- **Tooling**: https://developer.android.com/jetpack/compose/tooling + +### Accompanist +- **Documentation**: https://google.github.io/accompanist/ +- **Permissions**: https://google.github.io/accompanist/permissions/ +- **Pager Indicators**: https://google.github.io/accompanist/pager/ + +### Layout +- **ConstraintLayout Compose**: https://developer.android.com/jetpack/compose/layouts/constraintlayout + +## Architecture & Dependency Injection + +### Hilt +- **Documentation**: https://dagger.dev/hilt/ +- **Android Guide**: https://developer.android.com/training/dependency-injection/hilt-android +- **Compose Integration**: https://developer.android.com/jetpack/compose/libraries#hilt + +### Lifecycle Components +- **Documentation**: https://developer.android.com/jetpack/androidx/releases/lifecycle +- **ViewModel**: https://developer.android.com/topic/libraries/architecture/viewmodel +- **Compose Integration**: https://developer.android.com/jetpack/compose/libraries#viewmodel + +### Data Persistence +- **DataStore Preferences**: https://developer.android.com/topic/libraries/architecture/datastore +- **Room**: https://developer.android.com/training/data-storage/room + +## Networking & Serialization + +### Ktor +- **Documentation**: https://ktor.io/docs/ +- **Client Documentation**: https://ktor.io/docs/getting-started-ktor-client.html +- **Android Guide**: https://ktor.io/docs/client-engines.html#android + +### Serialization +- **Kotlinx Serialization**: https://kotlinlang.org/docs/serialization.html +- **Protobuf**: https://protobuf.dev/ + +## Bitcoin & Lightning Network + +### LDK Node Android +- **GitHub**: https://github.com/synonymdev/ldk-node (Fork) +- **Upstream Docs**: https://lightningdevkit.org/ +- **API Reference**: https://docs.rs/ldk-node/latest/ldk_node/ + +### Bitkit Core +- **GitHub**: https://github.com/synonymdev/bitkit-core +- **Custom Android bindings for Bitcoin operations** + +### Cryptography +- **BouncyCastle**: https://www.bouncycastle.org/java.html +- **Provider Documentation**: https://www.bouncycastle.org/documentation.html + +## Media & Scanning + +### Camera +- **CameraX**: https://developer.android.com/training/camerax +- **Camera2 API**: https://developer.android.com/training/camerax/architecture + +### Barcode/QR Scanning +- **ZXing**: https://github.com/zxing/zxing +- **ML Kit Barcode**: https://developers.google.com/ml-kit/vision/barcode-scanning + +### Animations +- **Lottie Compose**: https://airbnb.io/lottie/#/android-compose + +## Firebase + +### Firebase Platform +- **Documentation**: https://firebase.google.com/docs/android/setup +- **Messaging**: https://firebase.google.com/docs/cloud-messaging/android/client +- **Analytics**: https://firebase.google.com/docs/analytics/get-started?platform=android + +## Security & Authentication + +### Biometric +- **Documentation**: https://developer.android.com/jetpack/androidx/releases/biometric +- **Guide**: https://developer.android.com/training/sign-in/biometric-auth + +## Background Processing + +### WorkManager +- **Documentation**: https://developer.android.com/topic/libraries/architecture/workmanager +- **Hilt Integration**: https://developer.android.com/training/dependency-injection/hilt-jetpack + +## Utilities + +### Date/Time +- **Kotlinx DateTime**: https://github.com/Kotlin/kotlinx-datetime + +### Charts +- **Compose Charts**: https://github.com/ehsannarmani/ComposeCharts + +### Native Libraries +- **JNA**: https://github.com/java-native-access/jna + +## Testing Libraries + +### Unit Testing +- **JUnit**: https://junit.org/junit4/ +- **Mockito Kotlin**: https://github.com/mockito/mockito-kotlin +- **Robolectric**: http://robolectric.org/ +- **Turbine**: https://github.com/cashapp/turbine + +### Android Testing +- **Espresso**: https://developer.android.com/training/testing/espresso +- **Compose Testing**: https://developer.android.com/jetpack/compose/testing +- **Hilt Testing**: https://developer.android.com/training/dependency-injection/hilt-testing + +## Key Configuration Notes + +### Compose Compiler Flags +- StrongSkipping is disabled +- OptimizeNonSkippingGroups is enabled + +### Build Configuration +- Minimum SDK: 28 +- Target SDK: 35 +- Kotlin JVM Target: 11 +- Compose BOM manages all Compose library versions + +## Development Guidelines + +### When Adding New Libraries +1. Check if functionality exists in current libraries first +2. Prefer AndroidX/Jetpack libraries when available +3. Ensure compatibility with current Compose BOM version +4. Add to libs.versions.toml for version management +5. Update this documentation with links and usage notes + +### Version Management +- All versions are centralized in `gradle/libs.versions.toml` +- Use BOM (Bill of Materials) for related library groups +- Keep major dependencies (Compose, Kotlin, Hilt) aligned + +### Testing Strategy +- Unit tests: JUnit + Mockito + Robolectric +- Integration tests: Hilt testing + Room testing +- UI tests: Compose testing + Espresso +- Flow testing: Turbine for StateFlow/Flow testing diff --git a/.cursor/rules/rules.arch.mdc b/.cursor/rules/rules.arch.mdc new file mode 100644 index 000000000..9737c9115 --- /dev/null +++ b/.cursor/rules/rules.arch.mdc @@ -0,0 +1,28 @@ +--- +description: +globs: +alwaysApply: true +--- +### Business Layer Design Principles +- Always prefer creating specialized methods over complex inline logic +- If a feature implementation requires >5 lines of method chaining, extract to a new method +- New feature implementations should read like the business requirement + +### API Design Guidelines +- Create domain-specific methods that match the business language + - Method names should describe WHAT, not HOW + - Hide complexity behind simple, single-purpose APIs + +### Refactoring Triggers +- When implementing new features, always ask: "Does the business layer need a new method to support this cleanly?" +- If you see repeated patterns across different features, extract them into reusable methods +- Prefer failing fast with meaningful defaults over complex null handling in business logic + +### Business Logic Method Design +- Service methods should handle their own error cases and return simple types + - Let callers focus on business logic, not infrastructure concerns + +### Implementation Priority +- Before writing complex feature logic, spend time designing the ideal API first +- Ask: "What would the cleanest possible calling code look like?" then work backwards +- Create the relevant methods needed to achieve that clean calling code diff --git a/.cursor/rules/rules.main.mdc b/.cursor/rules/rules.main.mdc new file mode 100644 index 000000000..fe903e7c2 --- /dev/null +++ b/.cursor/rules/rules.main.mdc @@ -0,0 +1,68 @@ +--- +description: +globs: +alwaysApply: true +--- +## Setup rules: +- you're a senior kotlin android developer with very good knowledge of react native and swift, and you're also an expert in bitcoin and lightning network developer, especially with ldk and ldk-node. +- do not add code comments, except for math, complex logic, docs +- do not remove existing code comments +- when working with libraries, always check `.cursor/notes/libs.md` for documentation links and usage patterns + +## Rules for communication: +- don't be overly enthusiastic in your words, be terse, plain and factual +- do not reply with arguments why the changes are better + +--- + +## Rules for Android code: +- use official kotlin code guide +- always use trailing commas in parameters list, except after modifiers parameter +- when invoking any composable function, always pass the modifier argument last in the call, sse named parameters if needed to ensure this order, and omit trailing commas after it. +- never modify `strings.xml` +- always keep compose preview at end of file +- split screen composables into stateful wrapper parent and stateless child which can be rendered in the previews. +- wrap previews in `AppThemeSurface` composable, and name them simple, like `Preview`, `Preview2`. +- In every ViewModel file, declare the UiState data class immediately AFTER the ViewModel class (never above it) +- prefer to use `_uiState.update {}` for updating ui state flow value +- prefer to add business logic to UI via viewmodels, which can delegate to repositories/service classes +- avoid creating intermediate tuples/triples variables in UI, instead inline logic to the components properties +- do not wire navigation through viewmodel +- do not add logs to viewmodels +- never add any viewmodel as dependency to another viewmodel +- name composable callback parameters with prefix `onClick`, like `onClickSomething` not `onSomethingClick` +- when possible, pass entire `uiState` to the inner Content composable, not individual parameters for each uiState param +- use existing components from `to.bitkit.ui.components` package +- prefer `Dp` unit for new composable parameters +- prefer notation `5000u` and `5000uL` for unsigned integers and long, avoid using `5000U` and `5000UL` +- logging should happen at repository level, viewmodel methods should not re-log if repository already logs the same thing +- prefer list condition check using `in`, i.e. use `myElement in myList` instead of `myList.contains(myElement)` +- for localization use `getString(resId).replace("{param}", hostParam)` +- always trim user input strings, apply the `userInput.trim()` in the viewmodel +- prefer `runCatching` over try/catch +- in compose callbacks, instead of calling `stringResource()` declare `val context = LocalContext.current` and use `context.getString()` +- prefer using `ULong` wherever possible and write ULong values as `1000u` instead of `1000UL` +- prefer to expression body for potential one-line methods + +### Rules for Compose Navigation: +- we use strongly typed navigation in compose, most routes are in `ContentView.kt` file, package `to.bitkit.ui.Routes` +- you should use navigation like: `navController.navigate(Routes.ExternalConnection)` + +## Rules to map RN (react native) to Compose: +- use `docs/screens-map.md` for mapping screens +- map `color = "secondary"` to `color = Colors.White64` +- map colors to `Colors.kt` +- map `Pressable` or `TouchableOpacity` to `Modifier.clickableAlpha { onClick() } ` +- map text components to compose components from `ui/components/Text.kt` +- map settings components to compose components from from `ui/components/settings/*.kt` +- use spacer components from `ui/components/Spacers.kt` +- map `EChannelStatus.open` from react native to `channelDetails.isChannelReady` in kotlin +- map `EChannelStatus.pending` from react native to `!channelDetails.isChannelReady` in kotlin +- map border to `HorizontalDivider()`, without color or thickness params, default color is already White10. + +--- + +## Rules for Android Unit tests and Instrumentation tests: +- run unit tests for specific files like this: `./gradlew :app:testDevDebugUnitTest --tests "to.bitkit.repositories.LightningRepoTest"` +- write unit tests in the same style and using same libraries as: `CurrencyRepoTest`, `LightningRepoTest`, `WalletRepoTest` +- in unit tests, use asserts from `kotlin.test` and mockito-kotlin for mocks diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 55dcdf70c..7cef9571e 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -51,7 +51,8 @@ jobs: Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback. - # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md - # or https://docs.claude.com/en/docs/claude-code/cli-reference for available options - claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"' + Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR. + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://code.claude.com/docs/en/cli-reference for available options + claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"' diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 671e05298..511e692b7 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -13,10 +13,14 @@ on: jobs: claude: if: | - (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || - (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude') && + contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude') && + contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude') && + contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.review.author_association)) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')) && + contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.issue.author_association)) runs-on: ubuntu-latest permissions: contents: write # Allow creating branches/commits @@ -45,6 +49,5 @@ jobs: # Optional: Add claude_args to customize behavior and configuration # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md - # or https://docs.claude.com/en/docs/claude-code/cli-reference for available options + # or https://code.claude.com/docs/en/cli-reference for available options # claude_args: '--allowed-tools Bash(gh pr:*)' - diff --git a/.gitignore b/.gitignore index 43ecfebbd..6da892352 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ *.iml .gradle .idea +!.idea/codeStyles/ .kotlin .DS_Store /build @@ -10,9 +11,8 @@ local.properties # AI .ai -.cursor *.local.* -CLAUDE.md +!*.local.template* # Secrets google-services.json .env diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 000000000..b175032ad --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,174 @@ + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 000000000..79ee123c2 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..96829e391 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,194 @@ +# CLAUDE.md + +This file provides guidance to AI agents like Cursor/Claude Code/Codex/WARP when working with code in this repository. + +## Build Commands + +```sh +# compile +./gradlew compileDevDebugKotlin + +# Build for dev +./gradlew assembleDevDebug + +# Run unit tests +./gradlew testDevDebugUnitTest + +# Run specific unit test file +./gradlew testDevDebugUnitTest --tests LightningRepoTest + +# Run instrumented tests +./gradlew connectedDevDebugAndroidTest + +# Build for E2E tests +E2E=true ./gradlew assembleDevRelease + +# Build for E2E tests with geoblocking disabled +GEO=false E2E=true ./gradlew assembleDevRelease + +# Lint using detekt +./gradlew detekt + +# Auto-format using detekt +./gradlew detekt --auto-correct + +# Update detekt baseline +./gradlew detektBaseline + +# Install dev build +./gradlew installDevDebug + +# Clean build artifacts +./gradlew clean +``` + +## Architecture Overview + +### Tech Stack +- **Language**: Kotlin +- **UI Framework**: Jetpack Compose with Material3 +- **Architecture**: MVVM with Hilt dependency injection +- **Database**: Room +- **Networking**: Ktor +- **Bitcoin/Lightning**: LDK Node, bitkitcore library +- **State Management**: StateFlow, SharedFlow +- **Navigation**: Compose Navigation with strongly typed routes +- **Push Notifications**: Firebase +- **Storage**: DataStore with json files + +### Project Structure +- **app/src/main/java/to/bitkit/** + - **App.kt**: Application class with Hilt setup + - **ui/**: All UI components + - **MainActivity.kt**: Single activity hosting all screens + - **screens/**: Feature-specific screens organized by domain + - **components/**: Reusable UI components + - **theme/**: Material3 theme configuration + - **viewmodels/**: Shared ViewModels for business logic + - **repositories/**: Data access layer + - **services/**: Core services (Lightning, Currency, etc.) + - **data/**: Data layer: database, DTOs, and data stores + - **di/**: Dependency Injection: Hilt modules + - **models/**: Domain models + - **ext/**: Kotlin extensions + - **utils/**: Utility functions + - **usecases/**: Domain layer: use cases + +### Key Architecture Patterns +1. **Single Activity Architecture**: MainActivity hosts all screens via Compose Navigation +2. **Repository Pattern**: Repositories abstract data sources from ViewModels +3. **Service Layer**: Core business logic in services (LightningService, WalletService) +4. **Reactive State Management**: ViewModels expose UI state via StateFlow +5. **Coroutine-based Async**: All async operations use Kotlin coroutines + +### Build Variants +- **dev**: Regtest network for development +- **tnet**: Testnet network +- **mainnet**: Production (currently commented out) + +## Common Pitfalls + +### ❌ DON'T +```kotlin +GlobalScope.launch { } // Use viewModelScope +val result = nullable!!.doSomething() // Use safe calls +Text("Send Payment") // Use string resources +class Service(@Inject val vm: ViewModel) // Never inject VMs +suspend fun getData() = runBlocking { } // Use withContext +``` + +### ✅ DO +```kotlin +viewModelScope.launch { } +val result = nullable?.doSomething() ?: default +Text(stringResource(R.string.send_payment)) +class Service { fun process(data: Data) } +suspend fun getData() = withContext(Dispatchers.IO) { } +``` + +## Key File Paths + +- **Main Activity**: `app/src/main/java/to/bitkit/ui/MainActivity.kt` +- **Navigation**: `app/src/main/java/to/bitkit/ui/ContentView.kt` +- **Lightning Service**: `app/src/main/java/to/bitkit/services/LightningService.kt` +- **App ViewModel**: `app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt` +- **Wallet ViewModel**: `app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt` + +## Common Patterns + +### ViewModel State +```kotlin +private val _uiState = MutableStateFlow(InitialState) +val uiState: StateFlow = _uiState.asStateFlow() + +fun updateState(action: Action) { + viewModelScope.launch { + _uiState.update { it.copy(/* fields */) } + } +} +``` + +### Repository +```kotlin +suspend fun getData(): Result = withContext(Dispatchers.IO) { + runCatching { + Result.success(apiService.fetchData()) + }.onFailure { e -> + Logger.error("Failed", e = e, context = TAG) + } +} +``` + +### Rules +- USE coding rules from `.cursor/default.rules.mdc` +- ALWAYS run `./gradlew compileDevDebugKotlin` after code changes to verify code compiles +- ALWAYS run `./gradlew testDevDebugUnitTest` after code changes to verify tests succeed and fix accordingly +- ALWAYS run `./gradlew detekt` after code changes to check for new lint issues and fix accordingly +- ALWAYS ask clarifying questions to ensure an optimal plan when encountering functional or technical uncertainties in requests +- ALWAYS when fixing lint or test failures prefer to do the minimal amount of changes to fix the issues +- USE single-line commit messages under 50 chars; use template format: `feat: add something new` +- USE `git diff HEAD sourceFilePath` to diff an uncommitted file against the last commit +- ALWAYS check existing code patterns before implementing new features +- USE existing extensions and utilities rather than creating new ones +- ALWAYS consider applying YAGNI (You Aren't Gonna Need It) principle for new code +- ALWAYS reuse existing constants +- ALWAYS ensure a method exist before calling it +- ALWAYS remove unused code after refactors +- ALWAYS follow Material3 design guidelines for UI components +- ALWAYS ensure proper error handling in coroutines +- ALWAYS acknowledge datastore async operations run synchronously in a suspend context +- NEVER use `runBlocking` in suspend functions +- ALWAYS pass the TAG as context to `Logger` calls, e.g. `Logger.debug("message", context = TAG)` +- ALWAYS use the Result API instead of try-catch +- NEVER wrap methods returning `Result` in try-catch +- NEVER inject ViewModels as dependencies - Only android activities and composable functions can use viewmodels +- NEVER hardcode strings and always preserve string resources +- ALWAYS localize in ViewModels using injected `@ApplicationContext`, e.g. `context.getString()` +- ALWAYS use `remember` for expensive Compose computations +- ALWAYS add modifiers to the last place in the argument list when calling `@Composable` functions +- PREFER declaring small dependant classes, constants, interfaces or top-level functions in the same file with the core class where these are used +- ALWAYS create data classes for state AFTER viewModel class in same file +- ALWAYS return early where applicable, PREFER guard-like `if` conditions like `if (condition) return` +- ALWAYS write the documentation for new features as markdown files in `docs/` +- NEVER write code in the documentation files +- NEVER add code comments to private functions, classes, etc +- ALWAYS use `_uiState.update { }`, NEVER use `_stateFlow.value =` +- ALWAYS add the warranted changes in unit tests to keep the unit tests succeeding +- ALWAYS follow the patterns of the existing code in `app/src/test` when writing new unit tests +- ALWAYS be mindful of thread safety when working with mutable lists & state +- ALWAYS split screen composables into parent accepting viewmodel + inner private child accepting state and callbacks `Content()` +- ALWAYS name lambda parameters in a composable function using present tense, NEVER use past tense +- ALWAYS list 3 suggested commit messages after implementation work +- NEVER use `wheneverBlocking` when in an unit test where you're using expression body and already wrapping the test with a `= test {}` lambda. +- ALWAYS add business logic to Repository layer via methods returning `Result` and use it in ViewModels +- ALWAYS use services to wrap RUST code exposed via bindings +- ALWAYS order upstream architectural data flow this way: `UI -> ViewModel -> Repository -> RUST` and vice-versa for downstream +- ALWAYS add new string string resources in alphabetical order in `strings.xml` +- ALWAYS use template in `.github/pull_request_template.md` for PR descriptions +- ALWAYS wrap `ULong` numbers with `USat` in arithmetic operations, to guard against overflows + +### Architecture Guidelines +- Use `LightningNodeService` to manage background notifications while the node is running +- Use `LightningService` to wrap node's RUST APIs and manage the inner lifecycle of the node +- Use `LightningRepo` to defining the business logic for the node operations, usually delegating to `LightningService` +- Use `WakeNodeWorker` to manage the handling of remote notifications received via cloud messages diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 000000000..47dc3e3d8 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/GEMINI.md b/GEMINI.md new file mode 120000 index 000000000..47dc3e3d8 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/README.md b/README.md index 926fdd967..f8e3ad2a0 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,17 @@ Please focus on: - Thread safety in coroutines ``` +#### Local Development Setup (YOLO Mode) + +To enable auto-approved permissions for Claude Code during local development: + +```sh +cp .claude/settings.local.template.json .claude/settings.local.json +``` + +This reduces confirmation prompts for common operations (Bash, Read, Edit, Write, etc.). +Destructive operations like `rm -rf`, `git commit`, and `git push` still require confirmation. + ## License This project is licensed under the MIT License. diff --git a/WARP.md b/WARP.md new file mode 120000 index 000000000..47dc3e3d8 --- /dev/null +++ b/WARP.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1ef22e2c9..4c0960f94 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -2,7 +2,7 @@ import com.android.build.gradle.internal.api.BaseVariantOutputImpl import io.gitlab.arturbosch.detekt.Detekt import org.gradle.api.tasks.testing.logging.TestExceptionFormat import org.gradle.api.tasks.testing.logging.TestLogEvent -import org.jetbrains.kotlin.compose.compiler.gradle.ComposeFeatureFlag +import org.jetbrains.kotlin.gradle.dsl.JvmTarget import java.io.FileInputStream import java.util.Properties @@ -40,11 +40,11 @@ val locales = listOf("en", "ar", "ca", "cs", "de", "el", "es", "fr", "it", "nl", android { namespace = "to.bitkit" - compileSdk = 35 + compileSdk = 36 defaultConfig { applicationId = "to.bitkit" minSdk = 28 - targetSdk = 35 + targetSdk = 36 versionCode = 15 versionName = "0.0.15" testInstrumentationRunner = "to.bitkit.test.HiltTestRunner" @@ -127,8 +127,15 @@ android { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } - kotlinOptions { - jvmTarget = "11" + kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_11 + freeCompilerArgs.addAll( + listOf( + "-XXLanguage:+PropertyParamAnnotationDefaultTargetMode", + ) + ) + } } buildFeatures { buildConfig = true @@ -168,8 +175,6 @@ android { composeCompiler { featureFlags = setOf( - ComposeFeatureFlag.StrongSkipping.disabled(), - ComposeFeatureFlag.OptimizeNonSkippingGroups, ) reportsDestination = layout.buildDirectory.dir("compose_compiler") } @@ -196,7 +201,7 @@ dependencies { implementation(libs.bouncycastle.provider.jdk) implementation(libs.ldk.node.android) { exclude(group = "net.java.dev.jna", module = "jna") } implementation(libs.bitkit.core) - implementation(libs.vss) + implementation(libs.vss.client) // Firebase implementation(platform(libs.firebase.bom)) implementation(libs.firebase.messaging) diff --git a/app/detekt-baseline.xml b/app/detekt-baseline.xml index 56314ea81..b2beb5587 100644 --- a/app/detekt-baseline.xml +++ b/app/detekt-baseline.xml @@ -6,78 +6,14 @@ ComplexCondition:ElectrumConfigViewModel.kt$ElectrumConfigViewModel$currentState.host.isBlank() || port == null || port <= 0 || protocol == null ComplexCondition:MapWebViewClient.kt$MapWebViewClient$it.errorCode == ERROR_HOST_LOOKUP || it.errorCode == ERROR_CONNECT || it.errorCode == ERROR_TIMEOUT || it.errorCode == ERROR_FILE_NOT_FOUND ComplexCondition:ShopWebViewClient.kt$ShopWebViewClient$it.errorCode == ERROR_HOST_LOOKUP || it.errorCode == ERROR_CONNECT || it.errorCode == ERROR_TIMEOUT || it.errorCode == ERROR_FILE_NOT_FOUND - ComposableParamOrder:ActivityDetailScreen.kt$ActivityDetailScreen - ComposableParamOrder:ActivityExploreScreen.kt$ActivityExploreScreen - ComposableParamOrder:ActivityIcon.kt$ActivityIcon - ComposableParamOrder:ActivityIcon.kt$CircularIcon - ComposableParamOrder:AppStatus.kt$AppStatus - ComposableParamOrder:AuthCheckView.kt$AuthCheckView - ComposableParamOrder:AuthCheckView.kt$AuthCheckViewContent - ComposableParamOrder:AuthCheckView.kt$PinPad - ComposableParamOrder:BalanceHeaderView.kt$BalanceHeader - ComposableParamOrder:BlockCard.kt$BlockCard - ComposableParamOrder:BoostTransactionSheet.kt$BoostTransactionSheet - ComposableParamOrder:BoostTransactionSheet.kt$QuantityButton - ComposableParamOrder:CalculatorCard.kt$CalculatorCard - ComposableParamOrder:CalculatorCard.kt$CalculatorCardContent - ComposableParamOrder:CalculatorPreviewScreen.kt$CalculatorPreviewScreen - ComposableParamOrder:FactsCard.kt$FactsCard - ComposableParamOrder:HeadlineCard.kt$HeadlineCard - ComposableParamOrder:HomeScreen.kt$Content - ComposableParamOrder:InfoScreenContent.kt$InfoScreenContent - ComposableParamOrder:Money.kt$MoneyCaptionB - ComposableParamOrder:OnboardingSlidesScreen.kt$OnboardingSlidesScreen - ComposableParamOrder:PriceCard.kt$PriceCard - ComposableParamOrder:ReceiveConfirmScreen.kt$ReceiveConfirmScreen - ComposableParamOrder:ReportIssueScreen.kt$ReportIssueScreen - ComposableParamOrder:RestoreWalletScreen.kt$MnemonicInputField - ComposableParamOrder:SheetHost.kt$SheetHost - ComposableParamOrder:SpendingAmountScreen.kt$SpendingAmountScreen - ComposableParamOrder:SuggestionCard.kt$SuggestionCard - ComposableParamOrder:SwipeToConfirm.kt$SwipeToConfirm - ComposableParamOrder:TextInput.kt$TextInput - ComposableParamOrder:WeatherCard.kt$WeatherCard - ComposableParamOrder:WidgetsSettingsScreen.kt$WidgetsSettingsContent - CompositionLocalAllowlist:Locals.kt$LocalActivityListViewModel - CompositionLocalAllowlist:Locals.kt$LocalAppViewModel - CompositionLocalAllowlist:Locals.kt$LocalBackupsViewModel - CompositionLocalAllowlist:Locals.kt$LocalBalances - CompositionLocalAllowlist:Locals.kt$LocalBlocktankViewModel - CompositionLocalAllowlist:Locals.kt$LocalCurrencies - CompositionLocalAllowlist:Locals.kt$LocalCurrencyViewModel - CompositionLocalAllowlist:Locals.kt$LocalSettingsViewModel - CompositionLocalAllowlist:Locals.kt$LocalTransferViewModel - CompositionLocalAllowlist:Locals.kt$LocalWalletViewModel - ConstructorParameterNaming:AddressChecker.kt$AddressInfo$val chain_stats: AddressStats - ConstructorParameterNaming:AddressChecker.kt$AddressInfo$val mempool_stats: AddressStats - ConstructorParameterNaming:AddressChecker.kt$AddressStats$val funded_txo_count: Int - ConstructorParameterNaming:AddressChecker.kt$AddressStats$val funded_txo_sum: Int - ConstructorParameterNaming:AddressChecker.kt$AddressStats$val spent_txo_count: Int - ConstructorParameterNaming:AddressChecker.kt$AddressStats$val spent_txo_sum: Int - ConstructorParameterNaming:AddressChecker.kt$AddressStats$val tx_count: Int - ConstructorParameterNaming:AddressChecker.kt$TxInput$val is_coinbase: Boolean? = null - ConstructorParameterNaming:AddressChecker.kt$TxInput$val scriptsig_asm: String? = null - ConstructorParameterNaming:AddressChecker.kt$TxOutput$val scriptpubkey_address: String? = null - ConstructorParameterNaming:AddressChecker.kt$TxOutput$val scriptpubkey_asm: String? = null - ConstructorParameterNaming:AddressChecker.kt$TxOutput$val scriptpubkey_type: String? = null - ConstructorParameterNaming:AddressChecker.kt$TxStatus$val block_hash: String? = null - ConstructorParameterNaming:AddressChecker.kt$TxStatus$val block_height: Int? = null - ConstructorParameterNaming:AddressChecker.kt$TxStatus$val block_time: Long? = null - CyclomaticComplexMethod:ActivityDetailScreen.kt$@Composable private fun ActivityDetailContent( item: Activity, tags: List<String>, onRemoveTag: (String) -> Unit, onAddTagClick: () -> Unit, onClickBoost: () -> Unit, onExploreClick: (String) -> Unit, onCopy: (String) -> Unit, ) CyclomaticComplexMethod:ActivityListGrouped.kt$private fun groupActivityItems(activityItems: List<Activity>): List<Any> CyclomaticComplexMethod:ActivityRow.kt$@Composable fun ActivityRow( item: Activity, onClick: (String) -> Unit, testTag: String, ) - CyclomaticComplexMethod:ActivityRow.kt$@Composable private fun TransactionStatusText( txType: PaymentType, isLightning: Boolean, status: PaymentState?, isTransfer: Boolean, ) - CyclomaticComplexMethod:AppStatusScreen.kt$@Composable private fun Content( uiState: AppStatusUiState = AppStatusUiState(), onBack: () -> Unit = {}, onClose: () -> Unit = {}, onInternetClick: () -> Unit = {}, onElectrumClick: () -> Unit = {}, onNodeClick: () -> Unit = {}, onChannelsClick: () -> Unit = {}, onBackupClick: () -> Unit = {}, ) CyclomaticComplexMethod:AppViewModel.kt$AppViewModel$private fun observeSendEvents() CyclomaticComplexMethod:AppViewModel.kt$AppViewModel$private suspend fun handleSanityChecks(amountSats: ULong) CyclomaticComplexMethod:BlocktankRegtestScreen.kt$@Composable fun BlocktankRegtestScreen( navController: NavController, viewModel: BlocktankRegtestViewModel = hiltViewModel(), ) - CyclomaticComplexMethod:ChannelDetailScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable private fun Content( channel: ChannelUi, blocktankOrders: List<IBtOrder> = emptyList(), cjitEntries: List<IcJitEntry> = emptyList(), txDetails: TxDetails? = null, isRefreshing: Boolean = false, onBack: () -> Unit = {}, onClose: () -> Unit = {}, onRefresh: () -> Unit = {}, onCopyText: (String) -> Unit = {}, onOpenUrl: (String) -> Unit = {}, onSupport: (Any) -> Unit = {}, onCloseConnection: () -> Unit = {}, ) CyclomaticComplexMethod:ConfirmMnemonicScreen.kt$@Composable fun ConfirmMnemonicScreen( uiState: BackupContract.UiState, onContinue: () -> Unit, onBack: () -> Unit, ) - CyclomaticComplexMethod:ContentView.kt$@Composable fun ContentView( appViewModel: AppViewModel, walletViewModel: WalletViewModel, blocktankViewModel: BlocktankViewModel, currencyViewModel: CurrencyViewModel, activityListViewModel: ActivityListViewModel, transferViewModel: TransferViewModel, settingsViewModel: SettingsViewModel, backupsViewModel: BackupsViewModel, ) - CyclomaticComplexMethod:CoreService.kt$ActivityService$private suspend fun processOnchainPayment( kind: PaymentKind.Onchain, payment: PaymentDetails, forceUpdate: Boolean, ) CyclomaticComplexMethod:HealthRepo.kt$HealthRepo$private fun collectState() CyclomaticComplexMethod:HomeScreen.kt$@Composable fun HomeScreen( mainUiState: MainUiState, drawerState: DrawerState, rootNavController: NavController, walletNavController: NavHostController, settingsViewModel: SettingsViewModel, walletViewModel: WalletViewModel, appViewModel: AppViewModel, activityListViewModel: ActivityListViewModel, homeViewModel: HomeViewModel = hiltViewModel(), ) - CyclomaticComplexMethod:LightningService.kt$LightningService$private fun logEvent(event: Event) CyclomaticComplexMethod:SendSheet.kt$@Composable fun SendSheet( appViewModel: AppViewModel, walletViewModel: WalletViewModel, startDestination: SendRoute = SendRoute.Recipient, ) CyclomaticComplexMethod:SettingsButtonRow.kt$@Composable fun SettingsButtonRow( title: String, modifier: Modifier = Modifier, subtitle: String? = null, value: SettingsButtonValue = SettingsButtonValue.None, description: String? = null, iconRes: Int? = null, iconTint: Color = Color.Unspecified, iconSize: Dp = 32.dp, maxLinesSubtitle: Int = Int.MAX_VALUE, enabled: Boolean = true, loading: Boolean = false, onClick: () -> Unit, ) CyclomaticComplexMethod:Slider.kt$@Composable fun StepSlider( value: Int, steps: List<Int>, onValueChange: (Int) -> Unit, modifier: Modifier = Modifier, ) @@ -99,93 +35,44 @@ ForbiddenComment:ActivityDetailScreen.kt$/* TODO: Implement assign functionality */ ForbiddenComment:ActivityRow.kt$// TODO: calculate confirmsIn text ForbiddenComment:BoostTransactionViewModel.kt$BoostTransactionUiState$// TODO: Implement dynamic time estimation - ForbiddenComment:ChannelStatusView.kt$// TODO: handle closed channels marking & detection ForbiddenComment:ContentView.kt$// TODO: display as sheet ForbiddenComment:Env.kt$Env$// TODO: remove this to load from BT API instead ForbiddenComment:ExternalNodeViewModel.kt$ExternalNodeViewModel$// TODO: pass customFeeRate to ldk-node when supported ForbiddenComment:LightningConnectionsViewModel.kt$LightningConnectionsViewModel$// TODO: sort channels to get consistent index; node.listChannels returns a list in random order - ForbiddenComment:LightningNodeService.kt$LightningNodeService$// TODO: Get from resources + ForbiddenComment:LightningService.kt$LightningService$// TODO: cleanup sensitive data after implementing a `SecureString` value holder for Keychain return values ForbiddenComment:Notifications.kt$// TODO: review if needed: ForbiddenComment:SuccessScreen.kt$// TODO: verify backup FunctionOnlyReturningConstant:ShopWebViewInterface.kt$ShopWebViewInterface$@JavascriptInterface fun isReady(): Boolean ImplicitDefaultLocale:BlocksService.kt$BlocksService$String.format("%.2f", blockInfo.difficulty / 1_000_000_000_000.0) ImplicitDefaultLocale:PriceService.kt$PriceService$String.format("%.2f", price) InstanceOfCheckForException:LightningService.kt$LightningService$e is NodeException - LambdaParameterEventTrailing:CalculatorCard.kt$onFiatChange - LambdaParameterEventTrailing:QrScanningScreen.kt$onSubmitDebug - LambdaParameterEventTrailing:ReceiveQrScreen.kt$onClickEditInvoice - LambdaParameterEventTrailing:SettingsButtonRow.kt$onClick - LambdaParameterEventTrailing:SuggestionCard.kt$onClick - LambdaParameterEventTrailing:ToastView.kt$onDismiss - LambdaParameterInRestartableEffect:AuthCheckView.kt$validatePin - LambdaParameterInRestartableEffect:BiometricPrompt.kt$onSuccess - LambdaParameterInRestartableEffect:BoostTransactionSheet.kt$onFailure - LambdaParameterInRestartableEffect:BoostTransactionSheet.kt$onMaxFee - LambdaParameterInRestartableEffect:BoostTransactionSheet.kt$onMinFee - LambdaParameterInRestartableEffect:BoostTransactionSheet.kt$onSuccess - LambdaParameterInRestartableEffect:EditInvoiceScreen.kt$navigateReceiveConfirm - LambdaParameterInRestartableEffect:EditInvoiceScreen.kt$onBack - LambdaParameterInRestartableEffect:EditInvoiceScreen.kt$updateInvoice - LambdaParameterInRestartableEffect:InitializingWalletView.kt$onComplete - LambdaParameterInRestartableEffect:LnurlChannelScreen.kt$onConnected - LambdaParameterInRestartableEffect:PinConfirmScreen.kt$onPinConfirmed - LambdaParameterInRestartableEffect:PricePreviewScreen.kt$onClose - LambdaParameterInRestartableEffect:QrCodeImage.kt$onBitmapGenerated - LambdaParameterInRestartableEffect:QrScanningScreen.kt$onBack - LambdaParameterInRestartableEffect:QrScanningScreen.kt$onScanSuccess - LambdaParameterInRestartableEffect:ReportIssueScreen.kt$navigateResultScreen - LambdaParameterInRestartableEffect:SendCoinSelectionScreen.kt$onRender - LambdaParameterInRestartableEffect:SendConfirmScreen.kt$onNavigateToPin - LambdaParameterInRestartableEffect:SendPinCheckScreen.kt$onSuccess - LambdaParameterInRestartableEffect:SendQuickPayScreen.kt$onPaymentComplete - LambdaParameterInRestartableEffect:SendQuickPayScreen.kt$onShowError - LambdaParameterInRestartableEffect:SheetHost.kt$onDismiss - LambdaParameterInRestartableEffect:SpendingAmountScreen.kt$onOrderCreated - LambdaParameterInRestartableEffect:SpendingAmountScreen.kt$toast - LambdaParameterInRestartableEffect:SpendingAmountScreen.kt$toastException LargeClass:AppViewModel.kt$AppViewModel : ViewModel LargeClass:LightningRepo.kt$LightningRepo - LongMethod:AppViewModel.kt$AppViewModel$private fun observeLdkNodeEvents() LongMethod:AppViewModel.kt$AppViewModel$private suspend fun proceedWithPayment() + LongMethod:ContentView.kt$@Suppress("LongParameterList") private fun NavGraphBuilder.home( walletViewModel: WalletViewModel, appViewModel: AppViewModel, activityListViewModel: ActivityListViewModel, settingsViewModel: SettingsViewModel, navController: NavHostController, drawerState: DrawerState, ) LongMethod:ContentView.kt$private fun NavGraphBuilder.widgets( navController: NavHostController, settingsViewModel: SettingsViewModel, currencyViewModel: CurrencyViewModel, ) LongMethod:CoreService.kt$ActivityService$suspend fun generateRandomTestData(count: Int = 100) - LongMethod:LightningService.kt$LightningService$private fun logEvent(event: Event) LongMethod:MainActivity.kt$MainActivity$override fun onCreate(savedInstanceState: Bundle?) LongParameterList:BiometricPrompt.kt$( activity: Context, title: String, cancelButtonText: String, onAuthSucceed: () -> Unit, onAuthFailed: (() -> Unit), onAuthError: ((errorCode: Int, errString: CharSequence) -> Unit), ) LongParameterList:BiometricPrompt.kt$( activity: Context, title: String, cancelButtonText: String, onAuthSucceeded: () -> Unit, onAuthFailed: (() -> Unit), onAuthError: ((errorCode: Int, errString: CharSequence) -> Unit), onUnsupported: () -> Unit, ) LongParameterList:CoreService.kt$ActivityService$( filter: ActivityFilter? = null, txType: PaymentType? = null, tags: List<String>? = null, search: String? = null, minDate: ULong? = null, maxDate: ULong? = null, limit: UInt? = null, sortDirection: SortDirection? = null, ) LongParameterList:CoreService.kt$BlocktankService$( channelSizeSat: ULong, invoiceSat: ULong, invoiceDescription: String, nodeId: String, channelExpiryWeeks: UInt, options: CreateCjitOptions, ) LongParameterList:CoreService.kt$OnchainService$( mnemonicPhrase: String, derivationPathStr: String?, network: Network?, bip39Passphrase: String?, isChange: Boolean?, startIndex: UInt?, count: UInt?, ) - LongParameterList:LightningConnectionsViewModel.kt$LightningConnectionsViewModel$( @ApplicationContext private val context: Context, @BgDispatcher private val bgDispatcher: CoroutineDispatcher, private val lightningRepo: LightningRepo, internal val blocktankRepo: BlocktankRepo, private val logsRepo: LogsRepo, private val addressChecker: AddressChecker, private val ldkNodeEventBus: LdkNodeEventBus, private val walletRepo: WalletRepo, ) - LongParameterList:LightningRepo.kt$LightningRepo$( @BgDispatcher private val bgDispatcher: CoroutineDispatcher, private val lightningService: LightningService, private val ldkNodeEventBus: LdkNodeEventBus, private val settingsStore: SettingsStore, private val coreService: CoreService, private val lspNotificationsService: LspNotificationsService, private val firebaseMessaging: FirebaseMessaging, private val keychain: Keychain, private val lnurlService: LnurlService, private val cacheStore: CacheStore, ) - LongParameterList:Notifications.kt$( title: String?, text: String?, extras: Bundle? = null, bigText: String? = null, id: Int = Random.nextInt(), context: Context, ) LongParameterList:WidgetsRepo.kt$WidgetsRepo$( @BgDispatcher private val bgDispatcher: CoroutineDispatcher, private val newsService: NewsService, private val factsService: FactsService, private val blocksService: BlocksService, private val weatherService: WeatherService, private val priceService: PriceService, private val widgetsStore: WidgetsStore, private val settingsStore: SettingsStore, ) LoopWithTooManyJumpStatements:MonetaryVisualTransformation.kt$MonetaryVisualTransformation.<no name provided>$for - LoopWithTooManyJumpStatements:TransferViewModel.kt$TransferViewModel$while MagicNumber:ActivityDetailScreen.kt$40 MagicNumber:ActivityExploreScreen.kt$40 MagicNumber:ActivityListViewModel.kt$ActivityListViewModel$300 MagicNumber:AddressViewerScreen.kt$1500000L MagicNumber:AddressViewerScreen.kt$250000L - MagicNumber:AddressViewerScreen.kt$50000L MagicNumber:AddressViewerViewModel.kt$AddressViewerViewModel$300 MagicNumber:AppStatus.kt$0.4f - MagicNumber:AppViewModel.kt$AppViewModel$1000 - MagicNumber:AppViewModel.kt$AppViewModel$250 - MagicNumber:AppViewModel.kt$AppViewModel$500 MagicNumber:ArticleModel.kt$24 MagicNumber:ArticleModel.kt$30 MagicNumber:ArticleModel.kt$60 - MagicNumber:AutoReadClipboardHandler.kt$1000 MagicNumber:BackupNavSheetViewModel.kt$BackupNavSheetViewModel$200 - MagicNumber:BackupsViewModel.kt$BackupsViewModel$500 - MagicNumber:BiometricsView.kt$5 - MagicNumber:ChangePinConfirmScreen.kt$500 MagicNumber:ChannelDetailScreen.kt$1.5f - MagicNumber:ChannelOrdersScreen.kt$10 MagicNumber:ConfirmMnemonicScreen.kt$300 - MagicNumber:ContentView.kt$100 - MagicNumber:ContentView.kt$500 MagicNumber:CoreService.kt$ActivityService$64 MagicNumber:Crypto.kt$Crypto$16 MagicNumber:Crypto.kt$Crypto$32 @@ -195,60 +82,33 @@ MagicNumber:ElectrumServer.kt$60001 MagicNumber:ElectrumServer.kt$60002 MagicNumber:HomeScreen.kt$0.8f - MagicNumber:HomeScreen.kt$3 MagicNumber:HttpModule.kt$HttpModule$30_000 MagicNumber:HttpModule.kt$HttpModule$60_000 - MagicNumber:InitializingWalletView.kt$500 MagicNumber:InitializingWalletView.kt$99.9 - MagicNumber:LightningConnectionsViewModel.kt$LightningConnectionsViewModel$500 - MagicNumber:NewsService.kt$NewsService$10 - MagicNumber:OnboardingSlidesScreen.kt$3 - MagicNumber:OnboardingSlidesScreen.kt$4 - MagicNumber:OnboardingSlidesScreen.kt$5 - MagicNumber:PinConfirmScreen.kt$500 MagicNumber:PinPromptScreen.kt$0.8f - MagicNumber:PriceCard.kt$1000 - MagicNumber:PriceCard.kt$3.0 - MagicNumber:PriceCard.kt$4.0 - MagicNumber:PricePreviewScreen.kt$3.0 - MagicNumber:PricePreviewScreen.kt$4.0 MagicNumber:ProgressSteps.kt$12f - MagicNumber:QrScanningScreen.kt$100 - MagicNumber:ReceiveQrScreen.kt$17.33f MagicNumber:ReceiveQrScreen.kt$32 MagicNumber:RestoreWalletScreen.kt$12 MagicNumber:SavingsConfirmScreen.kt$300 - MagicNumber:SavingsProgressScreen.kt$2500 - MagicNumber:SavingsProgressScreen.kt$5000 MagicNumber:SendConfirmScreen.kt$1_234 MagicNumber:SendConfirmScreen.kt$300 MagicNumber:SendConfirmScreen.kt$43 MagicNumber:SendConfirmScreen.kt$654_321 - MagicNumber:SettingUpScreen.kt$5000 - MagicNumber:SettingsViewModel.kt$SettingsViewModel$5000 - MagicNumber:ShareSheet.kt$100 MagicNumber:ShowMnemonicScreen.kt$12 MagicNumber:ShowMnemonicScreen.kt$24 MagicNumber:ShowMnemonicScreen.kt$300 - MagicNumber:Slider.kt$10 MagicNumber:Slider.kt$20 - MagicNumber:Slider.kt$5 - MagicNumber:Slider.kt$50 MagicNumber:SpendingConfirmScreen.kt$300 - MagicNumber:String.kt$3 MagicNumber:SwipeToConfirm.kt$1500 - MagicNumber:SwipeToConfirm.kt$500 MatchingDeclarationName:AddressType.kt$AddressTypeInfo MatchingDeclarationName:Button.kt$ButtonSize MatchingDeclarationName:CoinSelectPreferenceScreen.kt$CoinSelectPreferenceTestTags MatchingDeclarationName:LightningChannel.kt$ChannelStatusUi - MatchingDeclarationName:LightningConnectionsScreen.kt$LightningConnectionsTestTags MatchingDeclarationName:ReceiveConfirmScreen.kt$CjitEntryDetails MatchingDeclarationName:ReportIssueScreen.kt$ReportIssueTestTags MatchingDeclarationName:ResetAndRestoreScreen.kt$ResetAndRestoreTestTags MatchingDeclarationName:SavingsProgressScreen.kt$SavingsProgressState MatchingDeclarationName:SettingsButtonRow.kt$SettingsButtonValue - MaxLineLength:ActivityDetailScreen.kt$description = "Unable to increase the fee any further. Otherwise, it will exceed half the current input balance" MaxLineLength:BlocksEditScreen.kt$enabled = blocksPreferences.run { showBlock || showTime || showDate || showTransactions || showSize || showSource } MaxLineLength:BlocktankRegtestScreen.kt$"Initiating channel close with fundingTxId: $fundingTxId, vout: $vout, forceCloseAfter: $forceCloseAfter" MaxLineLength:BlocktankRepo.kt$BlocktankRepo$"Buying channel with lspBalanceSat: $receivingBalanceSats, channelExpiryWeeks: $channelExpiryWeeks, options: $options" @@ -262,10 +122,6 @@ MaxLineLength:LightningRepo.kt$LightningRepo$"accelerateByCpfp error originalTxId: $originalTxId, satsPerVByte: $satsPerVByte destinationAddress: $destinationAddress" MaxLineLength:LightningRepo.kt$LightningRepo$"accelerateByCpfp success, newDestinationTxId: $newDestinationTxId originalTxId: $originalTxId, satsPerVByte: $satsPerVByte destinationAddress: $destinationAddress" MaxLineLength:LightningRepo.kt$LightningRepo$"bumpFeeByRbf success, replacementTxId: $replacementTxId originalTxId: $originalTxId, satsPerVByte: $satsPerVByte" - MaxLineLength:LightningService.kt$LightningService$"⏳ Channel pending: channelId: $channelId userChannelId: $userChannelId formerTemporaryChannelId: $formerTemporaryChannelId counterpartyNodeId: $counterpartyNodeId fundingTxo: $fundingTxo" - MaxLineLength:LightningService.kt$LightningService$"⛔ Channel closed: channelId: $channelId userChannelId: $userChannelId counterpartyNodeId: $counterpartyNodeId reason: $reason" - MaxLineLength:LightningService.kt$LightningService$"👐 Channel ready: channelId: $channelId userChannelId: $userChannelId counterpartyNodeId: $counterpartyNodeId" - MaxLineLength:LightningService.kt$LightningService$"🫰 Payment claimable: paymentId: $paymentId paymentHash: $paymentHash claimableAmountMsat: $claimableAmountMsat" MaxLineLength:ReceiveLiquidityScreen.kt$if (isAdditional) R.string.wallet__receive_liquidity__label_additional else R.string.wallet__receive_liquidity__label MaxLineLength:ReceiveLiquidityScreen.kt$if (isAdditional) R.string.wallet__receive_liquidity__nav_title_additional else R.string.wallet__receive_liquidity__nav_title MaxLineLength:ReceiveLiquidityScreen.kt$if (isAdditional) R.string.wallet__receive_liquidity__text_additional else R.string.wallet__receive_liquidity__text @@ -273,7 +129,6 @@ MaxLineLength:SendAddressScreen.kt$addressInput = "bitcoin:bc17tq4mtkq86vte7a26e0za560kgflwqsvxznmer5?lightning=LNBC1PQUVNP8KHGPLNF6REGS3VY5F40AJFUN4S2JUDQQNP4TK9MP6LWWLWTC3XX3UUEVYZ4EVQU3X4NQDX348QPP5WJC9DWNTAFN7FZEZFVDC3MHV67SX2LD2MG602E3LEZDMFT29JLWQSP54QKM4G8A2KD5RGEKACA3CH4XV4M2MQDN62F8S2CCRES9QYYSGQCQPCXQRRSSRZJQWQKZS03MNNHSTKR9DN2XQRC8VW5X6CEWAL8C6RW6QQ3T02T3R" MaxLineLength:SettingsScreen.kt$if (newValue) R.string.settings__dev_enabled_message else R.string.settings__dev_disabled_message MaxLineLength:WeatherService.kt$WeatherService$val avgFeeUsd = currencyRepo.convertSatsToFiat(avgFeeSats.toLong(), currency = USD_CURRENCY).getOrNull() ?: return FeeCondition.AVERAGE - MaximumLineLength:ActivityDetailScreen.kt$ MaximumLineLength:BlocksEditScreen.kt$ MaximumLineLength:BlocktankRegtestScreen.kt$ MaximumLineLength:BlocktankRepo.kt$BlocktankRepo$ @@ -283,7 +138,6 @@ MaximumLineLength:LightningBalance.kt$ MaximumLineLength:LightningConnectionsScreen.kt$ MaximumLineLength:LightningRepo.kt$LightningRepo$ - MaximumLineLength:LightningService.kt$LightningService$ MaximumLineLength:ReceiveLiquidityScreen.kt$ MaximumLineLength:SecuritySettingsScreen.kt$ MaximumLineLength:SendAddressScreen.kt$ @@ -293,130 +147,19 @@ MayBeConst:Env.kt$Env.TransactionDefaults$/** * Minimum value in sats for an output. Outputs below the dust limit may not be processed because the fees * required to include them in a block would be greater than the value of the transaction itself. * */ val dustLimit = 546u MayBeConst:Env.kt$Env.TransactionDefaults$/** Total recommended tx base fee in sats */ val recommendedBaseFee = 256u MemberNameEqualsClassName:Keychain.kt$Keychain$private val keychain = context.keychainDataStore - ModifierComposed:AllActivityScreen.kt$swipeToChangeTab - ModifierComposed:Modifiers.kt$clickableAlpha - ModifierComposed:SheetHeight.kt$sheetHeight - ModifierMissing:AboutScreen.kt$AboutScreen - ModifierMissing:ActivityAddTagSheet.kt$ActivityAddTagSheet - ModifierMissing:ActivityDetailScreen.kt$ActivityDetailScreen - ModifierMissing:ActivityListSimple.kt$ActivityListSimple - ModifierMissing:ActivityRow.kt$ActivityRow - ModifierMissing:AddWidgetsScreen.kt$AddWidgetsScreen - ModifierMissing:BackupSheet.kt$BackupSheet - ModifierMissing:BiometricsView.kt$BiometricsView - ModifierMissing:BlocksEditScreen.kt$BlocksEditContent - ModifierMissing:BlocksPreviewScreen.kt$BlocksPreviewContent - ModifierMissing:BlocktankRegtestScreen.kt$BlocktankRegtestScreen - ModifierMissing:BuyIntroScreen.kt$BuyIntroContent - ModifierMissing:CalculatorPreviewScreen.kt$CalculatorPreviewContent - ModifierMissing:CameraPermissionView.kt$DeniedContent - ModifierMissing:ChannelStatusView.kt$ChannelStatusView - ModifierMissing:CreateProfileScreen.kt$CreateProfileScreen - ModifierMissing:CreateWalletWithPassphraseScreen.kt$CreateWalletWithPassphraseScreen - ModifierMissing:DefaultUnitSettingsScreen.kt$DefaultUnitSettingsScreenContent - ModifierMissing:DevSettingsScreen.kt$DevSettingsScreen - ModifierMissing:FactsEditScreen.kt$FactsEditContent - ModifierMissing:FactsPreviewScreen.kt$FactsPreviewContent - ModifierMissing:FundingAdvancedScreen.kt$FundingAdvancedScreen - ModifierMissing:FundingScreen.kt$FundingScreen - ModifierMissing:HeadlinesEditScreen.kt$HeadlinesEditContent - ModifierMissing:HeadlinesPreviewScreen.kt$HeadlinesPreviewContent - ModifierMissing:HomeNav.kt$HomeNav - ModifierMissing:InfoScreenContent.kt$InfoScreenContent - ModifierMissing:InitializingWalletView.kt$InitializingWalletView - ModifierMissing:IntroScreen.kt$IntroScreen - ModifierMissing:LocalCurrencySettingsScreen.kt$LocalCurrencySettingsContent - ModifierMissing:LogsScreen.kt$LogDetailScreen - ModifierMissing:LogsScreen.kt$LogsScreen - ModifierMissing:Money.kt$MoneyDisplay - ModifierMissing:OnboardingSlidesScreen.kt$OnboardingSlidesScreen - ModifierMissing:PinChooseScreen.kt$PinChooseScreen - ModifierMissing:PinSheet.kt$PinSheet - ModifierMissing:PriceEditScreen.kt$PriceEditContent - ModifierMissing:PricePreviewScreen.kt$PricePreviewContent - ModifierMissing:ProfileIntroScreen.kt$ProfileIntroScreen - ModifierMissing:QrScanningScreen.kt$QrScanningScreen - ModifierMissing:QuickPaySettingsScreen.kt$QuickPaySettingsScreenContent - ModifierMissing:ReceiveSheet.kt$ReceiveSheet - ModifierMissing:ReportIssueResultScreen.kt$ReportIssueResultScreen - ModifierMissing:ReportIssueScreen.kt$ReportIssueContent - ModifierMissing:RestoreWalletScreen.kt$MnemonicInputField - ModifierMissing:SavingsAdvancedScreen.kt$ChannelItem - ModifierMissing:SavingsAvailabilityScreen.kt$SavingsAvailabilityScreen - ModifierMissing:SavingsIntroScreen.kt$SavingsIntroScreen - ModifierMissing:SavingsWalletScreen.kt$SavingsWalletScreen - ModifierMissing:SendSheet.kt$SendSheet - ModifierMissing:SettingsScreen.kt$SettingsScreenContent - ModifierMissing:SheetHost.kt$SheetHost - ModifierMissing:ShopDiscoverScreen.kt$ShopDiscoverScreen - ModifierMissing:ShopIntroScreen.kt$ShopIntroScreen - ModifierMissing:ShopWebViewScreen.kt$ShopWebViewScreen - ModifierMissing:Spacers.kt$FillHeight - ModifierMissing:Spacers.kt$FillWidth - ModifierMissing:Spacers.kt$HorizontalSpacer - ModifierMissing:Spacers.kt$VerticalSpacer - ModifierMissing:SpendingIntroScreen.kt$SpendingIntroScreen - ModifierMissing:SpendingWalletScreen.kt$SpendingWalletScreen - ModifierMissing:SplashScreen.kt$SplashScreen - ModifierMissing:TermsOfUseScreen.kt$TermsOfUseScreen - ModifierMissing:ToastView.kt$ToastView - ModifierMissing:TransferIntroScreen.kt$TransferIntroScreen - ModifierMissing:WarningMultipleDevicesScreen.kt$WarningMultipleDevicesScreen - ModifierMissing:WeatherEditScreen.kt$WeatherEditContent - ModifierMissing:WeatherPreviewScreen.kt$WeatherPreviewContent - ModifierMissing:WidgetsIntroScreen.kt$WidgetsIntroScreen - ModifierNotUsedAtRoot:SettingsTextButtonRow.kt$modifier = modifier.then(if (!enabled) Modifier.alpha(0.5f) else Modifier) - ModifierWithoutDefault:ReceiveQrScreen.kt$modifier - ModifierWithoutDefault:SyncNodeView.kt$modifier - ModifierWithoutDefault:WalletBalanceView.kt$modifier - MultipleEmitters:ActivityExploreScreen.kt$LightningDetails - MultipleEmitters:DrawerMenu.kt$DrawerMenu - MultipleEmitters:SendConfirmScreen.kt$LnurlCommentSection - MultipleEmitters:SendConfirmScreen.kt$TagsSection - MutableStateAutoboxing:DragDropColumn.kt$mutableStateOf(0f) - MutableStateParam:ReceiveQrScreen.kt$cjitActive - MutableStateParam:ReceiveQrScreen.kt$cjitInvoice NestedBlockDepth:Context.kt$fun Context.copyAssetToStorage(asset: String, dest: String) NestedBlockDepth:LogsRepo.kt$LogsRepo$private fun createZipBase64(logFiles: List<LogFile>): String NestedBlockDepth:MonetaryVisualTransformation.kt$MonetaryVisualTransformation$private fun createOffsetMapping(original: String, transformed: String): OffsetMapping NestedBlockDepth:ShopWebViewInterface.kt$ShopWebViewInterface$@JavascriptInterface fun postMessage(message: String) + NoUnusedImports:ActivityDetailScreen.kt$to.bitkit.ui.screens.wallets.activity.ActivityDetailScreen.kt NoWildcardImports:LightningChannel.kt$import androidx.compose.foundation.layout.* - ParameterNaming:AddTagScreen.kt$onInputUpdated - ParameterNaming:AddTagScreen.kt$onTagConfirmed - ParameterNaming:AddTagScreen.kt$onTagSelected - ParameterNaming:AddWidgetsScreen.kt$onWidgetSelected - ParameterNaming:AddressViewerScreen.kt$onAddressSelected - ParameterNaming:AddressViewerScreen.kt$onSearchTextChanged - ParameterNaming:BiometricPrompt.kt$onFailed - ParameterNaming:BiometricPrompt.kt$onUnsupported - ParameterNaming:EditInvoiceScreen.kt$onTextChanged - ParameterNaming:ExternalConnectionScreen.kt$onNodeConnected - ParameterNaming:FundingScreen.kt$onAdvanced - ParameterNaming:LnurlChannelScreen.kt$onConnected - ParameterNaming:LocationBlockScreen.kt$onBackPressed - ParameterNaming:PinChooseScreen.kt$onPinChosen - ParameterNaming:PinConfirmScreen.kt$onPinConfirmed - ParameterNaming:QrCodeImage.kt$onBitmapGenerated - ParameterNaming:ReceiveAmountScreen.kt$onCjitCreated - ParameterNaming:SpendingAdvancedScreen.kt$onOrderCreated - ParameterNaming:SpendingAmountScreen.kt$onOrderCreated - ParameterNaming:TransactionSpeedSettingsScreen.kt$onSpeedSelected - PreviewPublic:CameraPermissionView.kt$PreviewDenied - PreviewPublic:CameraPermissionView.kt$PreviewInSheet - PreviewPublic:CameraPermissionView.kt$PreviewRequired - PreviewPublic:EmptyWalletView.kt$EmptyStateViewPreview - PreviewPublic:HighlightLabel.kt$FlexibleLogoPreview - PreviewPublic:HighlightLabel.kt$LongTextLogoPreview - PreviewPublic:SplashScreen.kt$SplashScreenPreview PrintStackTrace:ShareSheet.kt$e ReturnCount:AppViewModel.kt$AppViewModel$private suspend fun handleSanityChecks(amountSats: ULong) ReturnCount:FcmService.kt$FcmService$private fun decryptPayload(response: EncryptedNotification) ReturnCount:LightningConnectionsViewModel.kt$LightningConnectionsViewModel$private fun findUpdatedChannel( currentChannel: ChannelDetails, allChannels: List<ChannelDetails>, ): ChannelDetails? SwallowedException:Crypto.kt$Crypto$e: Exception TooGenericExceptionCaught:ActivityDetailViewModel.kt$ActivityDetailViewModel$e: Exception - TooGenericExceptionCaught:ActivityDetailViewModel.kt$ActivityDetailViewModel$e: Throwable TooGenericExceptionCaught:ActivityRepo.kt$ActivityRepo$e: Exception - TooGenericExceptionCaught:AddressChecker.kt$AddressChecker$e: Exception TooGenericExceptionCaught:AppViewModel.kt$AppViewModel$e: Exception TooGenericExceptionCaught:ArticleModel.kt$e: Exception TooGenericExceptionCaught:BackupNavSheetViewModel.kt$BackupNavSheetViewModel$e: Throwable @@ -426,19 +169,16 @@ TooGenericExceptionCaught:BlocktankRepo.kt$BlocktankRepo$e: Throwable TooGenericExceptionCaught:BoostTransactionViewModel.kt$BoostTransactionViewModel$e: Exception TooGenericExceptionCaught:ChannelOrdersScreen.kt$e: Throwable - TooGenericExceptionCaught:ContentView.kt$e: Throwable TooGenericExceptionCaught:CoreService.kt$ActivityService$e: Exception TooGenericExceptionCaught:CoreService.kt$CoreService$e: Exception TooGenericExceptionCaught:Crypto.kt$Crypto$e: Exception TooGenericExceptionCaught:CurrencyRepo.kt$CurrencyRepo$e: Exception TooGenericExceptionCaught:CurrencyService.kt$CurrencyService$e: Exception TooGenericExceptionCaught:ElectrumConfigViewModel.kt$ElectrumConfigViewModel$e: Exception - TooGenericExceptionCaught:LightningConnectionsViewModel.kt$LightningConnectionsViewModel$e: Exception TooGenericExceptionCaught:LightningRepo.kt$LightningRepo$e: Throwable TooGenericExceptionCaught:LightningService.kt$LightningService$e: Exception TooGenericExceptionCaught:LogsRepo.kt$LogsRepo$e: Exception TooGenericExceptionCaught:LogsViewModel.kt$LogsViewModel$e: Exception - TooGenericExceptionCaught:NewTransactionSheetDetails.kt$NewTransactionSheetDetails.Companion$e: Exception TooGenericExceptionCaught:PriceService.kt$PriceService$e: Exception TooGenericExceptionCaught:QrScanningScreen.kt$e: Exception TooGenericExceptionCaught:SendCoinSelectionViewModel.kt$SendCoinSelectionViewModel$e: Throwable @@ -467,7 +207,6 @@ TooManyFunctions:LightningRepo.kt$LightningRepo TooManyFunctions:LightningService.kt$LightningService : BaseCoroutineScope TooManyFunctions:SettingsViewModel.kt$SettingsViewModel : ViewModel - TooManyFunctions:TagMetadataDao.kt$TagMetadataDao TooManyFunctions:Text.kt$to.bitkit.ui.components.Text.kt TooManyFunctions:TransferViewModel.kt$TransferViewModel : ViewModel TooManyFunctions:WalletRepo.kt$WalletRepo @@ -476,7 +215,6 @@ TooManyFunctions:WidgetsStore.kt$WidgetsStore TopLevelPropertyNaming:DrawerMenu.kt$private const val zIndexMenu = 11f TopLevelPropertyNaming:DrawerMenu.kt$private const val zIndexScrim = 10f - UnusedPrivateProperty:CurrencyRepoTest.kt$CurrencyRepoTest$private val toastEventBus: ToastEventBus = mock() WildcardImport:LightningChannel.kt$import androidx.compose.foundation.layout.* diff --git a/app/src/androidTest/java/to/bitkit/ui/screens/widgets/blocks/BlocksEditScreenTest.kt b/app/src/androidTest/java/to/bitkit/ui/screens/widgets/blocks/BlocksEditScreenTest.kt index e9d6d043e..906a21eba 100644 --- a/app/src/androidTest/java/to/bitkit/ui/screens/widgets/blocks/BlocksEditScreenTest.kt +++ b/app/src/androidTest/java/to/bitkit/ui/screens/widgets/blocks/BlocksEditScreenTest.kt @@ -30,7 +30,6 @@ class BlocksEditScreenTest { @Test fun testBlocksEditScreenWithDefaultPreferences() { // Arrange - var closeClicked = false var backClicked = false var blockClicked = false var timeClicked = false @@ -45,7 +44,6 @@ class BlocksEditScreenTest { composeTestRule.setContent { AppThemeSurface { BlocksEditContent( - onClose = { closeClicked = true }, onBack = { backClicked = true }, onClickShowBlock = { blockClicked = true }, onClickShowTime = { timeClicked = true }, @@ -111,7 +109,6 @@ class BlocksEditScreenTest { composeTestRule.setContent { AppThemeSurface { BlocksEditContent( - onClose = {}, onBack = {}, onClickShowBlock = {}, onClickShowTime = {}, @@ -143,7 +140,6 @@ class BlocksEditScreenTest { composeTestRule.setContent { AppThemeSurface { BlocksEditContent( - onClose = {}, onBack = {}, onClickShowBlock = {}, onClickShowTime = {}, @@ -177,7 +173,6 @@ class BlocksEditScreenTest { composeTestRule.setContent { AppThemeSurface { BlocksEditContent( - onClose = {}, onBack = {}, onClickShowBlock = {}, onClickShowTime = {}, @@ -220,7 +215,6 @@ class BlocksEditScreenTest { composeTestRule.setContent { AppThemeSurface { BlocksEditContent( - onClose = {}, onBack = {}, onClickShowBlock = { blockClicked = true }, onClickShowTime = { timeClicked = true }, @@ -277,7 +271,6 @@ class BlocksEditScreenTest { composeTestRule.setContent { AppThemeSurface { BlocksEditContent( - onClose = {}, onBack = {}, onClickShowBlock = {}, onClickShowTime = {}, @@ -305,7 +298,6 @@ class BlocksEditScreenTest { composeTestRule.setContent { AppThemeSurface { BlocksEditContent( - onClose = {}, onBack = {}, onClickShowBlock = {}, onClickShowTime = {}, diff --git a/app/src/androidTest/java/to/bitkit/ui/screens/widgets/blocks/BlocksPreviewScreenTest.kt b/app/src/androidTest/java/to/bitkit/ui/screens/widgets/blocks/BlocksPreviewScreenTest.kt index 5f2f0aca8..3a5393d50 100644 --- a/app/src/androidTest/java/to/bitkit/ui/screens/widgets/blocks/BlocksPreviewScreenTest.kt +++ b/app/src/androidTest/java/to/bitkit/ui/screens/widgets/blocks/BlocksPreviewScreenTest.kt @@ -28,7 +28,6 @@ class BlocksPreviewContentTest { @Test fun testBlocksPreviewWithEnabledWidget() { // Arrange - var closeClicked = false var backClicked = false var editClicked = false var deleteClicked = false @@ -38,7 +37,6 @@ class BlocksPreviewContentTest { composeTestRule.setContent { AppThemeSurface { BlocksPreviewContent( - onClose = { closeClicked = true }, onBack = { backClicked = true }, onClickEdit = { editClicked = true }, onClickDelete = { deleteClicked = true }, @@ -85,7 +83,6 @@ class BlocksPreviewContentTest { @Test fun testBlocksPreviewWithDisabledWidget() { // Arrange - var closeClicked = false var backClicked = false var editClicked = false var deleteClicked = false @@ -95,7 +92,6 @@ class BlocksPreviewContentTest { composeTestRule.setContent { AppThemeSurface { BlocksPreviewContent( - onClose = { closeClicked = true }, onBack = { backClicked = true }, onClickEdit = { editClicked = true }, onClickDelete = { deleteClicked = true }, @@ -137,7 +133,6 @@ class BlocksPreviewContentTest { composeTestRule.setContent { AppThemeSurface { BlocksPreviewContent( - onClose = {}, onBack = {}, onClickEdit = {}, onClickDelete = {}, @@ -162,7 +157,6 @@ class BlocksPreviewContentTest { composeTestRule.setContent { AppThemeSurface { BlocksPreviewContent( - onClose = {}, onBack = {}, onClickEdit = {}, onClickDelete = {}, @@ -194,13 +188,11 @@ class BlocksPreviewContentTest { @Test fun testNavigationCallbacks() { // Arrange - var closeClicked = false var backClicked = false composeTestRule.setContent { AppThemeSurface { BlocksPreviewContent( - onClose = { closeClicked = true }, onBack = { backClicked = true }, onClickEdit = {}, onClickDelete = {}, @@ -232,7 +224,6 @@ class BlocksPreviewContentTest { composeTestRule.setContent { AppThemeSurface { BlocksPreviewContent( - onClose = {}, onBack = {}, onClickEdit = {}, onClickDelete = {}, @@ -258,7 +249,6 @@ class BlocksPreviewContentTest { composeTestRule.setContent { AppThemeSurface { BlocksPreviewContent( - onClose = {}, onBack = {}, onClickEdit = {}, onClickDelete = {}, @@ -291,7 +281,6 @@ class BlocksPreviewContentTest { composeTestRule.setContent { AppThemeSurface { BlocksPreviewContent( - onClose = {}, onBack = {}, onClickEdit = {}, onClickDelete = {}, @@ -314,7 +303,6 @@ class BlocksPreviewContentTest { composeTestRule.setContent { AppThemeSurface { BlocksPreviewContent( - onClose = {}, onBack = {}, onClickEdit = {}, onClickDelete = {}, diff --git a/app/src/androidTest/java/to/bitkit/ui/screens/widgets/facts/FactsEditScreenTest.kt b/app/src/androidTest/java/to/bitkit/ui/screens/widgets/facts/FactsEditScreenTest.kt index ab799b484..36d1d5ebf 100644 --- a/app/src/androidTest/java/to/bitkit/ui/screens/widgets/facts/FactsEditScreenTest.kt +++ b/app/src/androidTest/java/to/bitkit/ui/screens/widgets/facts/FactsEditScreenTest.kt @@ -21,7 +21,6 @@ class FactsEditContentTest { @Test fun testFactsEditScreenWithDefaultPreferences() { // Arrange - var closeClicked = false var backClicked = false var sourceClicked = false var resetClicked = false @@ -31,7 +30,6 @@ class FactsEditContentTest { composeTestRule.setContent { AppThemeSurface { FactsEditContent( - onClose = { closeClicked = true }, onBack = { backClicked = true }, onClickShowSource = { sourceClicked = true }, onClickReset = { resetClicked = true }, @@ -93,7 +91,6 @@ class FactsEditContentTest { composeTestRule.setContent { AppThemeSurface { FactsEditContent( - onClose = {}, onBack = {}, onClickShowSource = {}, onClickReset = { resetClicked = true }, @@ -118,7 +115,6 @@ class FactsEditContentTest { composeTestRule.setContent { AppThemeSurface { FactsEditContent( - onClose = {}, onBack = {}, onClickShowSource = {}, onClickReset = {}, @@ -139,7 +135,6 @@ class FactsEditContentTest { composeTestRule.setContent { AppThemeSurface { FactsEditContent( - onClose = {}, onBack = {}, onClickShowSource = {}, onClickReset = {}, @@ -178,7 +173,6 @@ class FactsEditContentTest { composeTestRule.setContent { AppThemeSurface { FactsEditContent( - onClose = {}, onBack = {}, onClickShowSource = {}, onClickReset = {}, @@ -200,7 +194,6 @@ class FactsEditContentTest { composeTestRule.setContent { AppThemeSurface { FactsEditContent( - onClose = {}, onBack = {}, onClickShowSource = {}, onClickReset = {}, @@ -217,7 +210,6 @@ class FactsEditContentTest { @Test fun testAllCallbacksTriggered() { // Arrange - var closeClicked = false var backClicked = false var sourceClicked = false var resetClicked = false @@ -228,7 +220,6 @@ class FactsEditContentTest { composeTestRule.setContent { AppThemeSurface { FactsEditContent( - onClose = { closeClicked = true }, onBack = { backClicked = true }, onClickShowSource = { sourceClicked = true }, onClickReset = { resetClicked = true }, diff --git a/app/src/androidTest/java/to/bitkit/ui/screens/widgets/facts/FactsPreviewScreenTest.kt b/app/src/androidTest/java/to/bitkit/ui/screens/widgets/facts/FactsPreviewScreenTest.kt index 960638e33..14c269e49 100644 --- a/app/src/androidTest/java/to/bitkit/ui/screens/widgets/facts/FactsPreviewScreenTest.kt +++ b/app/src/androidTest/java/to/bitkit/ui/screens/widgets/facts/FactsPreviewScreenTest.kt @@ -20,7 +20,6 @@ class FactsPreviewContentTest { @Test fun testFactsPreviewWithEnabledWidget() { // Arrange - var closeClicked = false var backClicked = false var editClicked = false var deleteClicked = false @@ -30,7 +29,6 @@ class FactsPreviewContentTest { composeTestRule.setContent { AppThemeSurface { FactsPreviewContent( - onClose = { closeClicked = true }, onBack = { backClicked = true }, onClickEdit = { editClicked = true }, onClickDelete = { deleteClicked = true }, @@ -77,7 +75,6 @@ class FactsPreviewContentTest { @Test fun testFactsPreviewWithDisabledWidget() { // Arrange - var closeClicked = false var backClicked = false var editClicked = false var deleteClicked = false @@ -87,7 +84,6 @@ class FactsPreviewContentTest { composeTestRule.setContent { AppThemeSurface { FactsPreviewContent( - onClose = { closeClicked = true }, onBack = { backClicked = true }, onClickEdit = { editClicked = true }, onClickDelete = { deleteClicked = true }, @@ -122,7 +118,6 @@ class FactsPreviewContentTest { composeTestRule.setContent { AppThemeSurface { FactsPreviewContent( - onClose = {}, onBack = {}, onClickEdit = {}, onClickDelete = {}, @@ -147,7 +142,6 @@ class FactsPreviewContentTest { composeTestRule.setContent { AppThemeSurface { FactsPreviewContent( - onClose = {}, onBack = {}, onClickEdit = {}, onClickDelete = {}, @@ -179,13 +173,11 @@ class FactsPreviewContentTest { @Test fun testNavigationCallbacks() { // Arrange - var closeClicked = false var backClicked = false composeTestRule.setContent { AppThemeSurface { FactsPreviewContent( - onClose = { closeClicked = true }, onBack = { backClicked = true }, onClickEdit = {}, onClickDelete = {}, @@ -210,7 +202,6 @@ class FactsPreviewContentTest { composeTestRule.setContent { AppThemeSurface { FactsPreviewContent( - onClose = {}, onBack = {}, onClickEdit = {}, onClickDelete = {}, @@ -236,7 +227,6 @@ class FactsPreviewContentTest { composeTestRule.setContent { AppThemeSurface { FactsPreviewContent( - onClose = {}, onBack = {}, onClickEdit = {}, onClickDelete = {}, @@ -262,7 +252,6 @@ class FactsPreviewContentTest { composeTestRule.setContent { AppThemeSurface { FactsPreviewContent( - onClose = {}, onBack = {}, onClickEdit = {}, onClickDelete = {}, diff --git a/app/src/androidTest/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesEditContentTest.kt b/app/src/androidTest/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesEditContentTest.kt index f78d729a8..b3ff9239e 100644 --- a/app/src/androidTest/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesEditContentTest.kt +++ b/app/src/androidTest/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesEditContentTest.kt @@ -31,7 +31,6 @@ class HeadlinesEditContentTest { @Test fun testHeadlinesEditScreenWithDefaultPreferences() { // Arrange - var closeClicked = false var backClicked = false var timeClicked = false var resetClicked = false @@ -42,7 +41,6 @@ class HeadlinesEditContentTest { composeTestRule.setContent { AppThemeSurface { HeadlinesEditContent( - onClose = { closeClicked = true }, onBack = { backClicked = true }, onClickTime = { timeClicked = true }, onClickReset = { resetClicked = true }, @@ -118,7 +116,6 @@ class HeadlinesEditContentTest { composeTestRule.setContent { AppThemeSurface { HeadlinesEditContent( - onClose = {}, onBack = {}, onClickTime = {}, onClickReset = { resetClicked = true }, @@ -150,7 +147,6 @@ class HeadlinesEditContentTest { composeTestRule.setContent { AppThemeSurface { HeadlinesEditContent( - onClose = {}, onBack = {}, onClickTime = {}, onClickReset = {}, @@ -177,7 +173,6 @@ class HeadlinesEditContentTest { composeTestRule.setContent { AppThemeSurface { HeadlinesEditContent( - onClose = {}, onBack = {}, onClickTime = {}, onClickReset = {}, @@ -199,7 +194,6 @@ class HeadlinesEditContentTest { composeTestRule.setContent { AppThemeSurface { HeadlinesEditContent( - onClose = {}, onBack = {}, onClickTime = {}, onClickReset = {}, @@ -246,7 +240,6 @@ class HeadlinesEditContentTest { composeTestRule.setContent { AppThemeSurface { HeadlinesEditContent( - onClose = {}, onBack = {}, onClickTime = {}, onClickReset = {}, @@ -272,7 +265,6 @@ class HeadlinesEditContentTest { composeTestRule.setContent { AppThemeSurface { HeadlinesEditContent( - onClose = {}, onBack = {}, onClickTime = {}, onClickReset = {}, @@ -290,7 +282,6 @@ class HeadlinesEditContentTest { @Test fun testAllCallbacksTriggered() { // Arrange - var closeClicked = false var backClicked = false var timeClicked = false var resetClicked = false @@ -305,7 +296,6 @@ class HeadlinesEditContentTest { composeTestRule.setContent { AppThemeSurface { HeadlinesEditContent( - onClose = { closeClicked = true }, onBack = { backClicked = true }, onClickTime = { timeClicked = true }, onClickReset = { resetClicked = true }, diff --git a/app/src/androidTest/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesPreviewContentTest.kt b/app/src/androidTest/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesPreviewContentTest.kt index e037640b7..a69371fc4 100644 --- a/app/src/androidTest/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesPreviewContentTest.kt +++ b/app/src/androidTest/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesPreviewContentTest.kt @@ -29,7 +29,6 @@ class HeadlinesPreviewContentTest { @Test fun testHeadlinesPreviewWithImplementedHeadlines() { // Arrange - var closeClicked = false var backClicked = false var editClicked = false var deleteClicked = false @@ -39,7 +38,6 @@ class HeadlinesPreviewContentTest { composeTestRule.setContent { AppThemeSurface { HeadlinesPreviewContent( - onClose = { closeClicked = true }, onBack = { backClicked = true }, onClickEdit = { editClicked = true }, onClickDelete = { deleteClicked = true }, @@ -86,7 +84,6 @@ class HeadlinesPreviewContentTest { @Test fun testHeadlinesPreviewWithoutImplementedHeadlines() { // Arrange - var closeClicked = false var backClicked = false var editClicked = false var deleteClicked = false @@ -96,7 +93,6 @@ class HeadlinesPreviewContentTest { composeTestRule.setContent { AppThemeSurface { HeadlinesPreviewContent( - onClose = { closeClicked = true }, onBack = { backClicked = true }, onClickEdit = { editClicked = true }, onClickDelete = { deleteClicked = true }, @@ -134,7 +130,6 @@ class HeadlinesPreviewContentTest { composeTestRule.setContent { AppThemeSurface { HeadlinesPreviewContent( - onClose = {}, onBack = {}, onClickEdit = {}, onClickDelete = {}, @@ -159,7 +154,6 @@ class HeadlinesPreviewContentTest { composeTestRule.setContent { AppThemeSurface { HeadlinesPreviewContent( - onClose = {}, onBack = {}, onClickEdit = {}, onClickDelete = {}, @@ -191,13 +185,11 @@ class HeadlinesPreviewContentTest { @Test fun testNavigationCallbacks() { // Arrange - var closeClicked = false var backClicked = false composeTestRule.setContent { AppThemeSurface { HeadlinesPreviewContent( - onClose = { closeClicked = true }, onBack = { backClicked = true }, onClickEdit = {}, onClickDelete = {}, @@ -223,7 +215,6 @@ class HeadlinesPreviewContentTest { composeTestRule.setContent { AppThemeSurface { HeadlinesPreviewContent( - onClose = {}, onBack = {}, onClickEdit = {}, onClickDelete = {}, diff --git a/app/src/androidTest/java/to/bitkit/ui/screens/widgets/weather/WeatherEditScreenTest.kt b/app/src/androidTest/java/to/bitkit/ui/screens/widgets/weather/WeatherEditScreenTest.kt index df07f2497..b58f05596 100644 --- a/app/src/androidTest/java/to/bitkit/ui/screens/widgets/weather/WeatherEditScreenTest.kt +++ b/app/src/androidTest/java/to/bitkit/ui/screens/widgets/weather/WeatherEditScreenTest.kt @@ -31,7 +31,6 @@ class WeatherEditScreenTest { @Test fun testWeatherEditScreenWithDefaultPreferences() { // Arrange - var closeClicked = false var backClicked = false var titleClicked = false var descriptionClicked = false @@ -44,7 +43,6 @@ class WeatherEditScreenTest { composeTestRule.setContent { AppThemeSurface { WeatherEditContent( - onClose = { closeClicked = true }, onBack = { backClicked = true }, onClickShowTitle = { titleClicked = true }, onClickShowDescription = { descriptionClicked = true }, @@ -106,7 +104,6 @@ class WeatherEditScreenTest { composeTestRule.setContent { AppThemeSurface { WeatherEditContent( - onClose = {}, onBack = {}, onClickShowTitle = {}, onClickShowDescription = {}, @@ -136,7 +133,6 @@ class WeatherEditScreenTest { composeTestRule.setContent { AppThemeSurface { WeatherEditContent( - onClose = {}, onBack = {}, onClickShowTitle = {}, onClickShowDescription = {}, @@ -166,7 +162,6 @@ class WeatherEditScreenTest { composeTestRule.setContent { AppThemeSurface { WeatherEditContent( - onClose = {}, onBack = {}, onClickShowTitle = {}, onClickShowDescription = {}, @@ -203,7 +198,6 @@ class WeatherEditScreenTest { composeTestRule.setContent { AppThemeSurface { WeatherEditContent( - onClose = {}, onBack = {}, onClickShowTitle = { titleClicked = true }, onClickShowDescription = { descriptionClicked = true }, @@ -251,7 +245,6 @@ class WeatherEditScreenTest { composeTestRule.setContent { AppThemeSurface { WeatherEditContent( - onClose = {}, onBack = {}, onClickShowTitle = {}, onClickShowDescription = {}, @@ -277,7 +270,6 @@ class WeatherEditScreenTest { composeTestRule.setContent { AppThemeSurface { WeatherEditContent( - onClose = {}, onBack = {}, onClickShowTitle = {}, onClickShowDescription = {}, @@ -326,7 +318,6 @@ class WeatherEditScreenTest { composeTestRule.setContent { AppThemeSurface { WeatherEditContent( - onClose = {}, onBack = {}, onClickShowTitle = {}, onClickShowDescription = {}, diff --git a/app/src/androidTest/java/to/bitkit/ui/screens/widgets/weather/WeatherPreviewScreenTest.kt b/app/src/androidTest/java/to/bitkit/ui/screens/widgets/weather/WeatherPreviewScreenTest.kt index 8c1de2847..be8dee972 100644 --- a/app/src/androidTest/java/to/bitkit/ui/screens/widgets/weather/WeatherPreviewScreenTest.kt +++ b/app/src/androidTest/java/to/bitkit/ui/screens/widgets/weather/WeatherPreviewScreenTest.kt @@ -30,7 +30,6 @@ class WeatherPreviewContentTest { @Test fun testWeatherPreviewWithEnabledWidget() { // Arrange - var closeClicked = false var backClicked = false var editClicked = false var deleteClicked = false @@ -40,7 +39,6 @@ class WeatherPreviewContentTest { composeTestRule.setContent { AppThemeSurface { WeatherPreviewContent( - onClose = { closeClicked = true }, onBack = { backClicked = true }, onClickEdit = { editClicked = true }, onClickDelete = { deleteClicked = true }, @@ -87,7 +85,6 @@ class WeatherPreviewContentTest { @Test fun testWeatherPreviewWithDisabledWidget() { // Arrange - var closeClicked = false var backClicked = false var editClicked = false var deleteClicked = false @@ -97,7 +94,6 @@ class WeatherPreviewContentTest { composeTestRule.setContent { AppThemeSurface { WeatherPreviewContent( - onClose = { closeClicked = true }, onBack = { backClicked = true }, onClickEdit = { editClicked = true }, onClickDelete = { deleteClicked = true }, @@ -137,7 +133,6 @@ class WeatherPreviewContentTest { composeTestRule.setContent { AppThemeSurface { WeatherPreviewContent( - onClose = {}, onBack = {}, onClickEdit = {}, onClickDelete = {}, @@ -162,7 +157,6 @@ class WeatherPreviewContentTest { composeTestRule.setContent { AppThemeSurface { WeatherPreviewContent( - onClose = {}, onBack = {}, onClickEdit = {}, onClickDelete = {}, @@ -194,13 +188,11 @@ class WeatherPreviewContentTest { @Test fun testNavigationCallbacks() { // Arrange - var closeClicked = false var backClicked = false composeTestRule.setContent { AppThemeSurface { WeatherPreviewContent( - onClose = { closeClicked = true }, onBack = { backClicked = true }, onClickEdit = {}, onClickDelete = {}, @@ -230,7 +222,6 @@ class WeatherPreviewContentTest { composeTestRule.setContent { AppThemeSurface { WeatherPreviewContent( - onClose = {}, onBack = {}, onClickEdit = {}, onClickDelete = {}, @@ -256,7 +247,6 @@ class WeatherPreviewContentTest { composeTestRule.setContent { AppThemeSurface { WeatherPreviewContent( - onClose = {}, onBack = {}, onClickEdit = {}, onClickDelete = {}, @@ -287,7 +277,6 @@ class WeatherPreviewContentTest { composeTestRule.setContent { AppThemeSurface { WeatherPreviewContent( - onClose = {}, onBack = {}, onClickEdit = {}, onClickDelete = {}, @@ -310,7 +299,6 @@ class WeatherPreviewContentTest { composeTestRule.setContent { AppThemeSurface { WeatherPreviewContent( - onClose = {}, onBack = {}, onClickEdit = {}, onClickDelete = {}, @@ -333,7 +321,6 @@ class WeatherPreviewContentTest { composeTestRule.setContent { AppThemeSurface { WeatherPreviewContent( - onClose = {}, onBack = {}, onClickEdit = {}, onClickDelete = {}, diff --git a/app/src/androidTest/java/to/bitkit/ui/settings/support/ReportIssueScreenTest.kt b/app/src/androidTest/java/to/bitkit/ui/settings/support/ReportIssueScreenTest.kt index 845e0ed55..d7e9f2c2b 100644 --- a/app/src/androidTest/java/to/bitkit/ui/settings/support/ReportIssueScreenTest.kt +++ b/app/src/androidTest/java/to/bitkit/ui/settings/support/ReportIssueScreenTest.kt @@ -29,7 +29,6 @@ class ReportIssueContentTest { AppThemeSurface { ReportIssueContent( onBack = {}, - onClose = {}, onConfirm = {}, onUpdateEmail = {}, onUpdateMessage = {}, @@ -54,7 +53,6 @@ class ReportIssueContentTest { AppThemeSurface { ReportIssueContent( onBack = {}, - onClose = {}, onConfirm = {}, onUpdateEmail = { emailUpdated = it }, onUpdateMessage = {}, @@ -77,7 +75,6 @@ class ReportIssueContentTest { AppThemeSurface { ReportIssueContent( onBack = {}, - onClose = {}, onConfirm = {}, onUpdateEmail = {}, onUpdateMessage = { messageUpdated = it }, @@ -100,7 +97,6 @@ class ReportIssueContentTest { AppThemeSurface { ReportIssueContent( onBack = {}, - onClose = {}, onConfirm = { sendClicked = true }, onUpdateEmail = {}, onUpdateMessage = {}, @@ -121,7 +117,6 @@ class ReportIssueContentTest { AppThemeSurface { ReportIssueContent( onBack = {}, - onClose = {}, onConfirm = {}, onUpdateEmail = {}, onUpdateMessage = {}, @@ -140,7 +135,6 @@ class ReportIssueContentTest { AppThemeSurface { ReportIssueContent( onBack = {}, - onClose = {}, onConfirm = {}, onUpdateEmail = {}, onUpdateMessage = {}, @@ -163,7 +157,6 @@ class ReportIssueContentTest { AppThemeSurface { ReportIssueContent( onBack = {}, - onClose = {}, onConfirm = {}, onUpdateEmail = {}, onUpdateMessage = {}, diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d9daa8a76..e4693f14b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -20,6 +20,11 @@ + + + android:exported="false" /> + + - + + + - - @@ -90,16 +101,20 @@ + + - - diff --git a/app/src/main/java/to/bitkit/di/EnvModule.kt b/app/src/main/java/to/bitkit/di/EnvModule.kt index f7b3c187c..7bd559988 100644 --- a/app/src/main/java/to/bitkit/di/EnvModule.kt +++ b/app/src/main/java/to/bitkit/di/EnvModule.kt @@ -6,9 +6,10 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import kotlinx.datetime.Clock import org.lightningdevkit.ldknode.Network import to.bitkit.env.Env +import kotlin.time.Clock +import kotlin.time.ExperimentalTime @Module @InstallIn(SingletonComponent::class) @@ -19,6 +20,7 @@ object EnvModule { return Env.network } + @OptIn(ExperimentalTime::class) @Provides fun provideClock(): Clock { return Clock.System diff --git a/app/src/main/java/to/bitkit/domain/commands/NotifyPaymentReceivedHandler.kt b/app/src/main/java/to/bitkit/domain/commands/NotifyPaymentReceivedHandler.kt index 576422178..c9ad3869e 100644 --- a/app/src/main/java/to/bitkit/domain/commands/NotifyPaymentReceivedHandler.kt +++ b/app/src/main/java/to/bitkit/domain/commands/NotifyPaymentReceivedHandler.kt @@ -25,8 +25,8 @@ import javax.inject.Singleton @Singleton class NotifyPaymentReceivedHandler @Inject constructor( - @param:ApplicationContext private val context: Context, - @param:IoDispatcher private val ioDispatcher: CoroutineDispatcher, + @ApplicationContext private val context: Context, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, private val activityRepo: ActivityRepo, private val currencyRepo: CurrencyRepo, private val settingsStore: SettingsStore, diff --git a/app/src/main/java/to/bitkit/ext/Activities.kt b/app/src/main/java/to/bitkit/ext/Activities.kt index 73153cf40..9b960ffbe 100644 --- a/app/src/main/java/to/bitkit/ext/Activities.kt +++ b/app/src/main/java/to/bitkit/ext/Activities.kt @@ -1,6 +1,8 @@ package to.bitkit.ext import com.synonym.bitkitcore.Activity +import com.synonym.bitkitcore.LightningActivity +import com.synonym.bitkitcore.OnchainActivity import com.synonym.bitkitcore.PaymentState import com.synonym.bitkitcore.PaymentType @@ -70,3 +72,75 @@ fun Activity.Onchain.boostType() = when (this.v1.txType) { } enum class BoostType { RBF, CPFP } + +@Suppress("LongParameterList") +fun LightningActivity.Companion.create( + id: String, + txType: PaymentType, + status: PaymentState, + value: ULong, + invoice: String, + timestamp: ULong, + fee: ULong = 0u, + message: String = "", + preimage: String? = null, + createdAt: ULong? = timestamp, + updatedAt: ULong? = createdAt, + seenAt: ULong? = null, +) = LightningActivity( + id = id, + txType = txType, + status = status, + value = value, + fee = fee, + invoice = invoice, + message = message, + timestamp = timestamp, + preimage = preimage, + createdAt = createdAt, + updatedAt = updatedAt, + seenAt = seenAt, +) + +@Suppress("LongParameterList") +fun OnchainActivity.Companion.create( + id: String, + txType: PaymentType, + txId: String, + value: ULong, + fee: ULong, + address: String, + timestamp: ULong, + confirmed: Boolean = false, + feeRate: ULong = 1u, + isBoosted: Boolean = false, + boostTxIds: List = emptyList(), + isTransfer: Boolean = false, + doesExist: Boolean = true, + confirmTimestamp: ULong? = null, + channelId: String? = null, + transferTxId: String? = null, + createdAt: ULong? = timestamp, + updatedAt: ULong? = createdAt, + seenAt: ULong? = null, +) = OnchainActivity( + id = id, + txType = txType, + txId = txId, + value = value, + fee = fee, + feeRate = feeRate, + address = address, + confirmed = confirmed, + timestamp = timestamp, + isBoosted = isBoosted, + boostTxIds = boostTxIds, + isTransfer = isTransfer, + doesExist = doesExist, + confirmTimestamp = confirmTimestamp, + channelId = channelId, + transferTxId = transferTxId, + createdAt = createdAt, + updatedAt = updatedAt, + seenAt = seenAt, +) diff --git a/app/src/main/java/to/bitkit/ext/ByteArray.kt b/app/src/main/java/to/bitkit/ext/ByteArray.kt index 1246163b2..7aa91d0d5 100644 --- a/app/src/main/java/to/bitkit/ext/ByteArray.kt +++ b/app/src/main/java/to/bitkit/ext/ByteArray.kt @@ -12,13 +12,11 @@ fun ByteArray.toHex(): String = this.toHexString() fun String.fromHex(): ByteArray = this.hexToByteArray() // endregion -// region base64 @OptIn(ExperimentalEncodingApi::class) fun ByteArray.toBase64(): String = Base64.encode(this) @OptIn(ExperimentalEncodingApi::class) fun String.fromBase64(): ByteArray = Base64.decode(this) -// endregion val String.uByteList get() = this.toByteArray().map { it.toUByte() } diff --git a/app/src/main/java/to/bitkit/ext/DateTime.kt b/app/src/main/java/to/bitkit/ext/DateTime.kt index ebf9fabc0..bea254c2b 100644 --- a/app/src/main/java/to/bitkit/ext/DateTime.kt +++ b/app/src/main/java/to/bitkit/ext/DateTime.kt @@ -10,11 +10,12 @@ import android.icu.text.RelativeDateTimeFormatter.AbsoluteUnit import android.icu.text.RelativeDateTimeFormatter.Direction import android.icu.text.RelativeDateTimeFormatter.RelativeUnit import android.icu.util.ULocale -import kotlinx.datetime.Clock import kotlinx.datetime.LocalDate import kotlinx.datetime.TimeZone import kotlinx.datetime.atStartOfDayIn +import kotlinx.datetime.number import kotlinx.datetime.toJavaLocalDate +import kotlinx.datetime.toJavaMonth import kotlinx.datetime.toKotlinLocalDate import kotlinx.datetime.toLocalDateTime import java.text.SimpleDateFormat @@ -26,9 +27,13 @@ import java.time.temporal.ChronoUnit import java.util.Calendar import java.util.Date import java.util.Locale +import kotlin.time.Clock import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.ExperimentalTime +import kotlin.time.Instant as KInstant +@OptIn(ExperimentalTime::class) fun nowMillis(clock: Clock = Clock.System): Long = clock.now().toEpochMilliseconds() fun nowTimestamp(): Instant = Instant.now().truncatedTo(ChronoUnit.SECONDS) @@ -63,6 +68,7 @@ fun Long.toLocalizedTimestamp(): String { } @Suppress("LongMethod") +@OptIn(ExperimentalTime::class) fun Long.toRelativeTimeString( locale: Locale = Locale.getDefault(), clock: Clock = Clock.System, @@ -102,7 +108,8 @@ fun Long.toRelativeTimeString( fun getDaysInMonth(month: LocalDate): List { val firstDayOfMonth = LocalDate(month.year, month.month, Constants.FIRST_DAY_OF_MONTH) - val daysInMonth = month.month.length(isLeapYear(month.year)) + // FIXME fix month.number + val daysInMonth = month.month.toJavaMonth().length(isLeapYear(month.year)) // Get the day of week for the first day (1 = Monday, 7 = Sunday) val firstDayOfWeek = firstDayOfMonth.dayOfWeek.ordinal + CalendarConstants.CALENDAR_WEEK_OFFSET @@ -136,6 +143,7 @@ fun isLeapYear(year: Int): Boolean { (year % Constants.LEAP_YEAR_DIVISOR_400 == 0) } +@OptIn(ExperimentalTime::class) fun isDateInRange( dateMillis: Long, startMillis: Long?, @@ -145,9 +153,9 @@ fun isDateInRange( if (startMillis == null) return false val end = endMillis ?: startMillis - val normalizedDate = kotlinx.datetime.Instant.fromEpochMilliseconds(dateMillis).toLocalDateTime(zone).date - val normalizedStart = kotlinx.datetime.Instant.fromEpochMilliseconds(startMillis).toLocalDateTime(zone).date - val normalizedEnd = kotlinx.datetime.Instant.fromEpochMilliseconds(end).toLocalDateTime(zone).date + val normalizedDate = KInstant.fromEpochMilliseconds(dateMillis).toLocalDateTime(zone).date + val normalizedStart = KInstant.fromEpochMilliseconds(startMillis).toLocalDateTime(zone).date + val normalizedEnd = KInstant.fromEpochMilliseconds(end).toLocalDateTime(zone).date return normalizedDate in normalizedStart..normalizedEnd } @@ -155,7 +163,7 @@ fun isDateInRange( fun LocalDate.toMonthYearString(locale: Locale = Locale.getDefault()): String { val formatter = SimpleDateFormat(DatePattern.MONTH_YEAR_FORMAT, locale) val calendar = Calendar.getInstance() - calendar.set(year, monthNumber - CalendarConstants.MONTH_INDEX_OFFSET, Constants.FIRST_DAY_OF_MONTH) + calendar.set(year, month.number - CalendarConstants.MONTH_INDEX_OFFSET, Constants.FIRST_DAY_OF_MONTH) return formatter.format(calendar.time) } @@ -167,6 +175,7 @@ fun LocalDate.plusMonths(months: Int): LocalDate = toJavaLocalDate().plusMonths(months.toLong()).withDayOfMonth(1) // Always use first day of month for display .toKotlinLocalDate() +@OptIn(ExperimentalTime::class) fun LocalDate.endOfDay(zone: TimeZone = TimeZone.currentSystemDefault()): Long = atStartOfDayIn(zone).plus(1.days).minus(1.milliseconds).toEpochMilliseconds() diff --git a/app/src/main/java/to/bitkit/models/Currency.kt b/app/src/main/java/to/bitkit/models/Currency.kt index ebef4fd36..1b5774e7f 100644 --- a/app/src/main/java/to/bitkit/models/Currency.kt +++ b/app/src/main/java/to/bitkit/models/Currency.kt @@ -1,12 +1,13 @@ package to.bitkit.models -import kotlinx.datetime.Instant import kotlinx.serialization.Serializable import java.math.BigDecimal import java.math.RoundingMode import java.text.DecimalFormat import java.text.DecimalFormatSymbols import java.util.Locale +import kotlin.time.ExperimentalTime +import kotlin.time.Instant const val STUB_RATE = 115_150.0 const val BITCOIN_SYMBOL = "₿" @@ -37,11 +38,10 @@ data class FxRate( val currencyFlag: String, val lastUpdatedAt: Long, ) { - val rate: Double - get() = lastPrice.toDoubleOrNull() ?: 0.0 + val rate: Double get() = lastPrice.toDoubleOrNull() ?: 0.0 - val timestamp: Instant - get() = Instant.fromEpochMilliseconds(lastUpdatedAt) + @OptIn(ExperimentalTime::class) + val timestamp: Instant get() = Instant.fromEpochMilliseconds(lastUpdatedAt) } /** aka. Unit */ diff --git a/app/src/main/java/to/bitkit/repositories/ActivityRepo.kt b/app/src/main/java/to/bitkit/repositories/ActivityRepo.kt index 0f7b6382c..804845a5c 100644 --- a/app/src/main/java/to/bitkit/repositories/ActivityRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/ActivityRepo.kt @@ -21,7 +21,6 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout -import kotlinx.datetime.Clock import org.lightningdevkit.ldknode.ChannelDetails import org.lightningdevkit.ldknode.PaymentDetails import org.lightningdevkit.ldknode.PaymentDirection @@ -40,11 +39,14 @@ import to.bitkit.services.CoreService import to.bitkit.utils.Logger import javax.inject.Inject import javax.inject.Singleton +import kotlin.time.Clock +import kotlin.time.ExperimentalTime private const val SYNC_TIMEOUT_MS = 40_000L -@Singleton @Suppress("LargeClass", "LongParameterList") +@OptIn(ExperimentalTime::class) +@Singleton class ActivityRepo @Inject constructor( @BgDispatcher private val bgDispatcher: CoroutineDispatcher, private val coreService: CoreService, @@ -138,7 +140,7 @@ class ActivityRepo @Inject constructor( } } - private suspend fun findOpenChannelForTransaction(txid: String): String? { + private fun findOpenChannelForTransaction(txid: String): String? { return try { val channels = lightningRepo.lightningState.value.channels if (channels.isEmpty()) return null @@ -183,7 +185,7 @@ class ActivityRepo @Inject constructor( } /** - * Checks if a transaction was replaced (RBF) by checking if the activity exists but doesExist=false. + * Checks if a transaction was replaced (RBF) by checking if the activity exists but `doesExist=false`. */ suspend fun wasTransactionReplaced(txid: String): Boolean = withContext(bgDispatcher) { val onchainActivity = getOnchainActivityByTxId(txid) ?: return@withContext false @@ -374,7 +376,7 @@ class ActivityRepo @Inject constructor( /** * Updates an activity and marks the old one as removed from mempool (for RBF). * In case of failure in the update or marking as removed, the data will be cached - * to try again on the next sync + * to try again on the next sync. */ suspend fun replaceActivity( id: String, @@ -460,9 +462,6 @@ class ActivityRepo @Inject constructor( }.awaitAll() } - /** - * Deletes an activity - */ suspend fun deleteActivity(id: String): Result = withContext(bgDispatcher) { return@withContext runCatching { val deleted = coreService.activity.delete(id) @@ -477,9 +476,6 @@ class ActivityRepo @Inject constructor( } } - /** - * Inserts a new activity - */ suspend fun insertActivity(activity: Activity): Result = withContext(bgDispatcher) { return@withContext runCatching { if (activity.rawId() in cacheStore.data.first().deletedActivities) { @@ -493,9 +489,6 @@ class ActivityRepo @Inject constructor( } } - /** - * Upserts an activity (insert or update if exists) - */ suspend fun upsertActivity(activity: Activity): Result = withContext(bgDispatcher) { return@withContext runCatching { if (activity.rawId() in cacheStore.data.first().deletedActivities) { @@ -510,7 +503,7 @@ class ActivityRepo @Inject constructor( } /** - * Inserts a new activity for a fulfilled (channel ready) cjit channel order + * Inserts a new activity for a fulfilled (channel ready) CJIT order */ suspend fun insertActivityFromCjit( cjitEntry: IcJitEntry?, @@ -536,6 +529,7 @@ class ActivityRepo @Inject constructor( preimage = null, createdAt = now, updatedAt = null, + seenAt = null, // TODO implement synonymdev/bitkit-ios#270 changes ) ) ) diff --git a/app/src/main/java/to/bitkit/repositories/BackupRepo.kt b/app/src/main/java/to/bitkit/repositories/BackupRepo.kt index 2e98b443c..b3e147b04 100644 --- a/app/src/main/java/to/bitkit/repositories/BackupRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/BackupRepo.kt @@ -21,7 +21,6 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import kotlinx.datetime.Clock import to.bitkit.R import to.bitkit.data.AppDb import to.bitkit.data.CacheStore @@ -49,6 +48,8 @@ import to.bitkit.utils.jsonLogOf import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject import javax.inject.Singleton +import kotlin.time.Clock +import kotlin.time.ExperimentalTime /** * Manages backup & restore of wallet metadata to a remote VSS server. @@ -65,10 +66,11 @@ import javax.inject.Singleton * ``` */ @Suppress("LongParameterList") +@OptIn(ExperimentalTime::class) @Singleton class BackupRepo @Inject constructor( - @param:ApplicationContext private val context: Context, - @param:IoDispatcher private val ioDispatcher: CoroutineDispatcher, + @ApplicationContext private val context: Context, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, private val cacheStore: CacheStore, private val vssBackupClient: VssBackupClient, private val settingsStore: SettingsStore, diff --git a/app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt b/app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt index a8629a23b..82a39095e 100644 --- a/app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt @@ -18,7 +18,6 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import kotlinx.datetime.Clock import to.bitkit.data.CacheStore import to.bitkit.data.SettingsStore import to.bitkit.di.BgDispatcher @@ -41,16 +40,19 @@ import java.math.RoundingMode import javax.inject.Inject import javax.inject.Named import javax.inject.Singleton +import kotlin.time.Clock +import kotlin.time.ExperimentalTime @Suppress("TooManyFunctions") +@OptIn(ExperimentalTime::class) @Singleton class CurrencyRepo @Inject constructor( @BgDispatcher private val bgDispatcher: CoroutineDispatcher, private val currencyService: CurrencyService, private val settingsStore: SettingsStore, private val cacheStore: CacheStore, - @Named("enablePolling") private val enablePolling: Boolean, private val clock: Clock, + @Named("enablePolling") private val enablePolling: Boolean, ) : AmountInputHandler { private val repoScope = CoroutineScope(bgDispatcher + SupervisorJob()) private val _currencyState = MutableStateFlow(CurrencyState()) diff --git a/app/src/main/java/to/bitkit/repositories/HealthRepo.kt b/app/src/main/java/to/bitkit/repositories/HealthRepo.kt index 72b9a211d..56e8e85ee 100644 --- a/app/src/main/java/to/bitkit/repositories/HealthRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/HealthRepo.kt @@ -11,7 +11,6 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.datetime.Clock import to.bitkit.data.CacheStore import to.bitkit.di.BgDispatcher import to.bitkit.models.BackupCategory @@ -19,8 +18,11 @@ import to.bitkit.models.BackupItemStatus import to.bitkit.models.HealthState import javax.inject.Inject import javax.inject.Singleton +import kotlin.time.Clock import kotlin.time.Duration.Companion.minutes +import kotlin.time.ExperimentalTime +@OptIn(ExperimentalTime::class) @Singleton class HealthRepo @Inject constructor( @BgDispatcher private val bgDispatcher: CoroutineDispatcher, diff --git a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt index 2d9c35dfd..5163ca0e9 100644 --- a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt @@ -21,15 +21,16 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.tasks.await import kotlinx.coroutines.withContext -import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeoutOrNull import org.lightningdevkit.ldknode.Address import org.lightningdevkit.ldknode.BalanceDetails import org.lightningdevkit.ldknode.BestBlock import org.lightningdevkit.ldknode.ChannelConfig import org.lightningdevkit.ldknode.ChannelDetails +import org.lightningdevkit.ldknode.ClosureReason import org.lightningdevkit.ldknode.Event import org.lightningdevkit.ldknode.NodeStatus import org.lightningdevkit.ldknode.PaymentDetails @@ -61,9 +62,12 @@ import to.bitkit.services.NodeEventHandler import to.bitkit.utils.AppError import to.bitkit.utils.Logger import to.bitkit.utils.ServiceError +import to.bitkit.utils.errLogOf import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject import javax.inject.Singleton +import kotlin.coroutines.cancellation.CancellationException import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds @@ -96,6 +100,9 @@ class LightningRepo @Inject constructor( private val channelCache = ConcurrentHashMap() + private val syncMutex = Mutex() + private val syncPending = AtomicBoolean(false) + /** * Executes the provided operation only if the node is running. * If the node is not running, waits for it to be running for a specified timeout. @@ -152,6 +159,9 @@ class LightningRepo @Inject constructor( ): Result { return try { operation() + } catch (e: CancellationException) { + // Cancellation is expected during pull-to-refresh, rethrow per Kotlin best practices + throw e } catch (e: Throwable) { Logger.error("$operationName error", e, context = TAG) Result.failure(e) @@ -300,24 +310,35 @@ class LightningRepo @Inject constructor( } suspend fun sync(): Result = executeWhenNodeRunning("Sync") { - syncState() - if (_lightningState.value.isSyncingWallet) { - Logger.warn("Sync already in progress, waiting for existing sync.", context = TAG) + // If sync is in progress, mark pending and skip + if (!syncMutex.tryLock()) { + syncPending.set(true) + Logger.verbose("Sync in progress, pending sync marked", context = TAG) + return@executeWhenNodeRunning Result.success(Unit) } - withTimeout(SYNC_TIMEOUT_MS) { - _lightningState.first { !it.isSyncingWallet } + try { + do { + syncPending.set(false) + _lightningState.update { it.copy(isSyncingWallet = true) } + lightningService.sync() + refreshChannelCache() + syncState() + if (syncPending.get()) delay(SYNC_LOOP_DEBOUNCE_MS) + } while (syncPending.getAndSet(false)) + } finally { + _lightningState.update { it.copy(isSyncingWallet = false) } + syncMutex.unlock() } - _lightningState.update { it.copy(isSyncingWallet = true) } - lightningService.sync() - refreshChannelCache() - syncState() - _lightningState.update { it.copy(isSyncingWallet = false) } - Result.success(Unit) } + /** Clear pending sync flag. Called when manual pull-to-refresh takes priority. */ + fun clearPendingSync() { + syncPending.set(false) + } + private suspend fun refreshChannelCache() = withContext(bgDispatcher) { val channels = lightningService.channels ?: return@withContext channels.forEach { channel -> @@ -327,39 +348,26 @@ class LightningRepo @Inject constructor( private fun handleLdkEvent(event: Event) { when (event) { - is Event.ChannelPending -> { - scope.launch { - refreshChannelCache() - } + is Event.ChannelPending, + is Event.ChannelReady -> scope.launch { + refreshChannelCache() } - is Event.ChannelReady -> { - scope.launch { - refreshChannelCache() - } - } - - is Event.ChannelClosed -> { - val channelId = event.channelId - val reason = event.reason?.toString() ?: "" - scope.launch { - registerClosedChannel(channelId, reason) - } + is Event.ChannelClosed -> scope.launch { + registerClosedChannel( + channelId = event.channelId, + reason = event.reason, + ) } - else -> { - // Other events don't need special handling - } + else -> Unit // Other events don't need special handling } } - private suspend fun registerClosedChannel(channelId: String, reason: String?) = withContext(bgDispatcher) { + private suspend fun registerClosedChannel(channelId: String, reason: ClosureReason?) = withContext(bgDispatcher) { try { val channel = channelCache[channelId] ?: run { - Logger.error( - "Could not find channel details for closed channel: channelId=$channelId", - context = TAG - ) + Logger.error("Could not find channel details for closed channel: channelId=$channelId", context = TAG) return@withContext } @@ -391,7 +399,7 @@ class LightningRepo @Inject constructor( forwardingFeeProportionalMillionths = channel.config.forwardingFeeProportionalMillionths, forwardingFeeBaseMsat = channel.config.forwardingFeeBaseMsat, channelName = channelName, - channelClosureReason = reason.orEmpty() + channelClosureReason = reason?.toString().orEmpty(), ) coreService.activity.upsertClosedChannelList(listOf(closedChannel)) @@ -756,9 +764,11 @@ class LightningRepo @Inject constructor( utxosToSpend = utxosToSpend, ) Result.success(fee) - } catch (_: Throwable) { + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { val fallbackFee = 1000uL - Logger.warn("Error calculating fee, using fallback of $fallbackFee", context = TAG) + Logger.warn("Error calculating fee, using fallback of $fallbackFee ${errLogOf(e)}", context = TAG) Result.success(fallbackFee) } } @@ -772,7 +782,9 @@ class LightningRepo @Inject constructor( val satsPerVByte = fees.getSatsPerVByteFor(speed) satsPerVByte.toULong() }.onFailure { e -> - Logger.error("Error getFeeRateForSpeed. speed:$speed", e, context = TAG) + if (e !is CancellationException) { + Logger.error("Error getFeeRateForSpeed. speed:$speed", e, context = TAG) + } } } @@ -989,8 +1001,8 @@ class LightningRepo @Inject constructor( companion object { private const val TAG = "LightningRepo" - private const val SYNC_TIMEOUT_MS = 20_000L private const val CHANNEL_ID_PREVIEW_LENGTH = 10 + private const val SYNC_LOOP_DEBOUNCE_MS = 500L } } diff --git a/app/src/main/java/to/bitkit/repositories/PreActivityMetadataRepo.kt b/app/src/main/java/to/bitkit/repositories/PreActivityMetadataRepo.kt index 5b3ca2bb1..d0fa79b79 100644 --- a/app/src/main/java/to/bitkit/repositories/PreActivityMetadataRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/PreActivityMetadataRepo.kt @@ -7,7 +7,6 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.withContext -import kotlinx.datetime.Clock import to.bitkit.di.IoDispatcher import to.bitkit.ext.nowMillis import to.bitkit.ext.nowTimestamp @@ -15,7 +14,10 @@ import to.bitkit.services.CoreService import to.bitkit.utils.Logger import javax.inject.Inject import javax.inject.Singleton +import kotlin.time.Clock +import kotlin.time.ExperimentalTime +@OptIn(ExperimentalTime::class) @Singleton class PreActivityMetadataRepo @Inject constructor( @IoDispatcher private val ioDispatcher: CoroutineDispatcher, diff --git a/app/src/main/java/to/bitkit/repositories/TransferRepo.kt b/app/src/main/java/to/bitkit/repositories/TransferRepo.kt index 4e4596ddf..6aa2e8708 100644 --- a/app/src/main/java/to/bitkit/repositories/TransferRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/TransferRepo.kt @@ -4,7 +4,6 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.withContext -import kotlinx.datetime.Clock import org.lightningdevkit.ldknode.ChannelDetails import to.bitkit.data.dao.TransferDao import to.bitkit.data.entities.TransferEntity @@ -12,10 +11,13 @@ import to.bitkit.di.BgDispatcher import to.bitkit.ext.channelId import to.bitkit.models.TransferType import to.bitkit.utils.Logger -import java.util.* +import java.util.UUID import javax.inject.Inject import javax.inject.Singleton +import kotlin.time.Clock +import kotlin.time.ExperimentalTime +@OptIn(ExperimentalTime::class) @Singleton class TransferRepo @Inject constructor( @BgDispatcher private val bgDispatcher: CoroutineDispatcher, @@ -100,8 +102,10 @@ class TransferRepo @Inject constructor( Logger.debug("Channel $channelId balance swept, settled transfer: ${transfer.id}", context = TAG) } } + }.onSuccess { + Logger.verbose("syncTransferStates completed", context = TAG) }.onFailure { e -> - Logger.error("Failed to sync transfer states", e, context = TAG) + Logger.error("syncTransferStates error", e, context = TAG) } } diff --git a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt index f9a6f6370..7e82063d9 100644 --- a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt @@ -6,8 +6,10 @@ import com.synonym.bitkitcore.Scanner import com.synonym.bitkitcore.decode import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.first @@ -33,8 +35,11 @@ import to.bitkit.usecases.WipeWalletUseCase import to.bitkit.utils.Bip21Utils import to.bitkit.utils.Logger import to.bitkit.utils.ServiceError +import to.bitkit.utils.errLogOf +import to.bitkit.utils.measured import javax.inject.Inject import javax.inject.Singleton +import kotlin.coroutines.cancellation.CancellationException @Suppress("LongParameterList") @Singleton @@ -48,6 +53,7 @@ class WalletRepo @Inject constructor( private val preActivityMetadataRepo: PreActivityMetadataRepo, private val deriveBalanceStateUseCase: DeriveBalanceStateUseCase, private val wipeWalletUseCase: WipeWalletUseCase, + private val transferRepo: TransferRepo, ) { private val repoScope = CoroutineScope(bgDispatcher + SupervisorJob()) @@ -57,6 +63,8 @@ class WalletRepo @Inject constructor( private val _balanceState = MutableStateFlow(BalanceState()) val balanceState = _balanceState.asStateFlow() + private var eventSyncJob: Job? = null + init { repoScope.launch { lightningRepo.nodeEvents.collect { event -> @@ -160,32 +168,60 @@ class WalletRepo @Inject constructor( preActivityMetadataRepo.addPreActivityMetadata(preActivityMetadata) } - suspend fun syncNodeAndWallet(): Result = withContext(bgDispatcher) { + suspend fun syncNodeAndWallet(source: SyncSource = SyncSource.AUTO): Result = withContext(bgDispatcher) { + if (!lightningRepo.lightningState.value.nodeLifecycleState.isRunning()) { + Logger.debug("syncNodeAndWallet skipped: node not running", context = TAG) + return@withContext Result.failure(Exception("Node not running")) + } + + val sourceLabel = source.name.lowercase() val startHeight = lightningRepo.lightningState.value.block()?.height - Logger.verbose("syncNodeAndWallet started at block height=$startHeight", context = TAG) - syncBalances() - lightningRepo.sync().onSuccess { + Logger.debug("Sync $sourceLabel started at block height=$startHeight", context = TAG) + + val result = measured("Sync $sourceLabel") { syncBalances() - val endHeight = lightningRepo.lightningState.value.block()?.height - Logger.verbose("syncNodeAndWallet completed at block height=$endHeight", context = TAG) - return@withContext Result.success(Unit) - }.onFailure { e -> - if (e is TimeoutCancellationException) { + lightningRepo.sync().onSuccess { syncBalances() + }.onFailure { e -> + if (e is TimeoutCancellationException) { + syncBalances() + } } - return@withContext Result.failure(e) } + + val endHeight = lightningRepo.lightningState.value.block()?.height + Logger.debug("Sync $sourceLabel completed at block height=$endHeight", context = TAG) + + result } suspend fun syncBalances() { deriveBalanceStateUseCase().onSuccess { balanceState -> runCatching { cacheStore.cacheBalance(balanceState) } _balanceState.update { balanceState } - }.onFailure { - Logger.warn("Could not sync balances", context = TAG) + }.onFailure { e -> + if (e !is CancellationException) { + Logger.warn("Could not sync balances ${errLogOf(e)}", context = TAG) + } + } + } + + /** Debounce syncs for [Event.SyncCompleted]. Rapid consecutive events are coalesced. */ + fun debounceSyncByEvent() { + eventSyncJob?.cancel() + eventSyncJob = repoScope.launch { + delay(EVENT_SYNC_DEBOUNCE_MS) + syncNodeAndWallet() + transferRepo.syncTransferStates() } } + /** Cancels any pending sync for [Event.SyncCompleted]. Called when manual pull-to-refresh takes priority. */ + fun cancelSyncByEvent() { + eventSyncJob?.cancel() + eventSyncJob = null + } + suspend fun refreshBip21ForEvent(event: Event) = withContext(bgDispatcher) { when (event) { is Event.ChannelReady -> { @@ -570,6 +606,7 @@ class WalletRepo @Inject constructor( private companion object { const val TAG = "WalletRepo" + const val EVENT_SYNC_DEBOUNCE_MS = 500L } } @@ -583,3 +620,5 @@ data class WalletState( val receiveOnSpendingBalance: Boolean = true, val walletExists: Boolean = false, ) + +enum class SyncSource { AUTO, MANUAL } diff --git a/app/src/main/java/to/bitkit/services/CoreService.kt b/app/src/main/java/to/bitkit/services/CoreService.kt index 76c621cf2..34b18f06b 100644 --- a/app/src/main/java/to/bitkit/services/CoreService.kt +++ b/app/src/main/java/to/bitkit/services/CoreService.kt @@ -69,6 +69,7 @@ import to.bitkit.async.ServiceQueue import to.bitkit.data.CacheStore import to.bitkit.env.Env import to.bitkit.ext.amountSats +import to.bitkit.ext.create import to.bitkit.models.toCoreNetwork import to.bitkit.utils.AppError import to.bitkit.utils.Logger @@ -473,18 +474,17 @@ class ActivityService( status = state ) } else { - LightningActivity( + LightningActivity.create( id = payment.id, txType = payment.direction.toPaymentType(), status = state, value = payment.amountSats ?: 0u, - fee = (payment.feePaidMsat ?: 0u) / 1000u, invoice = kind.bolt11 ?: "Loading...", - message = kind.description.orEmpty(), timestamp = payment.latestUpdateTimestamp, + fee = (payment.feePaidMsat ?: 0u) / 1000u, + message = kind.description.orEmpty(), preimage = kind.preimage, - createdAt = payment.latestUpdateTimestamp, - updatedAt = payment.latestUpdateTimestamp, + seenAt = null, // TODO implement synonymdev/bitkit-ios#270 changes ) } @@ -597,25 +597,19 @@ class ActivityService( ): OnchainActivity { val isTransfer = channelId != null - return OnchainActivity( + return OnchainActivity.create( id = payment.id, txType = payment.direction.toPaymentType(), txId = kind.txid, value = payment.amountSats ?: 0u, fee = (payment.feePaidMsat ?: 0u) / 1000u, - feeRate = 1u, address = resolvedAddress ?: "Loading...", - confirmed = confirmationData.isConfirmed, timestamp = confirmationData.timestamp, - isBoosted = false, - boostTxIds = emptyList(), + confirmed = confirmationData.isConfirmed, isTransfer = isTransfer, - doesExist = true, confirmTimestamp = confirmationData.confirmedTimestamp, channelId = channelId, - transferTxId = null, - createdAt = confirmationData.timestamp, - updatedAt = confirmationData.timestamp, + seenAt = null, // TODO implement synonymdev/bitkit-ios#270 changes ) } @@ -726,42 +720,35 @@ class ActivityService( if (isLightning) { id = "test-lightning-$i" activity = Activity.Lightning( - LightningActivity( + LightningActivity.create( id = id, txType = txType, status = status, value = value, - fee = (1..1_000).random().toULong(), invoice = "lnbc$value", - message = possibleMessages.random(), timestamp = txTimestamp, + fee = (1..1_000).random().toULong(), + message = possibleMessages.random(), preimage = if (Random.nextBoolean()) "preimage$i" else null, - createdAt = txTimestamp, - updatedAt = txTimestamp ) ) } else { id = "test-onchain-$i" activity = Activity.Onchain( - OnchainActivity( + OnchainActivity.create( id = id, txType = txType, txId = "a".repeat(64), // Mock txid value = value, fee = (100..10_000).random().toULong(), - feeRate = (1..100).random().toULong(), address = "bc1...$i", - confirmed = Random.nextBoolean(), timestamp = txTimestamp, + confirmed = Random.nextBoolean(), + feeRate = (1..100).random().toULong(), isBoosted = Random.nextBoolean(), - boostTxIds = emptyList(), isTransfer = Random.nextBoolean(), - doesExist = true, confirmTimestamp = if (Random.nextBoolean()) txTimestamp + 3600.toULong() else null, channelId = if (Random.nextBoolean()) "channel$i" else null, - transferTxId = null, - createdAt = txTimestamp, - updatedAt = txTimestamp, ) ) } diff --git a/app/src/main/java/to/bitkit/services/LightningService.kt b/app/src/main/java/to/bitkit/services/LightningService.kt index b69559555..2583081e9 100644 --- a/app/src/main/java/to/bitkit/services/LightningService.kt +++ b/app/src/main/java/to/bitkit/services/LightningService.kt @@ -24,7 +24,6 @@ import org.lightningdevkit.ldknode.ElectrumSyncConfig import org.lightningdevkit.ldknode.Event import org.lightningdevkit.ldknode.FeeRate import org.lightningdevkit.ldknode.Node -import org.lightningdevkit.ldknode.NodeEntropy import org.lightningdevkit.ldknode.NodeException import org.lightningdevkit.ldknode.NodeStatus import org.lightningdevkit.ldknode.PaymentDetails @@ -118,43 +117,32 @@ class LightningService @Inject constructor( customRgsServerUrl: String?, config: Config, ): Node = ServiceQueue.LDK.background { - val nodeEntropy = NodeEntropy.fromBip39Mnemonic( - mnemonic = keychain.loadString(Keychain.Key.BIP39_MNEMONIC.name) ?: throw ServiceError.MnemonicNotFound, - passphrase = keychain.loadString(Keychain.Key.BIP39_PASSPHRASE.name), - ) - val builder = Builder.fromConfig(config).apply { setCustomLogger(LdkLogWriter()) configureChainSource(customServerUrl) configureGossipSource(customRgsServerUrl) + setEntropyBip39Mnemonic( + mnemonic = keychain.loadString(Keychain.Key.BIP39_MNEMONIC.name) ?: throw ServiceError.MnemonicNotFound, + passphrase = keychain.loadString(Keychain.Key.BIP39_PASSPHRASE.name), + ) } try { val vssStoreId = vssStoreIdProvider.getVssStoreId(walletIndex) - val lnurlAuthServerUrl = Env.lnurlAuthServerUrl val vssUrl = Env.vssServerUrl - Logger.verbose("Building ldk-node with vssUrl: '$vssUrl'") - Logger.verbose("Building ldk-node with lnurlAuthServerUrl: '$lnurlAuthServerUrl'") + val lnurlAuthServerUrl = Env.lnurlAuthServerUrl + val fixedHeaders = emptyMap() + Logger.verbose( + "Building ldk-node with \n\t vssUrl: '$vssUrl'\n\t lnurlAuthServerUrl: '$lnurlAuthServerUrl'" + ) if (lnurlAuthServerUrl.isNotEmpty()) { - builder.buildWithVssStore( - vssUrl = vssUrl, - storeId = vssStoreId, - lnurlAuthServerUrl = lnurlAuthServerUrl, - fixedHeaders = emptyMap(), - nodeEntropy = nodeEntropy, - ) + builder.buildWithVssStore(vssUrl, vssStoreId, lnurlAuthServerUrl, fixedHeaders) } else { - builder.buildWithVssStoreAndFixedHeaders( - vssUrl = vssUrl, - storeId = vssStoreId, - fixedHeaders = emptyMap(), - nodeEntropy = nodeEntropy, - ) + builder.buildWithVssStoreAndFixedHeaders(vssUrl, vssStoreId, fixedHeaders) } } catch (e: BuildException) { throw LdkError(e) } finally { - // cleanup sensitive data - nodeEntropy.destroy() + // TODO: cleanup sensitive data after implementing a `SecureString` value holder for Keychain return values } } @@ -674,7 +662,7 @@ class LightningService @Inject constructor( ): ULong { val node = this.node ?: throw ServiceError.NodeNotSetup - Logger.debug( + Logger.verbose( "Calculating fee for $amountSats sats to $address, UTXOs=${utxosToSpend?.size}, satsPerVByte=$satsPerVByte" ) @@ -686,7 +674,7 @@ class LightningService @Inject constructor( feeRate = convertVByteToKwu(satsPerVByte), utxosToSpend = utxosToSpend, ) - Logger.info("Calculated fee=$fee for $amountSats sats to $address, satsPerVByte=$satsPerVByte") + Logger.verbose("Calculated fee=$fee for $amountSats sats to $address, satsPerVByte=$satsPerVByte") fee } catch (e: NodeException) { throw LdkError(e) diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index 7483d6faa..705e1426b 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -10,6 +10,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf @@ -21,7 +22,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.core.net.toUri -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner @@ -36,6 +37,8 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import androidx.navigation.toRoute +import dev.chrisbanes.haze.hazeSource +import dev.chrisbanes.haze.rememberHazeState import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.serialization.Serializable @@ -86,7 +89,9 @@ import to.bitkit.ui.screens.transfer.external.ExternalFeeCustomScreen import to.bitkit.ui.screens.transfer.external.ExternalNodeViewModel import to.bitkit.ui.screens.transfer.external.ExternalSuccessScreen import to.bitkit.ui.screens.transfer.external.LnurlChannelScreen -import to.bitkit.ui.screens.wallets.HomeNav +import to.bitkit.ui.screens.wallets.HomeScreen +import to.bitkit.ui.screens.wallets.SavingsWalletScreen +import to.bitkit.ui.screens.wallets.SpendingWalletScreen import to.bitkit.ui.screens.wallets.activity.ActivityDetailScreen import to.bitkit.ui.screens.wallets.activity.ActivityExploreScreen import to.bitkit.ui.screens.wallets.activity.AllActivityScreen @@ -166,6 +171,7 @@ import to.bitkit.ui.sheets.SendSheet import to.bitkit.ui.sheets.UpdateSheet import to.bitkit.ui.theme.TRANSITION_SHEET_MS import to.bitkit.ui.utils.AutoReadClipboardHandler +import to.bitkit.ui.utils.RequestNotificationPermissions import to.bitkit.ui.utils.Transitions import to.bitkit.ui.utils.composableWithDefaultTransitions import to.bitkit.ui.utils.navigationWithDefaultTransitions @@ -195,6 +201,7 @@ fun ContentView( modifier: Modifier = Modifier, ) { val navController = rememberNavController() + val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) val context = LocalContext.current val lifecycle = LocalLifecycleOwner.current.lifecycle @@ -494,7 +501,7 @@ private fun RootNavHost( activityListViewModel = activityListViewModel, settingsViewModel = settingsViewModel, navController = navController, - drawerState = drawerState + drawerState = drawerState, ) allActivity( activityListViewModel = activityListViewModel, @@ -541,6 +548,7 @@ private fun RootNavHost( navController.navigateToTransferFunding() settingsViewModel.setHasSeenTransferIntro(true) }, + onBackClick = {}, ) } composableWithDefaultTransitions { @@ -630,7 +638,9 @@ private fun RootNavHost( composableWithDefaultTransitions { SettingUpScreen( viewModel = transferViewModel, - onContinueClick = { navController.popBackStack(inclusive = true) }, + onContinueClick = { + navController.navigateToHome() + } ) } composableWithDefaultTransitions { @@ -744,13 +754,77 @@ private fun NavGraphBuilder.home( drawerState: DrawerState, ) { composable { - HomeNav( - walletViewModel = walletViewModel, - appViewModel = appViewModel, - activityListViewModel = activityListViewModel, - settingsViewModel = settingsViewModel, - rootNavController = navController, - drawerState = drawerState, + val uiState by walletViewModel.uiState.collectAsStateWithLifecycle() + val hazeState = rememberHazeState() + + RequestNotificationPermissions( + onPermissionChange = { granted -> + settingsViewModel.setNotificationPreference(granted) + } + ) + Box( + modifier = Modifier + .fillMaxSize() + .hazeSource(hazeState) + ) { + HomeScreen( + mainUiState = uiState, + drawerState = drawerState, + rootNavController = navController, + walletNavController = navController, + settingsViewModel = settingsViewModel, + walletViewModel = walletViewModel, + appViewModel = appViewModel, + activityListViewModel = activityListViewModel, + ) + } + } + composable( + enterTransition = { Transitions.slideInHorizontally }, + exitTransition = { Transitions.slideOutHorizontally }, + ) { + val hasSeenSpendingIntro by settingsViewModel.hasSeenSpendingIntro.collectAsStateWithLifecycle() + val isGeoBlocked by appViewModel.isGeoBlocked.collectAsStateWithLifecycle() + val onchainActivities by activityListViewModel.onchainActivities.collectAsStateWithLifecycle() + + SavingsWalletScreen( + isGeoBlocked = isGeoBlocked, + onchainActivities = onchainActivities.orEmpty(), + onAllActivityButtonClick = { navController.navigateToAllActivity() }, + onActivityItemClick = { navController.navigateToActivityItem(it) }, + onEmptyActivityRowClick = { appViewModel.showSheet(Sheet.Receive) }, + onTransferToSpendingClick = { + if (!hasSeenSpendingIntro) { + navController.navigateToTransferSpendingIntro() + } else { + navController.navigateToTransferSpendingAmount() + } + }, + onBackClick = { navController.popBackStack() }, + ) + } + composable( + enterTransition = { Transitions.slideInHorizontally }, + exitTransition = { Transitions.slideOutHorizontally }, + ) { + val hasSeenSavingsIntro by settingsViewModel.hasSeenSavingsIntro.collectAsStateWithLifecycle() + val uiState by walletViewModel.uiState.collectAsStateWithLifecycle() + val lightningActivities by activityListViewModel.lightningActivities.collectAsStateWithLifecycle() + + SpendingWalletScreen( + uiState = uiState, + lightningActivities = lightningActivities.orEmpty(), + onAllActivityButtonClick = { navController.navigateToAllActivity() }, + onActivityItemClick = { navController.navigateToActivityItem(it) }, + onEmptyActivityRowClick = { appViewModel.showSheet(Sheet.Receive) }, + onTransferToSavingsClick = { + if (!hasSeenSavingsIntro) { + navController.navigateToTransferSavingsIntro() + } else { + navController.navigateToTransferSavingsAvailability() + } + }, + onBackClick = { navController.popBackStack() }, ) } } @@ -818,7 +892,8 @@ private fun NavGraphBuilder.profile( onContinue = { settingsViewModel.setHasSeenProfileIntro(true) navController.navigate(Routes.CreateProfile) - } + }, + onBackClick = { navController.popBackStack() } ) } composableWithDefaultTransitions { @@ -838,6 +913,9 @@ private fun NavGraphBuilder.shop( onContinue = { settingsViewModel.setHasSeenShopIntro(true) navController.navigate(Routes.ShopDiscover) + }, + onBackClick = { + navController.popBackStack() } ) } @@ -1227,7 +1305,8 @@ private fun NavGraphBuilder.widgets( onContinue = { settingsViewModel.setHasSeenWidgetsIntro(true) navController.navigate(Routes.AddWidget) - } + }, + onBackClick = {}, ) } composableWithDefaultTransitions { @@ -1243,6 +1322,7 @@ private fun NavGraphBuilder.widgets( } }, fiatSymbol = LocalCurrencies.current.currencySymbol, + onBackCLick = { navController.popBackStack() } ) } composableWithDefaultTransitions { @@ -1409,10 +1489,6 @@ inline fun NavController.navigateIfNotCurrent(route: T) { } } -fun NavController.navigateToSettings() = navigate( - route = Routes.Settings, -) - fun NavController.navigateToGeneralSettings() = navigate( route = Routes.GeneralSettings, ) @@ -1557,10 +1633,17 @@ fun NavController.navigateToAboutSettings() = navigate( ) // endregion +@Stable sealed interface Routes { @Serializable data object Home : Routes + @Serializable + data object Savings : Routes + + @Serializable + data object Spending : Routes + @Serializable data object Settings : Routes diff --git a/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt b/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt index 8fae03b0f..17c594fbe 100644 --- a/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt +++ b/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt @@ -30,7 +30,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController -import kotlinx.datetime.Clock import org.lightningdevkit.ldknode.BalanceDetails import org.lightningdevkit.ldknode.BalanceSource import org.lightningdevkit.ldknode.BestBlock @@ -62,12 +61,14 @@ import to.bitkit.ui.components.settings.SettingsTextButtonRow import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn -import to.bitkit.ui.shared.util.clickableAlpha +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.copyToClipboard import to.bitkit.ui.utils.withAccent import to.bitkit.viewmodels.MainUiState +import kotlin.time.Clock.System.now +import kotlin.time.ExperimentalTime @Composable fun NodeInfoScreen( @@ -463,11 +464,12 @@ private fun Preview() { } } +@OptIn(ExperimentalTime::class) @Preview(showSystemUi = true) @Composable private fun PreviewDevMode() { AppThemeSurface { - val syncTime = Clock.System.now().epochSeconds.toULong() + val syncTime = now().epochSeconds.toULong() Content( isDevModeEnabled = true, uiState = MainUiState( diff --git a/app/src/main/java/to/bitkit/ui/components/ActivityBanner.kt b/app/src/main/java/to/bitkit/ui/components/ActivityBanner.kt index 70d2d330f..00d35fbee 100644 --- a/app/src/main/java/to/bitkit/ui/components/ActivityBanner.kt +++ b/app/src/main/java/to/bitkit/ui/components/ActivityBanner.kt @@ -35,7 +35,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import to.bitkit.R import to.bitkit.models.ActivityBannerType -import to.bitkit.ui.shared.util.clickableAlpha +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.shared.util.outerGlow import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors diff --git a/app/src/main/java/to/bitkit/ui/components/AppStatus.kt b/app/src/main/java/to/bitkit/ui/components/AppStatus.kt index 15452402a..9e5ada2d5 100644 --- a/app/src/main/java/to/bitkit/ui/components/AppStatus.kt +++ b/app/src/main/java/to/bitkit/ui/components/AppStatus.kt @@ -29,7 +29,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R import to.bitkit.models.HealthState import to.bitkit.ui.appViewModel -import to.bitkit.ui.shared.util.clickableAlpha +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors diff --git a/app/src/main/java/to/bitkit/ui/components/AuthCheckView.kt b/app/src/main/java/to/bitkit/ui/components/AuthCheckView.kt index a788a44fb..848ae94fa 100644 --- a/app/src/main/java/to/bitkit/ui/components/AuthCheckView.kt +++ b/app/src/main/java/to/bitkit/ui/components/AuthCheckView.kt @@ -31,8 +31,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R import to.bitkit.env.Env import to.bitkit.ui.scaffold.AppTopBar +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.shared.util.blockPointerInputPassthrough -import to.bitkit.ui.shared.util.clickableAlpha import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.rememberBiometricAuthSupported diff --git a/app/src/main/java/to/bitkit/ui/components/BalanceHeaderView.kt b/app/src/main/java/to/bitkit/ui/components/BalanceHeaderView.kt index 1cb9905a4..0039fff56 100644 --- a/app/src/main/java/to/bitkit/ui/components/BalanceHeaderView.kt +++ b/app/src/main/java/to/bitkit/ui/components/BalanceHeaderView.kt @@ -29,8 +29,8 @@ import to.bitkit.ui.currencyViewModel import to.bitkit.ui.settingsViewModel import to.bitkit.ui.shared.UiConstants import to.bitkit.ui.shared.animations.BalanceAnimations +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.shared.modifiers.swipeToHide -import to.bitkit.ui.shared.util.clickableAlpha import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors diff --git a/app/src/main/java/to/bitkit/ui/components/BiometricsView.kt b/app/src/main/java/to/bitkit/ui/components/BiometricsView.kt index 8211643d4..d9ddebe48 100644 --- a/app/src/main/java/to/bitkit/ui/components/BiometricsView.kt +++ b/app/src/main/java/to/bitkit/ui/components/BiometricsView.kt @@ -23,7 +23,7 @@ import androidx.compose.ui.unit.dp import kotlinx.coroutines.delay import kotlinx.coroutines.launch import to.bitkit.R -import to.bitkit.ui.shared.util.clickableAlpha +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.utils.BiometricPrompt diff --git a/app/src/main/java/to/bitkit/ui/components/BottomSheet.kt b/app/src/main/java/to/bitkit/ui/components/BottomSheet.kt index 1ea07354b..eaf26b420 100644 --- a/app/src/main/java/to/bitkit/ui/components/BottomSheet.kt +++ b/app/src/main/java/to/bitkit/ui/components/BottomSheet.kt @@ -23,7 +23,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.tooling.preview.Preview @@ -86,14 +85,14 @@ fun BottomSheetPreview( modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit, ) { - val density = LocalDensity.current BottomSheet( onDismissRequest = {}, sheetState = remember { SheetState( skipPartiallyExpanded = true, - density = density, initialValue = SheetValue.Expanded, + positionalThreshold = { 0f }, + velocityThreshold = { 0f }, ) }, modifier = modifier, diff --git a/app/src/main/java/to/bitkit/ui/components/Button.kt b/app/src/main/java/to/bitkit/ui/components/Button.kt index f18aecdc4..6da684202 100644 --- a/app/src/main/java/to/bitkit/ui/components/Button.kt +++ b/app/src/main/java/to/bitkit/ui/components/Button.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import to.bitkit.ui.shared.modifiers.alphaFeedback import to.bitkit.ui.shared.util.primaryButtonStyle import to.bitkit.ui.theme.AppButtonDefaults import to.bitkit.ui.theme.AppThemeSurface @@ -72,57 +73,49 @@ fun PrimaryButton( containerColor = Color.Transparent, disabledContainerColor = Color.Transparent ), - contentPadding = PaddingValues(0.dp), + contentPadding = contentPadding, shape = buttonShape, - modifier = Modifier + modifier = modifier .then(if (fullWidth) Modifier.fillMaxWidth() else Modifier) .requiredHeight(size.height) - .then(modifier) + .primaryButtonStyle( + isEnabled = enabled && !isLoading, + shape = buttonShape, + primaryColor = color + ) + .alphaFeedback(enabled = enabled && !isLoading) ) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .then(if (fullWidth) Modifier.fillMaxWidth() else Modifier) - .requiredHeight(size.height) - .primaryButtonStyle( - isEnabled = enabled && !isLoading, - shape = buttonShape, - primaryColor = color - ) - .padding(contentPadding) - ) { - if (isLoading) { - CircularProgressIndicator( - color = Colors.White32, - strokeWidth = 2.dp, - modifier = Modifier.size(size.height / 2) - ) - } else { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - if (icon != null) { - Box( - modifier = if (enabled) { - Modifier - } else { - Modifier.graphicsLayer { - colorFilter = ColorFilter.tint(Colors.White32) - } + if (isLoading) { + CircularProgressIndicator( + color = Colors.White32, + strokeWidth = 2.dp, + modifier = Modifier.size(size.height / 2) + ) + } else { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + if (icon != null) { + Box( + modifier = if (enabled) { + Modifier + } else { + Modifier.graphicsLayer { + colorFilter = ColorFilter.tint(Colors.White32) } - ) { - icon() } - } - text?.let { - Text( - text = text, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) + ) { + icon() } } + text?.let { + Text( + text = text, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } } } } @@ -147,10 +140,9 @@ fun SecondaryButton( colors = AppButtonDefaults.secondaryColors, contentPadding = contentPadding, border = border, - modifier = Modifier + modifier = modifier .then(if (fullWidth) Modifier.fillMaxWidth() else Modifier) .requiredHeight(size.height) - .then(modifier) ) { if (isLoading) { CircularProgressIndicator( @@ -205,10 +197,9 @@ fun TertiaryButton( enabled = enabled && !isLoading, colors = AppButtonDefaults.tertiaryColors, contentPadding = contentPadding, - modifier = Modifier + modifier = modifier .then(if (fullWidth) Modifier.fillMaxWidth() else Modifier) .requiredHeight(size.height) - .then(modifier) ) { if (isLoading) { CircularProgressIndicator( @@ -258,6 +249,11 @@ private fun PrimaryButtonPreview() { text = "Primary", onClick = {}, ) + PrimaryButton( + text = "Primary with padding", + modifier = Modifier.padding(horizontal = 32.dp), + onClick = {}, + ) PrimaryButton( text = "Primary With Icon", onClick = {}, @@ -292,6 +288,25 @@ private fun PrimaryButtonPreview() { size = ButtonSize.Small, onClick = {}, ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + PrimaryButton( + text = "Primary Small", + fullWidth = false, + size = ButtonSize.Small, + modifier = Modifier.weight(1f), + onClick = {}, + ) + PrimaryButton( + text = "Primary Small", + fullWidth = false, + size = ButtonSize.Small, + modifier = Modifier.weight(1f), + onClick = {}, + ) + } PrimaryButton( text = "Primary Small Color Not Full", size = ButtonSize.Small, @@ -360,6 +375,11 @@ private fun SecondaryButtonPreview() { text = "Secondary", onClick = {}, ) + SecondaryButton( + text = "Secondary With padding", + modifier = Modifier.padding(horizontal = 32.dp), + onClick = {}, + ) SecondaryButton( text = "Secondary With Icon", onClick = {}, diff --git a/app/src/main/java/to/bitkit/ui/components/DrawerMenu.kt b/app/src/main/java/to/bitkit/ui/components/DrawerMenu.kt index 7c3c31822..0071996ac 100644 --- a/app/src/main/java/to/bitkit/ui/components/DrawerMenu.kt +++ b/app/src/main/java/to/bitkit/ui/components/DrawerMenu.kt @@ -46,8 +46,8 @@ import to.bitkit.R import to.bitkit.ui.Routes import to.bitkit.ui.navigateIfNotCurrent import to.bitkit.ui.navigateToHome +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.shared.util.blockPointerInputPassthrough -import to.bitkit.ui.shared.util.clickableAlpha import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.theme.InterFontFamily @@ -138,11 +138,8 @@ private fun Menu( label = stringResource(R.string.wallet__drawer__wallet), iconRes = R.drawable.ic_coins, onClick = { - val isOnHome = rootNavController.currentBackStackEntry - ?.destination?.hasRoute() ?: false - if (!isOnHome) { - rootNavController.navigateToHome() - } + val isInHome = rootNavController.currentBackStackEntry?.destination?.hasRoute() ?: false + if (!isInHome) rootNavController.navigateToHome() scope.launch { drawerState.close() } }, modifier = Modifier.testTag("DrawerWallet") diff --git a/app/src/main/java/to/bitkit/ui/components/Money.kt b/app/src/main/java/to/bitkit/ui/components/Money.kt index 899e377b8..cc863d7a1 100644 --- a/app/src/main/java/to/bitkit/ui/components/Money.kt +++ b/app/src/main/java/to/bitkit/ui/components/Money.kt @@ -16,7 +16,7 @@ import to.bitkit.models.formatToModernDisplay import to.bitkit.repositories.CurrencyState import to.bitkit.ui.LocalCurrencies import to.bitkit.ui.currencyViewModel -import to.bitkit.ui.shared.util.clickableAlpha +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.withAccent import java.math.BigDecimal diff --git a/app/src/main/java/to/bitkit/ui/components/NumberPad.kt b/app/src/main/java/to/bitkit/ui/components/NumberPad.kt index 636e47df5..fba65ef8e 100644 --- a/app/src/main/java/to/bitkit/ui/components/NumberPad.kt +++ b/app/src/main/java/to/bitkit/ui/components/NumberPad.kt @@ -33,7 +33,7 @@ import to.bitkit.models.PrimaryDisplay import to.bitkit.repositories.CurrencyState import to.bitkit.ui.LocalCurrencies import to.bitkit.ui.scaffold.ScreenColumn -import to.bitkit.ui.shared.util.clickableAlpha +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.viewmodels.AmountInputViewModel diff --git a/app/src/main/java/to/bitkit/ui/components/NumberPadActionButton.kt b/app/src/main/java/to/bitkit/ui/components/NumberPadActionButton.kt index 9fde35af7..f0691860b 100644 --- a/app/src/main/java/to/bitkit/ui/components/NumberPadActionButton.kt +++ b/app/src/main/java/to/bitkit/ui/components/NumberPadActionButton.kt @@ -2,15 +2,17 @@ package to.bitkit.ui.components import androidx.annotation.DrawableRes import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredHeight import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button import androidx.compose.material3.Icon -import androidx.compose.material3.Surface +import androidx.compose.material3.OutlinedButton import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -19,7 +21,9 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import to.bitkit.R -import to.bitkit.ui.theme.AppShapes +import to.bitkit.ui.shared.modifiers.alphaFeedback +import to.bitkit.ui.shared.util.primaryButtonStyle +import to.bitkit.ui.theme.AppButtonDefaults import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @@ -32,36 +36,72 @@ fun NumberPadActionButton( enabled: Boolean = true, @DrawableRes icon: Int? = null, ) { - val borderColor = if (enabled) Color.Transparent else color - val bgColor = if (enabled) Colors.White10 else Color.Transparent + val contentPadding = PaddingValues(horizontal = 8.dp, vertical = 5.dp) + val height = 28.dp + val buttonShape = RoundedCornerShape(8.dp) - Surface( - color = bgColor, - shape = AppShapes.small, - border = BorderStroke(1.dp, borderColor), - modifier = modifier - .clickable( - enabled = enabled, - onClick = onClick, - ) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.padding(PaddingValues(8.dp, 5.dp)) + if (enabled) { + Button( + onClick = onClick, + colors = AppButtonDefaults.primaryColors.copy( + containerColor = Color.Transparent, + disabledContainerColor = Color.Transparent + ), + contentPadding = contentPadding, + shape = buttonShape, + modifier = modifier + .requiredHeight(height) + .primaryButtonStyle( + isEnabled = true, + shape = buttonShape, + ) + .alphaFeedback(enabled = enabled) ) { - if (icon != null) { - Icon( - painter = painterResource(icon), - contentDescription = text, - tint = color, - modifier = Modifier.size(16.dp) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + if (icon != null) { + Icon( + painter = painterResource(icon), + contentDescription = text, + tint = color, + modifier = Modifier.size(16.dp) + ) + } + Caption13Up( + text = text, + color = color, + ) + } + } + } else { + OutlinedButton( + onClick = onClick, + shape = buttonShape, + colors = AppButtonDefaults.secondaryColors, + contentPadding = contentPadding, + border = BorderStroke(width = 1.dp, color = color), + modifier = modifier + .requiredHeight(height) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + if (icon != null) { + Icon( + painter = painterResource(icon), + contentDescription = text, + tint = color, + modifier = Modifier.size(16.dp) + ) + } + Caption13Up( + text = text, + color = color, ) } - Caption13Up( - text = text, - color = color, - ) } } } diff --git a/app/src/main/java/to/bitkit/ui/components/NumberPadTextField.kt b/app/src/main/java/to/bitkit/ui/components/NumberPadTextField.kt index 7f1cb0257..add65817c 100644 --- a/app/src/main/java/to/bitkit/ui/components/NumberPadTextField.kt +++ b/app/src/main/java/to/bitkit/ui/components/NumberPadTextField.kt @@ -22,7 +22,7 @@ import to.bitkit.models.USD_SYMBOL import to.bitkit.models.formatToModernDisplay import to.bitkit.repositories.CurrencyState import to.bitkit.ui.LocalCurrencies -import to.bitkit.ui.shared.util.clickableAlpha +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.viewmodels.AmountInputUiState diff --git a/app/src/main/java/to/bitkit/ui/components/SearchInput.kt b/app/src/main/java/to/bitkit/ui/components/SearchInput.kt index de619e885..8e3672eab 100644 --- a/app/src/main/java/to/bitkit/ui/components/SearchInput.kt +++ b/app/src/main/java/to/bitkit/ui/components/SearchInput.kt @@ -27,7 +27,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import to.bitkit.R -import to.bitkit.ui.shared.util.clickableAlpha +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.AppTextFieldDefaults import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors diff --git a/app/src/main/java/to/bitkit/ui/components/SuggestionCard.kt b/app/src/main/java/to/bitkit/ui/components/SuggestionCard.kt index 92aea1ddc..8c1a36fb8 100644 --- a/app/src/main/java/to/bitkit/ui/components/SuggestionCard.kt +++ b/app/src/main/java/to/bitkit/ui/components/SuggestionCard.kt @@ -38,7 +38,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import to.bitkit.R import to.bitkit.models.Suggestion -import to.bitkit.ui.shared.util.clickableAlpha +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.Colors diff --git a/app/src/main/java/to/bitkit/ui/components/TabBar.kt b/app/src/main/java/to/bitkit/ui/components/TabBar.kt index 0a1de06ba..fd629f34e 100644 --- a/app/src/main/java/to/bitkit/ui/components/TabBar.kt +++ b/app/src/main/java/to/bitkit/ui/components/TabBar.kt @@ -28,6 +28,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.draw.shadow import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.Stroke @@ -40,7 +41,7 @@ import dev.chrisbanes.haze.hazeSource import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.rememberHazeState import to.bitkit.R -import to.bitkit.ui.shared.util.clickableAlpha +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.shared.util.primaryButtonStyle import to.bitkit.ui.theme.AppThemeSurface @@ -175,7 +176,7 @@ fun BoxScope.TabBar( radius = (size.width - borderWidth) / 2, center = Offset(size.width / 2, size.height / 2), style = Stroke(width = borderWidth), - blendMode = androidx.compose.ui.graphics.BlendMode.DstIn + blendMode = BlendMode.DstIn ) } .clickableAlpha { onScanClick() } diff --git a/app/src/main/java/to/bitkit/ui/components/Tag.kt b/app/src/main/java/to/bitkit/ui/components/Tag.kt index 50c12db3a..cf6784141 100644 --- a/app/src/main/java/to/bitkit/ui/components/Tag.kt +++ b/app/src/main/java/to/bitkit/ui/components/Tag.kt @@ -18,7 +18,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import to.bitkit.R -import to.bitkit.ui.shared.util.clickableAlpha +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.AppShapes import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors diff --git a/app/src/main/java/to/bitkit/ui/components/Tooltip.kt b/app/src/main/java/to/bitkit/ui/components/Tooltip.kt index dfd0425d8..49547f262 100644 --- a/app/src/main/java/to/bitkit/ui/components/Tooltip.kt +++ b/app/src/main/java/to/bitkit/ui/components/Tooltip.kt @@ -4,14 +4,17 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.PlainTooltip +import androidx.compose.material3.TooltipAnchorPosition import androidx.compose.material3.TooltipBox import androidx.compose.material3.TooltipDefaults +import androidx.compose.material3.TooltipDefaults.rememberTooltipPositionProvider import androidx.compose.material3.TooltipState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.PopupPositionProvider import to.bitkit.ui.theme.Colors @OptIn(ExperimentalMaterial3Api::class) @@ -20,16 +23,20 @@ fun Tooltip( text: String, tooltipState: TooltipState, modifier: Modifier = Modifier, - content: @Composable () -> Unit + positionProvider: PopupPositionProvider = rememberTooltipPositionProvider(TooltipAnchorPosition.Above, 4.dp), + focusable: Boolean = false, + content: @Composable () -> Unit, ) { TooltipBox( modifier = modifier, - positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), + positionProvider = positionProvider, tooltip = { PlainTooltip( - caretSize = DpSize( - width = 16.dp, - height = 12.dp + caretShape = TooltipDefaults.caretShape( + DpSize( + width = 16.dp, + height = 12.dp + ) ), containerColor = Colors.Black92, contentColor = Colors.White, @@ -48,7 +55,7 @@ fun Tooltip( } }, state = tooltipState, - focusable = false, - content = content + focusable = focusable, + content = content, ) } diff --git a/app/src/main/java/to/bitkit/ui/components/settings/SettingsButtonRow.kt b/app/src/main/java/to/bitkit/ui/components/settings/SettingsButtonRow.kt index ae6cbd5d7..cf2abc86a 100644 --- a/app/src/main/java/to/bitkit/ui/components/settings/SettingsButtonRow.kt +++ b/app/src/main/java/to/bitkit/ui/components/settings/SettingsButtonRow.kt @@ -30,7 +30,7 @@ import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BodyMSB import to.bitkit.ui.components.BodyS import to.bitkit.ui.components.BodySSB -import to.bitkit.ui.shared.util.clickableAlpha +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors diff --git a/app/src/main/java/to/bitkit/ui/components/settings/SettingsSwitchRow.kt b/app/src/main/java/to/bitkit/ui/components/settings/SettingsSwitchRow.kt index ed1946ca9..194057284 100644 --- a/app/src/main/java/to/bitkit/ui/components/settings/SettingsSwitchRow.kt +++ b/app/src/main/java/to/bitkit/ui/components/settings/SettingsSwitchRow.kt @@ -15,7 +15,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import to.bitkit.ui.components.BodyM -import to.bitkit.ui.shared.util.clickableAlpha +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.AppSwitchDefaults import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors diff --git a/app/src/main/java/to/bitkit/ui/components/settings/SettingsTextButtonRow.kt b/app/src/main/java/to/bitkit/ui/components/settings/SettingsTextButtonRow.kt index 67a80285f..7eb66a4e6 100644 --- a/app/src/main/java/to/bitkit/ui/components/settings/SettingsTextButtonRow.kt +++ b/app/src/main/java/to/bitkit/ui/components/settings/SettingsTextButtonRow.kt @@ -24,7 +24,7 @@ import androidx.compose.ui.unit.dp import to.bitkit.R import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.Caption -import to.bitkit.ui.shared.util.clickableAlpha +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.shared.util.screen import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors diff --git a/app/src/main/java/to/bitkit/ui/onboarding/RestoreWalletScreen.kt b/app/src/main/java/to/bitkit/ui/onboarding/RestoreWalletScreen.kt index b30b5183c..aece81cf7 100644 --- a/app/src/main/java/to/bitkit/ui/onboarding/RestoreWalletScreen.kt +++ b/app/src/main/java/to/bitkit/ui/onboarding/RestoreWalletScreen.kt @@ -53,7 +53,7 @@ import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R import to.bitkit.ui.components.BodyM diff --git a/app/src/main/java/to/bitkit/ui/screens/profile/ProfileIntroScreen.kt b/app/src/main/java/to/bitkit/ui/screens/profile/ProfileIntroScreen.kt index 03126789c..27fe39a66 100644 --- a/app/src/main/java/to/bitkit/ui/screens/profile/ProfileIntroScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/profile/ProfileIntroScreen.kt @@ -26,11 +26,12 @@ import to.bitkit.ui.utils.withAccent @Composable fun ProfileIntroScreen( onContinue: () -> Unit, + onBackClick: () -> Unit, ) { ScreenColumn { AppTopBar( titleText = stringResource(R.string.slashtags__profile), - onBackClick = null, + onBackClick = onBackClick, actions = { DrawerNavIcon() }, ) @@ -68,7 +69,8 @@ fun ProfileIntroScreen( private fun Preview() { AppThemeSurface { ProfileIntroScreen( - onContinue = {} + onContinue = {}, + onBackClick = {} ) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicScreen.kt b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicScreen.kt index a28860e06..47282b6b5 100644 --- a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicScreen.kt @@ -20,7 +20,7 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import to.bitkit.R import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.FillHeight diff --git a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryModeScreen.kt b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryModeScreen.kt index 77de8c66b..4051a6fdc 100644 --- a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryModeScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryModeScreen.kt @@ -17,7 +17,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import to.bitkit.R import to.bitkit.ui.components.AuthCheckView import to.bitkit.ui.components.BodyM diff --git a/app/src/main/java/to/bitkit/ui/screens/settings/DevSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/screens/settings/DevSettingsScreen.kt index 41aed839a..455136253 100644 --- a/app/src/main/java/to/bitkit/ui/screens/settings/DevSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/settings/DevSettingsScreen.kt @@ -9,7 +9,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.navigation.NavController import org.lightningdevkit.ldknode.Network import to.bitkit.R diff --git a/app/src/main/java/to/bitkit/ui/screens/settings/FeeSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/screens/settings/FeeSettingsScreen.kt index d7fee8bc0..34cb37e89 100644 --- a/app/src/main/java/to/bitkit/ui/screens/settings/FeeSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/settings/FeeSettingsScreen.kt @@ -11,7 +11,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import com.synonym.bitkitcore.FeeRates diff --git a/app/src/main/java/to/bitkit/ui/screens/shop/ShopIntroScreen.kt b/app/src/main/java/to/bitkit/ui/screens/shop/ShopIntroScreen.kt index 269865df5..17fa1ea7e 100644 --- a/app/src/main/java/to/bitkit/ui/screens/shop/ShopIntroScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/shop/ShopIntroScreen.kt @@ -26,11 +26,12 @@ import to.bitkit.ui.utils.withAccent @Composable fun ShopIntroScreen( onContinue: () -> Unit, + onBackClick: () -> Unit, ) { ScreenColumn { AppTopBar( titleText = "", - onBackClick = null, + onBackClick = onBackClick, actions = { DrawerNavIcon() }, ) @@ -66,7 +67,8 @@ fun ShopIntroScreen( private fun Preview() { AppThemeSurface { ShopIntroScreen( - onContinue = {} + onContinue = {}, + onBackClick = {}, ) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/shop/shopDiscover/ShopDiscoverScreen.kt b/app/src/main/java/to/bitkit/ui/screens/shop/shopDiscover/ShopDiscoverScreen.kt index 4184c6c05..8517a76eb 100644 --- a/app/src/main/java/to/bitkit/ui/screens/shop/shopDiscover/ShopDiscoverScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/shop/shopDiscover/ShopDiscoverScreen.kt @@ -49,7 +49,7 @@ import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn -import to.bitkit.ui.shared.util.clickableAlpha +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt index 8b4af25e7..7e92fad64 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt @@ -22,7 +22,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Devices.NEXUS_5 import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R import to.bitkit.ext.mockOrder diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAmountScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAmountScreen.kt index ec32369c8..d313421c2 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAmountScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAmountScreen.kt @@ -18,7 +18,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Devices.NEXUS_5 import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R import to.bitkit.repositories.CurrencyState diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingConfirmScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingConfirmScreen.kt index 0432775b9..dd2c4cc60 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingConfirmScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingConfirmScreen.kt @@ -27,7 +27,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.synonym.bitkitcore.BtBolt11InvoiceState import com.synonym.bitkitcore.BtOrderState diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/TransferIntroScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/TransferIntroScreen.kt index 2c0c514f0..45192970b 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/TransferIntroScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/TransferIntroScreen.kt @@ -33,6 +33,7 @@ import to.bitkit.ui.utils.withAccent @Composable fun TransferIntroScreen( onContinueClick: () -> Unit = {}, + onBackClick: () -> Unit, ) { Box( contentAlignment = Alignment.TopCenter, @@ -51,7 +52,7 @@ fun TransferIntroScreen( ) AppTopBar( titleText = null, - onBackClick = null, + onBackClick = onBackClick, actions = { DrawerNavIcon() }, ) Column( @@ -79,6 +80,6 @@ fun TransferIntroScreen( @Composable private fun Preview() { AppThemeSurface { - TransferIntroScreen() + TransferIntroScreen(onBackClick = {}) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalAmountScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalAmountScreen.kt index cd321f96c..d960b32db 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalAmountScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalAmountScreen.kt @@ -19,7 +19,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Devices.NEXUS_5 import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R import to.bitkit.repositories.CurrencyState diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConfirmScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConfirmScreen.kt index 91f514e34..fbaae5a19 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConfirmScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConfirmScreen.kt @@ -41,7 +41,7 @@ import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.screens.transfer.external.ExternalNodeContract.SideEffect -import to.bitkit.ui.shared.util.clickableAlpha +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.withAccent diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelScreen.kt index 6329212e9..67b985d97 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelScreen.kt @@ -20,7 +20,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R import to.bitkit.env.Env diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeNav.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeNav.kt deleted file mode 100644 index 74d4507d3..000000000 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeNav.kt +++ /dev/null @@ -1,156 +0,0 @@ -package to.bitkit.ui.screens.wallets - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.DrawerState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController -import androidx.navigation.NavHostController -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import dev.chrisbanes.haze.hazeSource -import dev.chrisbanes.haze.rememberHazeState -import kotlinx.serialization.Serializable -import to.bitkit.ui.components.Sheet -import to.bitkit.ui.navigateToActivityItem -import to.bitkit.ui.navigateToAllActivity -import to.bitkit.ui.navigateToTransferSavingsAvailability -import to.bitkit.ui.navigateToTransferSavingsIntro -import to.bitkit.ui.navigateToTransferSpendingAmount -import to.bitkit.ui.navigateToTransferSpendingIntro -import to.bitkit.ui.utils.RequestNotificationPermissions -import to.bitkit.ui.utils.Transitions -import to.bitkit.viewmodels.ActivityListViewModel -import to.bitkit.viewmodels.AppViewModel -import to.bitkit.viewmodels.MainUiState -import to.bitkit.viewmodels.SettingsViewModel -import to.bitkit.viewmodels.WalletViewModel - -@Composable -fun HomeNav( - walletViewModel: WalletViewModel, - appViewModel: AppViewModel, - activityListViewModel: ActivityListViewModel, - settingsViewModel: SettingsViewModel, - rootNavController: NavController, - drawerState: DrawerState, -) { - val uiState: MainUiState by walletViewModel.uiState.collectAsStateWithLifecycle() - val hazeState = rememberHazeState() - - RequestNotificationPermissions( - onPermissionChange = { granted -> - settingsViewModel.setNotificationPreference(granted) - } - ) - - val walletNavController = rememberNavController() - - Box( - modifier = Modifier - .fillMaxSize() - .hazeSource(hazeState) - ) { - NavContent( - walletNavController = walletNavController, - rootNavController = rootNavController, - mainUiState = uiState, - drawerState = drawerState, - settingsViewModel = settingsViewModel, - appViewModel = appViewModel, - walletViewModel = walletViewModel, - activityListViewModel = activityListViewModel, - ) - } -} - -@Composable -private fun NavContent( - walletNavController: NavHostController, - rootNavController: NavController, - mainUiState: MainUiState, - drawerState: DrawerState, - settingsViewModel: SettingsViewModel, - appViewModel: AppViewModel, - walletViewModel: WalletViewModel, - activityListViewModel: ActivityListViewModel, -) { - NavHost( - navController = walletNavController, - startDestination = HomeRoutes.Home, - ) { - composable { - HomeScreen( - mainUiState = mainUiState, - drawerState = drawerState, - rootNavController = rootNavController, - walletNavController = walletNavController, - settingsViewModel = settingsViewModel, - walletViewModel = walletViewModel, - appViewModel = appViewModel, - activityListViewModel = activityListViewModel, - ) - } - composable( - enterTransition = { Transitions.slideInHorizontally }, - exitTransition = { Transitions.slideOutHorizontally }, - ) { - val hasSeenSpendingIntro by settingsViewModel.hasSeenSpendingIntro.collectAsStateWithLifecycle() - val isGeoBlocked by appViewModel.isGeoBlocked.collectAsStateWithLifecycle() - val onchainActivities by activityListViewModel.onchainActivities.collectAsStateWithLifecycle() - - SavingsWalletScreen( - isGeoBlocked = isGeoBlocked, - onchainActivities = onchainActivities.orEmpty(), - onAllActivityButtonClick = { rootNavController.navigateToAllActivity() }, - onActivityItemClick = { rootNavController.navigateToActivityItem(it) }, - onEmptyActivityRowClick = { appViewModel.showSheet(Sheet.Receive) }, - onTransferToSpendingClick = { - if (!hasSeenSpendingIntro) { - rootNavController.navigateToTransferSpendingIntro() - } else { - rootNavController.navigateToTransferSpendingAmount() - } - }, - onBackClick = { walletNavController.popBackStack() }, - ) - } - composable( - enterTransition = { Transitions.slideInHorizontally }, - exitTransition = { Transitions.slideOutHorizontally }, - ) { - val hasSeenSavingsIntro by settingsViewModel.hasSeenSavingsIntro.collectAsStateWithLifecycle() - val lightningActivities by activityListViewModel.lightningActivities.collectAsStateWithLifecycle() - SpendingWalletScreen( - uiState = mainUiState, - lightningActivities = lightningActivities.orEmpty(), - onAllActivityButtonClick = { rootNavController.navigateToAllActivity() }, - onActivityItemClick = { rootNavController.navigateToActivityItem(it) }, - onEmptyActivityRowClick = { appViewModel.showSheet(Sheet.Receive) }, - onTransferToSavingsClick = { - if (!hasSeenSavingsIntro) { - rootNavController.navigateToTransferSavingsIntro() - } else { - rootNavController.navigateToTransferSavingsAvailability() - } - }, - onBackClick = { walletNavController.popBackStack() }, - ) - } - } -} - -object HomeRoutes { - @Serializable - data object Home - - @Serializable - data object Savings - - @Serializable - data object Spending -} diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt index 876883ef2..5e8a6c64d 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt @@ -54,7 +54,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import androidx.navigation.NavHostController @@ -106,7 +106,7 @@ import to.bitkit.ui.screens.widgets.facts.FactsCard import to.bitkit.ui.screens.widgets.headlines.HeadlineCard import to.bitkit.ui.screens.widgets.price.PriceCard import to.bitkit.ui.screens.widgets.weather.WeatherCard -import to.bitkit.ui.shared.util.clickableAlpha +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.shared.util.shareText import to.bitkit.ui.sheets.BackupRoute import to.bitkit.ui.sheets.PinRoute @@ -359,7 +359,7 @@ private fun Content( sats = balances.totalOnchainSats.toLong(), icon = painterResource(id = R.drawable.ic_btc_circle), modifier = Modifier - .clickableAlpha { walletNavController.navigate(HomeRoutes.Savings) } + .clickableAlpha { walletNavController.navigate(Routes.Savings) } .padding(vertical = 4.dp) .testTag("ActivitySavings") ) @@ -370,7 +370,7 @@ private fun Content( sats = balances.totalLightningSats.toLong(), icon = painterResource(id = R.drawable.ic_ln_circle), modifier = Modifier - .clickableAlpha { walletNavController.navigate(HomeRoutes.Spending) } + .clickableAlpha { walletNavController.navigate(Routes.Spending) } .padding(vertical = 4.dp) .testTag("ActivitySpending") ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/SavingsWalletScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/SavingsWalletScreen.kt index 5b0befd82..d071dfe6d 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/SavingsWalletScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/SavingsWalletScreen.kt @@ -35,6 +35,7 @@ import to.bitkit.ui.components.IncomingTransfer import to.bitkit.ui.components.SecondaryButton import to.bitkit.ui.components.TabBar import to.bitkit.ui.scaffold.AppTopBar +import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.screens.wallets.activity.components.ActivityListGrouped import to.bitkit.ui.screens.wallets.activity.utils.previewOnchainActivityItems @@ -74,6 +75,7 @@ fun SavingsWalletScreen( contentScale = ContentScale.Fit, modifier = Modifier .align(Alignment.TopEnd) + .padding(top = 32.dp) .offset(x = (120).dp) .size(268.dp) ) @@ -82,6 +84,9 @@ fun SavingsWalletScreen( titleText = stringResource(R.string.wallet__savings__title), icon = painterResource(R.drawable.ic_btc_circle), onBackClick = onBackClick, + actions = { + DrawerNavIcon() + } ) Column( modifier = Modifier.padding(horizontal = 16.dp) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/SpendingWalletScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/SpendingWalletScreen.kt index d94c9289d..82460dca1 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/SpendingWalletScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/SpendingWalletScreen.kt @@ -36,6 +36,7 @@ import to.bitkit.ui.components.IncomingTransfer import to.bitkit.ui.components.SecondaryButton import to.bitkit.ui.components.TabBar import to.bitkit.ui.scaffold.AppTopBar +import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.screens.wallets.activity.components.ActivityListGrouped import to.bitkit.ui.screens.wallets.activity.utils.previewLightningActivityItems @@ -77,7 +78,7 @@ fun SpendingWalletScreen( contentScale = ContentScale.Fit, modifier = Modifier .align(Alignment.TopEnd) - .offset(x = (155).dp, y = (-35).dp) + .offset(x = (155).dp) .size(330.dp) ) ScreenColumn(noBackground = true) { @@ -85,6 +86,9 @@ fun SpendingWalletScreen( titleText = stringResource(R.string.wallet__spending__title), icon = painterResource(R.drawable.ic_ln_circle), onBackClick = onBackClick, + actions = { + DrawerNavIcon() + } ) Column( modifier = Modifier.padding(horizontal = 16.dp) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt index 7a737600e..ec593c539 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt @@ -39,7 +39,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.synonym.bitkitcore.Activity import com.synonym.bitkitcore.LightningActivity @@ -47,6 +47,7 @@ import com.synonym.bitkitcore.OnchainActivity import com.synonym.bitkitcore.PaymentState import com.synonym.bitkitcore.PaymentType import to.bitkit.R +import to.bitkit.ext.create import to.bitkit.ext.ellipsisMiddle import to.bitkit.ext.isSent import to.bitkit.ext.isTransfer @@ -70,8 +71,8 @@ import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.screens.wallets.activity.components.ActivityAddTagSheet import to.bitkit.ui.screens.wallets.activity.components.ActivityIcon +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.shared.modifiers.sheetHeight -import to.bitkit.ui.shared.util.clickableAlpha import to.bitkit.ui.sheets.BoostTransactionSheet import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @@ -824,18 +825,15 @@ private fun PreviewLightningSent() { AppThemeSurface { ActivityDetailContent( item = Activity.Lightning( - v1 = LightningActivity( + v1 = LightningActivity.create( id = "test-lightning-1", txType = PaymentType.SENT, status = PaymentState.SUCCEEDED, value = 50000UL, - fee = 1UL, invoice = "lnbc...", - message = "Thanks for paying at the bar. Here's my share.", timestamp = (System.currentTimeMillis() / 1000).toULong(), - preimage = null, - createdAt = null, - updatedAt = null, + fee = 1UL, + message = "Thanks for paying at the bar. Here's my share.", ) ), tags = listOf("Lunch", "Drinks"), @@ -855,25 +853,17 @@ private fun PreviewOnchain() { AppThemeSurface { ActivityDetailContent( item = Activity.Onchain( - v1 = OnchainActivity( + v1 = OnchainActivity.create( id = "test-onchain-1", txType = PaymentType.RECEIVED, txId = "abc123", value = 100000UL, fee = 500UL, - feeRate = 8UL, address = "bc1...", - confirmed = true, timestamp = (System.currentTimeMillis() / 1000 - 3600).toULong(), - isBoosted = false, - boostTxIds = emptyList(), - isTransfer = false, - doesExist = true, + confirmed = true, + feeRate = 8UL, confirmTimestamp = (System.currentTimeMillis() / 1000).toULong(), - channelId = null, - transferTxId = null, - createdAt = null, - updatedAt = null, ) ), tags = emptyList(), @@ -896,18 +886,15 @@ private fun PreviewSheetSmallScreen() { ) { ActivityDetailContent( item = Activity.Lightning( - v1 = LightningActivity( + v1 = LightningActivity.create( id = "test-lightning-1", txType = PaymentType.SENT, status = PaymentState.SUCCEEDED, value = 50000UL, - fee = 1UL, invoice = "lnbc...", - message = "Thanks for paying at the bar. Here's my share.", timestamp = (System.currentTimeMillis() / 1000).toULong(), - preimage = null, - createdAt = null, - updatedAt = null, + fee = 1UL, + message = "Thanks for paying at the bar. Here's my share.", ) ), tags = listOf("Lunch", "Drinks"), diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt index 4ceb770f3..fd7cdc3f6 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt @@ -32,7 +32,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.net.toUri -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.synonym.bitkitcore.Activity import com.synonym.bitkitcore.LightningActivity @@ -41,6 +41,7 @@ import com.synonym.bitkitcore.PaymentState import com.synonym.bitkitcore.PaymentType import org.lightningdevkit.ldknode.TransactionDetails import to.bitkit.R +import to.bitkit.ext.create import to.bitkit.ext.ellipsisMiddle import to.bitkit.ext.isSent import to.bitkit.ext.totalValue @@ -55,7 +56,7 @@ import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.screens.wallets.activity.components.ActivityIcon -import to.bitkit.ui.shared.util.clickableAlpha +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.copyToClipboard @@ -386,18 +387,16 @@ private fun PreviewLightning() { AppThemeSurface { ActivityExploreContent( item = Activity.Lightning( - v1 = LightningActivity( + v1 = LightningActivity.create( id = "test-lightning-1", txType = PaymentType.SENT, status = PaymentState.SUCCEEDED, value = 50000UL, - fee = 1UL, invoice = "lnbc...", - message = "Thanks for paying at the bar. Here's my share.", timestamp = (System.currentTimeMillis() / 1000).toULong(), + fee = 1UL, + message = "Thanks for paying at the bar. Here's my share.", preimage = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", - createdAt = null, - updatedAt = null, ), ), ) @@ -410,25 +409,17 @@ private fun PreviewOnchain() { AppThemeSurface { ActivityExploreContent( item = Activity.Onchain( - v1 = OnchainActivity( + v1 = OnchainActivity.create( id = "test-onchain-1", txType = PaymentType.RECEIVED, txId = "abc123", value = 100000UL, fee = 500UL, - feeRate = 8UL, address = "bc1...", - confirmed = true, timestamp = (System.currentTimeMillis() / 1000 - 3600).toULong(), - isBoosted = false, - boostTxIds = emptyList(), - isTransfer = false, - doesExist = true, + confirmed = true, + feeRate = 8UL, confirmTimestamp = (System.currentTimeMillis() / 1000).toULong(), - channelId = null, - transferTxId = null, - createdAt = null, - updatedAt = null, ), ), ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/DateRangeSelectorSheet.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/DateRangeSelectorSheet.kt index 757f6c9c7..6c47947fa 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/DateRangeSelectorSheet.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/DateRangeSelectorSheet.kt @@ -1,3 +1,5 @@ +@file:OptIn(ExperimentalTime::class) + package to.bitkit.ui.screens.wallets.activity import androidx.activity.compose.BackHandler @@ -47,11 +49,10 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch -import kotlinx.datetime.Clock -import kotlinx.datetime.Instant import kotlinx.datetime.LocalDate import kotlinx.datetime.TimeZone import kotlinx.datetime.atStartOfDayIn +import kotlinx.datetime.number import kotlinx.datetime.toLocalDateTime import to.bitkit.R import to.bitkit.ext.CalendarConstants @@ -73,8 +74,8 @@ import to.bitkit.ui.components.SecondaryButton import to.bitkit.ui.components.SheetSize import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.scaffold.SheetTopBar +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.shared.modifiers.sheetHeight -import to.bitkit.ui.shared.util.clickableAlpha import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @@ -83,7 +84,10 @@ import java.util.Calendar import java.util.Locale import kotlin.math.abs import kotlin.math.roundToInt +import kotlin.time.Clock.System.now import kotlin.time.Duration.Companion.days +import kotlin.time.ExperimentalTime +import kotlin.time.Instant // Animation private const val SWIPE_THRESHOLD_DP = 100 @@ -145,7 +149,7 @@ private fun Content( Instant.fromEpochMilliseconds(it) .toLocalDateTime(TimeZone.currentSystemDefault()) .date - } ?: Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date + } ?: now().toLocalDateTime(TimeZone.currentSystemDefault()).date ) } @@ -476,7 +480,7 @@ private fun CalendarGrid( } val today = remember { - Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date + now().toLocalDateTime(TimeZone.currentSystemDefault()).date } Column( @@ -532,7 +536,7 @@ private fun CalendarDayView( modifier = modifier .aspectRatio(DAY_ASPECT_RATIO) .clickableAlpha(onClick = onClick) - .testTag(if (isToday) "Today" else "Day-${date.dayOfMonth}") + .testTag(if (isToday) "Today" else "Day-${date.day}") ) { // Background for range selection if (isSelected) { @@ -616,7 +620,7 @@ private fun CalendarDayView( // Day number BodyMSB( - text = date.dayOfMonth.toString(), + text = date.day.toString(), color = if (isStartDate || isEndDate) Colors.Brand else Color.White ) } @@ -625,7 +629,7 @@ private fun CalendarDayView( private fun LocalDate.toFormattedString(): String { val formatter = SimpleDateFormat(DatePattern.DATE_FORMAT, Locale.getDefault()) val calendar = Calendar.getInstance() - calendar.set(year, monthNumber - CalendarConstants.MONTH_INDEX_OFFSET, dayOfMonth) + calendar.set(year, month.number - CalendarConstants.MONTH_INDEX_OFFSET, day) return formatter.format(calendar.time) } @@ -645,10 +649,10 @@ private fun PreviewWithSelection() { AppThemeSurface { BottomSheetPreview { Content( - initialStartDate = Clock.System.now() + initialStartDate = now() .minus(CalendarConstants.DAYS_IN_WEEK.days) .toEpochMilliseconds(), - initialEndDate = Clock.System.now().toEpochMilliseconds(), + initialEndDate = now().toEpochMilliseconds(), ) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityAddTagSheet.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityAddTagSheet.kt index f2809ab6a..c2d852389 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityAddTagSheet.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityAddTagSheet.kt @@ -10,7 +10,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.tooling.preview.Preview -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.ui.components.BottomSheet import to.bitkit.ui.components.BottomSheetPreview diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityIcon.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityIcon.kt index dc677fc01..ddeed3e91 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityIcon.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityIcon.kt @@ -24,6 +24,7 @@ import com.synonym.bitkitcore.OnchainActivity import com.synonym.bitkitcore.PaymentState import com.synonym.bitkitcore.PaymentType import to.bitkit.R +import to.bitkit.ext.create import to.bitkit.ext.doesExist import to.bitkit.ext.isBoosting import to.bitkit.ext.isTransfer @@ -131,8 +132,8 @@ fun CircularIcon( icon: Painter, iconColor: Color, backgroundColor: Color, - size: Dp = 32.dp, modifier: Modifier = Modifier, + size: Dp = 32.dp, ) { Box( contentAlignment = Alignment.Center, @@ -160,18 +161,14 @@ private fun Preview() { // Lightning Sent Succeeded ActivityIcon( activity = Activity.Lightning( - v1 = LightningActivity( + v1 = LightningActivity.create( id = "test-lightning-1", txType = PaymentType.SENT, status = PaymentState.SUCCEEDED, value = 50000uL, - fee = 1uL, invoice = "lnbc...", - message = "", timestamp = (System.currentTimeMillis() / 1000).toULong(), - preimage = null, - createdAt = null, - updatedAt = null, + fee = 1uL, ) ) ) @@ -179,18 +176,14 @@ private fun Preview() { // Lightning Received Failed ActivityIcon( activity = Activity.Lightning( - v1 = LightningActivity( + v1 = LightningActivity.create( id = "test-lightning-2", txType = PaymentType.RECEIVED, status = PaymentState.FAILED, value = 50000uL, - fee = 1uL, invoice = "lnbc...", - message = "", timestamp = (System.currentTimeMillis() / 1000).toULong(), - preimage = null, - createdAt = null, - updatedAt = null, + fee = 1uL, ) ) ) @@ -198,18 +191,14 @@ private fun Preview() { // Lightning Pending ActivityIcon( activity = Activity.Lightning( - v1 = LightningActivity( + v1 = LightningActivity.create( id = "test-lightning-3", txType = PaymentType.SENT, status = PaymentState.PENDING, value = 50000uL, - fee = 1uL, invoice = "lnbc...", - message = "", timestamp = (System.currentTimeMillis() / 1000).toULong(), - preimage = null, - createdAt = null, - updatedAt = null, + fee = 1uL, ) ) ) @@ -217,25 +206,17 @@ private fun Preview() { // Onchain Received ActivityIcon( activity = Activity.Onchain( - v1 = OnchainActivity( + v1 = OnchainActivity.create( id = "test-onchain-1", txType = PaymentType.RECEIVED, txId = "abc123", value = 100000uL, fee = 500uL, - feeRate = 8uL, address = "bc1...", - confirmed = true, timestamp = (System.currentTimeMillis() / 1000).toULong(), - isBoosted = false, - boostTxIds = emptyList(), - isTransfer = false, - doesExist = true, + confirmed = true, + feeRate = 8uL, confirmTimestamp = (System.currentTimeMillis() / 1000).toULong(), - channelId = null, - transferTxId = null, - createdAt = null, - updatedAt = null, ) ) ) @@ -243,25 +224,17 @@ private fun Preview() { // Onchain BOOST CPFP ActivityIcon( activity = Activity.Onchain( - v1 = OnchainActivity( + v1 = OnchainActivity.create( id = "test-onchain-1", txType = PaymentType.RECEIVED, txId = "abc123", value = 100000uL, fee = 500uL, - feeRate = 8uL, address = "bc1...", - confirmed = false, timestamp = (System.currentTimeMillis() / 1000).toULong(), + feeRate = 8uL, isBoosted = true, - boostTxIds = emptyList(), - isTransfer = false, - doesExist = true, confirmTimestamp = (System.currentTimeMillis() / 1000).toULong(), - channelId = null, - transferTxId = null, - createdAt = null, - updatedAt = null, ) ) ) @@ -269,25 +242,17 @@ private fun Preview() { // Onchain BOOST RBF ActivityIcon( activity = Activity.Onchain( - v1 = OnchainActivity( + v1 = OnchainActivity.create( id = "test-onchain-1", txType = PaymentType.SENT, txId = "abc123", value = 100000uL, fee = 500uL, - feeRate = 8uL, address = "bc1...", - confirmed = false, timestamp = (System.currentTimeMillis() / 1000).toULong(), + feeRate = 8uL, isBoosted = true, - boostTxIds = emptyList(), - isTransfer = false, - doesExist = true, confirmTimestamp = (System.currentTimeMillis() / 1000).toULong(), - channelId = null, - transferTxId = null, - createdAt = null, - updatedAt = null, ) ) ) @@ -295,25 +260,19 @@ private fun Preview() { // Onchain Transfer ActivityIcon( activity = Activity.Onchain( - v1 = OnchainActivity( + v1 = OnchainActivity.create( id = "test-onchain-2", txType = PaymentType.SENT, txId = "abc123", value = 100000uL, fee = 500uL, - feeRate = 8uL, address = "bc1...", - confirmed = true, timestamp = (System.currentTimeMillis() / 1000).toULong(), - isBoosted = false, - boostTxIds = emptyList(), + confirmed = true, + feeRate = 8uL, isTransfer = true, - doesExist = true, confirmTimestamp = (System.currentTimeMillis() / 1000).toULong(), - channelId = null, transferTxId = "transferTxId", - createdAt = null, - updatedAt = null, ) ) ) @@ -321,25 +280,20 @@ private fun Preview() { // Onchain Removed ActivityIcon( activity = Activity.Onchain( - v1 = OnchainActivity( + v1 = OnchainActivity.create( id = "test-onchain-2", txType = PaymentType.SENT, txId = "abc123", value = 100000uL, fee = 500uL, - feeRate = 8uL, address = "bc1...", - confirmed = true, timestamp = (System.currentTimeMillis() / 1000).toULong(), + confirmed = true, + feeRate = 8uL, isBoosted = true, - boostTxIds = emptyList(), - isTransfer = false, doesExist = false, confirmTimestamp = (System.currentTimeMillis() / 1000).toULong(), - channelId = null, transferTxId = "transferTxId", - createdAt = null, - updatedAt = null, ) ) ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityRow.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityRow.kt index a7e330f1f..856131218 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityRow.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityRow.kt @@ -47,7 +47,7 @@ import to.bitkit.ui.screens.wallets.activity.utils.previewActivityItems import to.bitkit.ui.settingsViewModel import to.bitkit.ui.shared.UiConstants import to.bitkit.ui.shared.animations.BalanceAnimations -import to.bitkit.ui.shared.util.clickableAlpha +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.theme.Shapes diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/CustomTabRowWithSpacing.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/CustomTabRowWithSpacing.kt index c4d62599b..55a1b4fd9 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/CustomTabRowWithSpacing.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/CustomTabRowWithSpacing.kt @@ -23,7 +23,7 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import to.bitkit.ui.components.CaptionB -import to.bitkit.ui.shared.util.clickableAlpha +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.Colors @Composable diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/EmptyActivityRow.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/EmptyActivityRow.kt index 3d8c95643..83b0e4842 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/EmptyActivityRow.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/EmptyActivityRow.kt @@ -17,7 +17,7 @@ import androidx.compose.ui.unit.dp import to.bitkit.R import to.bitkit.ui.components.BodyMSB import to.bitkit.ui.components.CaptionB -import to.bitkit.ui.shared.util.clickableAlpha +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/utils/PreviewItems.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/utils/PreviewItems.kt index bc99e8794..ed429302e 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/utils/PreviewItems.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/utils/PreviewItems.kt @@ -5,6 +5,7 @@ import com.synonym.bitkitcore.LightningActivity import com.synonym.bitkitcore.OnchainActivity import com.synonym.bitkitcore.PaymentState import com.synonym.bitkitcore.PaymentType +import to.bitkit.ext.create import java.util.Calendar val previewActivityItems = buildList { @@ -19,19 +20,17 @@ val previewActivityItems = buildList { // Today add( Activity.Onchain( - OnchainActivity( + OnchainActivity.create( id = "1", txType = PaymentType.RECEIVED, txId = "01", value = 42_000_u, fee = 200_u, - feeRate = 1_u, address = "bc1", confirmed = true, timestamp = today.epochSecond(), isBoosted = true, boostTxIds = listOf("02", "03"), - isTransfer = false, doesExist = false, confirmTimestamp = today.epochSecond(), channelId = "channelId", @@ -45,18 +44,16 @@ val previewActivityItems = buildList { // Yesterday add( Activity.Lightning( - LightningActivity( + LightningActivity.create( id = "2", txType = PaymentType.SENT, status = PaymentState.PENDING, value = 30_000_u, - fee = 15_u, invoice = "lnbc2", - message = "Custom very long lightning activity message to test truncation", timestamp = yesterday.epochSecond(), + fee = 15_u, + message = "Custom very long lightning activity message to test truncation", preimage = "preimage1", - createdAt = yesterday.epochSecond(), - updatedAt = yesterday.epochSecond(), ) ) ) @@ -64,18 +61,15 @@ val previewActivityItems = buildList { // This Week add( Activity.Lightning( - LightningActivity( + LightningActivity.create( id = "3", txType = PaymentType.RECEIVED, status = PaymentState.FAILED, value = 217_000_u, - fee = 17_u, invoice = "lnbc3", - message = "", timestamp = thisWeek.epochSecond(), + fee = 17_u, preimage = "preimage2", - createdAt = thisWeek.epochSecond(), - updatedAt = thisWeek.epochSecond(), ) ) ) @@ -83,25 +77,18 @@ val previewActivityItems = buildList { // This Month add( Activity.Onchain( - OnchainActivity( + OnchainActivity.create( id = "4", txType = PaymentType.SENT, txId = "04", value = 950_000_u, fee = 110_u, - feeRate = 1_u, address = "bc1", - confirmed = false, timestamp = thisMonth.epochSecond(), - isBoosted = false, - boostTxIds = emptyList(), isTransfer = true, - doesExist = true, confirmTimestamp = today.epochSecond() + 3600u, channelId = "channelId", transferTxId = "transferTxId", - createdAt = thisMonth.epochSecond(), - updatedAt = thisMonth.epochSecond(), ) ) ) @@ -109,18 +96,14 @@ val previewActivityItems = buildList { // Last Year add( Activity.Lightning( - LightningActivity( + LightningActivity.create( id = "5", txType = PaymentType.SENT, status = PaymentState.SUCCEEDED, value = 200_000_u, - fee = 1_u, invoice = "lnbc…", - message = "", timestamp = lastYear.epochSecond(), - preimage = null, - createdAt = lastYear.epochSecond(), - updatedAt = lastYear.epochSecond(), + fee = 1_u, ) ) ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/EditInvoiceScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/EditInvoiceScreen.kt index 660d311a1..f77c53a47 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/EditInvoiceScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/EditInvoiceScreen.kt @@ -38,7 +38,7 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.tooling.preview.Devices.NEXUS_5 import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R import to.bitkit.repositories.CurrencyState diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveAmountScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveAmountScreen.kt index c0e34149f..f88eba327 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveAmountScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveAmountScreen.kt @@ -24,7 +24,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Devices.NEXUS_5 import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.launch import to.bitkit.R @@ -44,8 +44,8 @@ import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.UnitButton import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.scaffold.SheetTopBar +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.shared.modifiers.sheetHeight -import to.bitkit.ui.shared.util.clickableAlpha import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveConfirmScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveConfirmScreen.kt index 0dcd6cc64..611224d24 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveConfirmScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveConfirmScreen.kt @@ -19,7 +19,7 @@ import androidx.compose.ui.tooling.preview.Devices.NEXUS_5 import androidx.compose.ui.tooling.preview.Devices.PIXEL_TABLET import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.serialization.Serializable import to.bitkit.R diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveLiquidityScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveLiquidityScreen.kt index 3577ade22..5f3b9c801 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveLiquidityScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveLiquidityScreen.kt @@ -15,7 +15,7 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import to.bitkit.R import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BodyMB diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index db350a194..0b23f52f9 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -18,7 +18,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState @@ -236,6 +235,7 @@ fun ReceiveQrScreen( tab = tab, walletState = walletState, cjitInvoice = cjitInvoice, + onClickEditInvoice = onClickEditInvoice, modifier = Modifier.weight(1f) ) } @@ -354,31 +354,36 @@ private fun ReceiveQrView( modifier = Modifier.size(18.dp) ) }, - modifier = Modifier.testTag("SpecifyInvoiceButton") + modifier = Modifier + .weight(1f) + .testTag("SpecifyInvoiceButton") ) - Tooltip( - text = stringResource(R.string.wallet__receive_copied), - tooltipState = qrButtonTooltipState - ) { - PrimaryButton( - text = stringResource(R.string.common__copy), - size = ButtonSize.Small, - onClick = { - context.setClipboardText(uri) - coroutineScope.launch { qrButtonTooltipState.show() } - }, - fullWidth = false, - color = Colors.White10, - icon = { - Icon( - painter = painterResource(R.drawable.ic_copy), - contentDescription = null, - tint = tab.accentColor, - modifier = Modifier.size(18.dp) - ) - }, - modifier = Modifier.testTag("ReceiveCopyQR") - ) + Box(modifier = Modifier.weight(1f)) { + Tooltip( + text = stringResource(R.string.wallet__receive_copied), + tooltipState = qrButtonTooltipState + ) { + PrimaryButton( + text = stringResource(R.string.common__copy), + size = ButtonSize.Small, + onClick = { + context.setClipboardText(uri) + coroutineScope.launch { qrButtonTooltipState.show() } + }, + fullWidth = true, + color = Colors.White10, + icon = { + Icon( + painter = painterResource(R.drawable.ic_copy), + contentDescription = null, + tint = tab.accentColor, + modifier = Modifier.size(18.dp) + ) + }, + modifier = Modifier + .testTag("ReceiveCopyQR") + ) + } } PrimaryButton( text = stringResource(R.string.common__share), @@ -398,6 +403,7 @@ private fun ReceiveQrView( modifier = Modifier.size(18.dp) ) }, + modifier = Modifier.weight(1f) ) } Spacer(modifier = Modifier.height(16.dp)) @@ -452,6 +458,7 @@ private fun ReceiveDetailsView( tab: ReceiveTab, walletState: MainUiState, cjitInvoice: String?, + onClickEditInvoice: () -> Unit, modifier: Modifier = Modifier, ) { Card( @@ -471,6 +478,7 @@ private fun ReceiveDetailsView( ), body = walletState.onchainAddress, type = CopyAddressType.ONCHAIN, + onClickEditInvoice = onClickEditInvoice, testTag = "ReceiveOnchainAddress", ) } @@ -487,6 +495,7 @@ private fun ReceiveDetailsView( ), body = walletState.onchainAddress, type = CopyAddressType.ONCHAIN, + onClickEditInvoice = onClickEditInvoice, testTag = "ReceiveOnchainAddress", ) } @@ -495,6 +504,7 @@ private fun ReceiveDetailsView( title = stringResource(R.string.wallet__receive_lightning_invoice), address = cjitInvoice ?: walletState.bolt11, type = CopyAddressType.LIGHTNING, + onClickEditInvoice = onClickEditInvoice, testTag = "ReceiveLightningAddress", ) } @@ -506,6 +516,7 @@ private fun ReceiveDetailsView( title = stringResource(R.string.wallet__receive_lightning_invoice), address = cjitInvoice ?: walletState.bolt11, type = CopyAddressType.LIGHTNING, + onClickEditInvoice = onClickEditInvoice, testTag = "ReceiveLightningAddress", ) } @@ -523,6 +534,7 @@ private fun CopyAddressCard( title: String, address: String, type: CopyAddressType, + onClickEditInvoice: () -> Unit, body: String? = null, testTag: String? = null, ) { @@ -536,14 +548,7 @@ private fun CopyAddressCard( .fillMaxWidth() .padding(24.dp) ) { - Row { - Caption13Up(text = title, color = Colors.White64) - - Spacer(modifier = Modifier.width(3.dp)) - - val iconRes = if (type == CopyAddressType.ONCHAIN) R.drawable.ic_bitcoin else R.drawable.ic_lightning_alt - Icon(painter = painterResource(iconRes), contentDescription = null, tint = Colors.White64) - } + Caption13Up(text = title, color = Colors.White64) Spacer(modifier = Modifier.height(16.dp)) BodyS( text = (body ?: address).truncate(32).uppercase(), @@ -553,28 +558,48 @@ private fun CopyAddressCard( Row( horizontalArrangement = Arrangement.spacedBy(16.dp) ) { + PrimaryButton( + text = stringResource(R.string.common__edit), + size = ButtonSize.Small, + onClick = onClickEditInvoice, + fullWidth = false, + color = Colors.White10, + icon = { + Icon( + painter = painterResource(R.drawable.ic_pencil_simple), + contentDescription = null, + tint = if (type == CopyAddressType.ONCHAIN) Colors.Brand else Colors.Purple, + modifier = Modifier.size(18.dp) + ) + }, + modifier = Modifier + .weight(1f) + .testTag("SpecifyInvoiceButton") + ) Tooltip( text = stringResource(R.string.wallet__receive_copied), tooltipState = tooltipState, ) { - PrimaryButton( - text = stringResource(R.string.common__copy), - size = ButtonSize.Small, - onClick = { - context.setClipboardText(address) - coroutineScope.launch { tooltipState.show() } - }, - fullWidth = false, - color = Colors.White10, - icon = { - Icon( - painter = painterResource(R.drawable.ic_copy), - contentDescription = null, - tint = if (type == CopyAddressType.ONCHAIN) Colors.Brand else Colors.Purple, - modifier = Modifier.size(18.dp) - ) - }, - ) + Box(modifier = Modifier.weight(1f)) { + PrimaryButton( + text = stringResource(R.string.common__copy), + size = ButtonSize.Small, + onClick = { + context.setClipboardText(address) + coroutineScope.launch { tooltipState.show() } + }, + fullWidth = false, + color = Colors.White10, + icon = { + Icon( + painter = painterResource(R.drawable.ic_copy), + contentDescription = null, + tint = if (type == CopyAddressType.ONCHAIN) Colors.Brand else Colors.Purple, + modifier = Modifier.size(18.dp) + ) + }, + ) + } } PrimaryButton( text = stringResource(R.string.common__share), @@ -804,6 +829,7 @@ private fun PreviewDetailsMode() { bolt11 = "lnbcrt500u1pn7umn7pp5x0s9lt9fwrff6rp70pz3guwnjgw97sjuv79...", ), cjitInvoice = null, + onClickEditInvoice = {}, modifier = Modifier.weight(1f) ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt index 0da519f81..3cee808dc 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt @@ -11,7 +11,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController @@ -25,7 +25,6 @@ import to.bitkit.ui.walletViewModel import to.bitkit.viewmodels.AmountInputViewModel import to.bitkit.viewmodels.MainUiState import to.bitkit.viewmodels.SettingsViewModel -import to.bitkit.viewmodels.WalletViewModelEffects @Composable fun ReceiveSheet( @@ -65,16 +64,6 @@ fun ReceiveSheet( showCreateCjit.value = !cjitInvoice.value.isNullOrBlank() } - LaunchedEffect(Unit) { - wallet.walletEffect.collect { effect -> - when (effect) { - WalletViewModelEffects.NavigateGeoBlockScreen -> { - navController.navigate(ReceiveRoute.GeoBlock) - } - } - } - } - ReceiveQrScreen( cjitInvoice = cjitInvoice.value, walletState = walletState, diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/AddTagScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/AddTagScreen.kt index 719039b16..1de166d58 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/AddTagScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/AddTagScreen.kt @@ -22,7 +22,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.delay import to.bitkit.R diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt index 25108cf0d..ebad53ac0 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt @@ -22,7 +22,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Devices.NEXUS_5 import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.synonym.bitkitcore.LnurlPayData import com.synonym.bitkitcore.LnurlWithdrawData @@ -51,8 +51,8 @@ import to.bitkit.ui.components.Text13Up import to.bitkit.ui.components.UnitButton import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.scaffold.SheetTopBar +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.shared.modifiers.sheetHeight -import to.bitkit.ui.shared.util.clickableAlpha import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendCoinSelectionScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendCoinSelectionScreen.kt index 87c6cf7c8..96b032de3 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendCoinSelectionScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendCoinSelectionScreen.kt @@ -24,7 +24,7 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.lightningdevkit.ldknode.OutPoint import org.lightningdevkit.ldknode.SpendableUtxo @@ -44,8 +44,8 @@ import to.bitkit.ui.components.TagButton import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.currencyViewModel import to.bitkit.ui.scaffold.SheetTopBar +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.shared.modifiers.sheetHeight -import to.bitkit.ui.shared.util.clickableAlpha import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppSwitchDefaults import to.bitkit.ui.theme.AppThemeSurface diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt index a839418ac..19a573b31 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt @@ -70,8 +70,8 @@ import to.bitkit.ui.components.rememberMoneyText import to.bitkit.ui.scaffold.AppAlertDialog import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.settingsViewModel +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.shared.modifiers.sheetHeight -import to.bitkit.ui.shared.util.clickableAlpha import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt index 6da22c346..32a0633ef 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt @@ -41,8 +41,8 @@ import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.components.settings.SectionHeader import to.bitkit.ui.scaffold.SheetTopBar +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.shared.modifiers.sheetHeight -import to.bitkit.ui.shared.util.clickableAlpha import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendPinCheckScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendPinCheckScreen.kt index 8c2df8bde..413573e47 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendPinCheckScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendPinCheckScreen.kt @@ -32,8 +32,8 @@ import to.bitkit.ui.components.NumberPad import to.bitkit.ui.components.NumberPadType import to.bitkit.ui.components.PinDots import to.bitkit.ui.scaffold.SheetTopBar +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.shared.modifiers.sheetHeight -import to.bitkit.ui.shared.util.clickableAlpha import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendQuickPayScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendQuickPayScreen.kt index 1b6ea536b..186b85b88 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendQuickPayScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendQuickPayScreen.kt @@ -15,7 +15,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R import to.bitkit.models.NodeLifecycleState diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/suggestion/BuyIntroScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/suggestion/BuyIntroScreen.kt index 2f586b053..96c924ac3 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/suggestion/BuyIntroScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/suggestion/BuyIntroScreen.kt @@ -15,7 +15,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.net.toUri -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import to.bitkit.R import to.bitkit.env.Env import to.bitkit.models.Suggestion diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/AddWidgetsScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/AddWidgetsScreen.kt index 31ca28913..300d8a5d3 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/AddWidgetsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/AddWidgetsScreen.kt @@ -19,12 +19,13 @@ import to.bitkit.ui.theme.AppThemeSurface @Composable fun AddWidgetsScreen( onWidgetSelected: (WidgetType) -> Unit, + onBackCLick: () -> Unit, fiatSymbol: String, ) { ScreenColumn { AppTopBar( titleText = stringResource(R.string.widgets__add), - onBackClick = null, + onBackClick = onBackCLick, actions = { DrawerNavIcon() }, ) @@ -99,7 +100,8 @@ private fun Preview() { AppThemeSurface { AddWidgetsScreen( onWidgetSelected = {}, - fiatSymbol = "$" + fiatSymbol = "$", + onBackCLick = {} ) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/WidgetsIntroScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/WidgetsIntroScreen.kt index a92100477..03a9e20c0 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/WidgetsIntroScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/WidgetsIntroScreen.kt @@ -27,11 +27,12 @@ import to.bitkit.ui.utils.withAccent @Composable fun WidgetsIntroScreen( onContinue: () -> Unit, + onBackClick: () -> Unit, ) { ScreenColumn { AppTopBar( titleText = "", - onBackClick = null, + onBackClick = onBackClick, actions = { DrawerNavIcon() }, ) @@ -68,7 +69,8 @@ fun WidgetsIntroScreen( private fun Preview() { AppThemeSurface { WidgetsIntroScreen( - onContinue = {} + onContinue = {}, + onBackClick = {} ) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/calculator/CalculatorPreviewScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/calculator/CalculatorPreviewScreen.kt index 322609712..0a9678b65 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/calculator/CalculatorPreviewScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/calculator/CalculatorPreviewScreen.kt @@ -22,7 +22,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R import to.bitkit.ui.components.BodyM diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/calculator/components/CalculatorCard.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/calculator/components/CalculatorCard.kt index 5ddff79ce..7888d6000 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/calculator/components/CalculatorCard.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/calculator/components/CalculatorCard.kt @@ -29,7 +29,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R import to.bitkit.models.BITCOIN_SYMBOL diff --git a/app/src/main/java/to/bitkit/ui/settings/AdvancedSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/AdvancedSettingsScreen.kt index d8c7917b0..1bd2beb90 100644 --- a/app/src/main/java/to/bitkit/ui/settings/AdvancedSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/AdvancedSettingsScreen.kt @@ -15,7 +15,7 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.navigation.NavController import to.bitkit.R import to.bitkit.ui.Routes diff --git a/app/src/main/java/to/bitkit/ui/settings/BackupSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/BackupSettingsScreen.kt index 969c60bbb..558d127f2 100644 --- a/app/src/main/java/to/bitkit/ui/settings/BackupSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/BackupSettingsScreen.kt @@ -47,12 +47,13 @@ import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.settingsViewModel -import to.bitkit.ui.shared.util.clickableAlpha +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.viewmodels.BackupCategoryUiState import to.bitkit.viewmodels.BackupStatusUiState import to.bitkit.viewmodels.toUiState +import kotlin.time.ExperimentalTime @Composable fun BackupSettingsScreen( @@ -145,6 +146,7 @@ private fun BackupSettingsScreenContent( } } +@OptIn(ExperimentalTime::class) @Composable private fun BackupStatusItem( uiState: BackupCategoryUiState, @@ -153,7 +155,7 @@ private fun BackupStatusItem( ) { val status = uiState.status - val timeString = if (status.synced == 0L) { + val time = if (status.synced == 0L) { stringResource(R.string.common__never) } else { status.synced.toRelativeTimeString() @@ -161,11 +163,8 @@ private fun BackupStatusItem( val subtitle = when { status.running -> stringResource(R.string.settings__backup__status_running) - !status.isRequired -> stringResource(R.string.settings__backup__status_success) - .replace("{time}", timeString) - - else -> stringResource(R.string.settings__backup__status_failed) - .replace("{time}", timeString) + !status.isRequired -> stringResource(R.string.settings__backup__status_success).replace("{time}", time) + else -> stringResource(R.string.settings__backup__status_failed).replace("{time}", time) } Row( diff --git a/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt b/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt index 00c7489d4..47778d650 100644 --- a/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt @@ -23,7 +23,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import kotlinx.coroutines.launch diff --git a/app/src/main/java/to/bitkit/ui/settings/ChannelOrdersScreen.kt b/app/src/main/java/to/bitkit/ui/settings/ChannelOrdersScreen.kt index 3c9fb99fb..ed5acd4ed 100644 --- a/app/src/main/java/to/bitkit/ui/settings/ChannelOrdersScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/ChannelOrdersScreen.kt @@ -55,7 +55,7 @@ import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.components.settings.SectionHeader import to.bitkit.ui.scaffold.AppTopBar -import to.bitkit.ui.shared.util.clickableAlpha +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.AppShapes import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors diff --git a/app/src/main/java/to/bitkit/ui/settings/LanguageSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/LanguageSettingsScreen.kt index bc011093a..3b21f3a2d 100644 --- a/app/src/main/java/to/bitkit/ui/settings/LanguageSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/LanguageSettingsScreen.kt @@ -10,7 +10,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.models.Language import to.bitkit.ui.components.Text13Up diff --git a/app/src/main/java/to/bitkit/ui/settings/LogsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/LogsScreen.kt index 616861093..0d7607cb3 100644 --- a/app/src/main/java/to/bitkit/ui/settings/LogsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/LogsScreen.kt @@ -32,14 +32,14 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.navigation.NavController import to.bitkit.ui.components.BodyMSB import to.bitkit.ui.components.Caption import to.bitkit.ui.navigateToLogDetail import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.ScreenColumn -import to.bitkit.ui.shared.util.clickableAlpha +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.Colors import to.bitkit.viewmodels.LogsViewModel diff --git a/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt index 490fd3da6..823a5d24d 100644 --- a/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt @@ -40,7 +40,7 @@ import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.settingsViewModel -import to.bitkit.ui.shared.util.clickableAlpha +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.AppThemeSurface private const val DEV_MODE_TAP_THRESHOLD = 5 @@ -65,6 +65,7 @@ fun SettingsScreen( onSupportClick = { navController.navigate(Routes.Support) }, onAboutClick = { navController.navigateToAboutSettings() }, onDevClick = { navController.navigateToDevSettings() }, + onBackClick = { navController.popBackStack() }, onCogTap = { haptic.performHapticFeedback(HapticFeedbackType.Confirm) enableDevModeTapCount = enableDevModeTapCount + 1 @@ -100,11 +101,12 @@ fun SettingsScreenContent( onAboutClick: () -> Unit, onDevClick: () -> Unit, onCogTap: () -> Unit, + onBackClick: () -> Unit, ) { ScreenColumn { AppTopBar( titleText = stringResource(R.string.settings__settings), - onBackClick = null, + onBackClick = onBackClick, actions = { DrawerNavIcon() }, ) Column( @@ -186,6 +188,7 @@ private fun Preview() { onAboutClick = {}, onDevClick = {}, onCogTap = {}, + onBackClick = {}, ) } } diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt index deb9dd4fa..59a555bb5 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt @@ -24,7 +24,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.net.toUri -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import to.bitkit.R @@ -45,7 +45,7 @@ import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn -import to.bitkit.ui.shared.util.clickableAlpha +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.AppShapes import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/CoinSelectPreferenceScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/CoinSelectPreferenceScreen.kt index c51cf5e34..9555246b5 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/CoinSelectPreferenceScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/CoinSelectPreferenceScreen.kt @@ -12,7 +12,7 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import to.bitkit.R diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt index 00322eb29..cd091065f 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt @@ -23,7 +23,7 @@ import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt index 36d0b52c6..cf74847db 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt @@ -20,7 +20,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController diff --git a/app/src/main/java/to/bitkit/ui/settings/appStatus/AppStatusScreen.kt b/app/src/main/java/to/bitkit/ui/settings/appStatus/AppStatusScreen.kt index b756c3ac9..9fe6a8799 100644 --- a/app/src/main/java/to/bitkit/ui/settings/appStatus/AppStatusScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/appStatus/AppStatusScreen.kt @@ -24,10 +24,9 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController -import kotlinx.datetime.Clock import to.bitkit.R import to.bitkit.ext.startActivityAppSettings import to.bitkit.ext.toLocalizedTimestamp @@ -42,10 +41,12 @@ import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn -import to.bitkit.ui.shared.util.clickableAlpha +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +import kotlin.time.Clock.System.now import kotlin.time.Duration.Companion.minutes +import kotlin.time.ExperimentalTime @Composable fun AppStatusScreen( @@ -246,6 +247,7 @@ private data class StatusUi( val state: HealthState, ) +@OptIn(ExperimentalTime::class) @Preview(showSystemUi = true) @Composable private fun Preview() { @@ -259,7 +261,7 @@ private fun Preview() { channels = HealthState.PENDING, backups = HealthState.READY, ), - backupSubtitle = Clock.System.now().minus(3.minutes).toEpochMilliseconds().toLocalizedTimestamp(), + backupSubtitle = now().minus(3.minutes).toEpochMilliseconds().toLocalizedTimestamp(), nodeSubtitle = NodeLifecycleState.Running.uiText, ), ) diff --git a/app/src/main/java/to/bitkit/ui/settings/backgroundPayments/BackgroundPaymentsIntroScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backgroundPayments/BackgroundPaymentsIntroScreen.kt index 01b15e798..08204ea77 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backgroundPayments/BackgroundPaymentsIntroScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backgroundPayments/BackgroundPaymentsIntroScreen.kt @@ -11,7 +11,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import to.bitkit.R import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.Display diff --git a/app/src/main/java/to/bitkit/ui/settings/backgroundPayments/BackgroundPaymentsSettings.kt b/app/src/main/java/to/bitkit/ui/settings/backgroundPayments/BackgroundPaymentsSettings.kt index 257ced12b..562bbfe62 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backgroundPayments/BackgroundPaymentsSettings.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backgroundPayments/BackgroundPaymentsSettings.kt @@ -12,7 +12,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R import to.bitkit.ui.components.BodyM diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/BackupIntroScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/BackupIntroScreen.kt index cd564c469..4efd1aa55 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/BackupIntroScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/BackupIntroScreen.kt @@ -4,10 +4,8 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable @@ -25,6 +23,7 @@ import to.bitkit.ui.components.Display import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton import to.bitkit.ui.components.SheetSize +import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.shared.modifiers.sheetHeight import to.bitkit.ui.shared.util.gradientBackground @@ -47,7 +46,6 @@ fun BackupIntroScreen( .testTag("BackupIntroView") ) { SheetTopBar(stringResource(R.string.security__backup_wallet)) - Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(horizontal = 16.dp), @@ -60,30 +58,28 @@ fun BackupIntroScreen( .weight(1f) .testTag("BackupIntroViewImage") ) - Display( text = stringResource(R.string.security__backup_title).withAccent(accentColor = Colors.Blue), color = Colors.White, modifier = Modifier .testTag("BackupIntroViewTitle") ) - Spacer(Modifier.height(8.dp)) + VerticalSpacer(8.dp) BodyM( - text = if (hasFunds) { - stringResource(R.string.security__backup_funds) - } else { - stringResource(R.string.security__backup_funds_no) + text = when (hasFunds) { + true -> stringResource(R.string.security__backup_funds) + else -> stringResource(R.string.security__backup_funds_no) }, color = Colors.White64, modifier = Modifier .testTag("BackupIntroViewDescription") ) - Spacer(Modifier.height(32.dp)) + VerticalSpacer(32.dp) Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier .fillMaxWidth() - .testTag("BackupIntroViewButtons"), - horizontalArrangement = Arrangement.spacedBy(16.dp) + .testTag("BackupIntroViewButtons") ) { SecondaryButton( text = stringResource(R.string.common__later), @@ -91,19 +87,18 @@ fun BackupIntroScreen( onClick = onClose, modifier = Modifier .weight(1f) - .testTag("BackupIntroViewCancel"), + .testTag("BackupIntroViewCancel") ) - PrimaryButton( text = stringResource(R.string.security__backup_button), fullWidth = false, onClick = onConfirm, modifier = Modifier .weight(1f) - .testTag("BackupIntroViewContinue"), + .testTag("BackupIntroViewContinue") ) } - Spacer(Modifier.height(16.dp)) + VerticalSpacer(16.dp) } } } diff --git a/app/src/main/java/to/bitkit/ui/settings/general/GeneralSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/general/GeneralSettingsScreen.kt index f90906e27..cc63bfc56 100644 --- a/app/src/main/java/to/bitkit/ui/settings/general/GeneralSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/general/GeneralSettingsScreen.kt @@ -12,7 +12,7 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import to.bitkit.R diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt index 992f70a0a..3081689de 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt @@ -71,7 +71,7 @@ import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.settings.lightning.components.ChannelStatusView -import to.bitkit.ui.shared.util.clickableAlpha +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.getBlockExplorerUrl diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsScreen.kt index 32618dc1a..d409a81b3 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsScreen.kt @@ -60,7 +60,7 @@ import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.navigateToTransferFunding import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.ScreenColumn -import to.bitkit.ui.shared.util.clickableAlpha +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.shared.util.shareZipFile import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors diff --git a/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinResultScreen.kt b/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinResultScreen.kt index 71bbae2e9..8913265f2 100644 --- a/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinResultScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinResultScreen.kt @@ -34,15 +34,19 @@ fun ChangePinResultScreen( onOkClick = { navController.popBackStack(inclusive = false) }, + onBackClick = { + navController.popBackStack() + } ) } @Composable private fun ChangePinResultContent( onOkClick: () -> Unit, + onBackClick: () -> Unit, ) { ScreenColumn { - AppTopBar(stringResource(R.string.security__cp_changed_title), onBackClick = null) + AppTopBar(stringResource(R.string.security__cp_changed_title), onBackClick = onBackClick) Column( modifier = Modifier.padding(horizontal = 16.dp) ) { @@ -81,6 +85,7 @@ private fun Preview() { AppThemeSurface { ChangePinResultContent( onOkClick = {}, + onBackClick = {}, ) } } diff --git a/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinScreen.kt b/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinScreen.kt index 4a199c10f..85c727ed0 100644 --- a/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinScreen.kt @@ -33,7 +33,7 @@ import to.bitkit.ui.navigateToChangePinNew import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn -import to.bitkit.ui.shared.util.clickableAlpha +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors diff --git a/app/src/main/java/to/bitkit/ui/settings/pin/PinBiometricsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/pin/PinBiometricsScreen.kt index c8345e6c2..b727056eb 100644 --- a/app/src/main/java/to/bitkit/ui/settings/pin/PinBiometricsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/pin/PinBiometricsScreen.kt @@ -38,7 +38,7 @@ import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.settingsViewModel -import to.bitkit.ui.shared.util.clickableAlpha +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppSwitchDefaults import to.bitkit.ui.theme.AppThemeSurface diff --git a/app/src/main/java/to/bitkit/ui/settings/pin/PinResultScreen.kt b/app/src/main/java/to/bitkit/ui/settings/pin/PinResultScreen.kt index 4e2c58247..a3eba7734 100644 --- a/app/src/main/java/to/bitkit/ui/settings/pin/PinResultScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/pin/PinResultScreen.kt @@ -29,7 +29,7 @@ import to.bitkit.ui.components.BodyMSB import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.settingsViewModel -import to.bitkit.ui.shared.util.clickableAlpha +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppSwitchDefaults import to.bitkit.ui.theme.AppThemeSurface diff --git a/app/src/main/java/to/bitkit/ui/settings/quickPay/QuickPaySettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/quickPay/QuickPaySettingsScreen.kt index 5978da09d..2c1cf66da 100644 --- a/app/src/main/java/to/bitkit/ui/settings/quickPay/QuickPaySettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/quickPay/QuickPaySettingsScreen.kt @@ -15,7 +15,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R import to.bitkit.ui.components.BodyM diff --git a/app/src/main/java/to/bitkit/ui/settings/support/ReportIssueScreen.kt b/app/src/main/java/to/bitkit/ui/settings/support/ReportIssueScreen.kt index e4dc66b40..4fc4689c9 100644 --- a/app/src/main/java/to/bitkit/ui/settings/support/ReportIssueScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/support/ReportIssueScreen.kt @@ -15,7 +15,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R import to.bitkit.ui.components.BodyM diff --git a/app/src/main/java/to/bitkit/ui/shared/modifiers/ClickableAlpha.kt b/app/src/main/java/to/bitkit/ui/shared/modifiers/ClickableAlpha.kt new file mode 100644 index 000000000..555d79d06 --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/shared/modifiers/ClickableAlpha.kt @@ -0,0 +1,138 @@ +package to.bitkit.ui.shared.modifiers + +import androidx.compose.animation.core.Animatable +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.layout.MeasureResult +import androidx.compose.ui.layout.MeasureScope +import androidx.compose.ui.node.DelegatingNode +import androidx.compose.ui.node.LayoutModifierNode +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.node.SemanticsModifierNode +import androidx.compose.ui.platform.InspectorInfo +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.SemanticsPropertyReceiver +import androidx.compose.ui.semantics.onClick +import androidx.compose.ui.semantics.role +import androidx.compose.ui.unit.Constraints +import kotlinx.coroutines.launch + +/** + * Adjusts the alpha of a composable when it is pressed and makes it clickable. + * When pressed, the alpha is reduced to provide visual feedback. + * If `onClick` is null, the clickable behavior is disabled. + * + * Analogue of `TouchableOpacity` in React Native. + */ +fun Modifier.clickableAlpha( + pressedAlpha: Float = 0.7f, + onClick: (() -> Unit)?, +): Modifier = if (onClick != null) { + this.then(ClickableAlphaElement(pressedAlpha, onClick)) +} else { + this +} + +private data class ClickableAlphaElement( + val pressedAlpha: Float, + val onClick: () -> Unit, +) : ModifierNodeElement() { + override fun create(): ClickableAlphaNode = ClickableAlphaNode(pressedAlpha, onClick) + + override fun update(node: ClickableAlphaNode) { + node.pressedAlpha = pressedAlpha + node.onClick = onClick + } + + override fun InspectorInfo.inspectableProperties() { + name = "clickableAlpha" + properties["pressedAlpha"] = pressedAlpha + properties["onClick"] = onClick + } +} + +private class ClickableAlphaNode( + var pressedAlpha: Float, + var onClick: () -> Unit, +) : DelegatingNode(), LayoutModifierNode, SemanticsModifierNode { + + private val animatable = Animatable(1f) + + init { + delegate( + SuspendingPointerInputModifierNode { + detectTapGestures( + onPress = { + coroutineScope.launch { animatable.animateTo(pressedAlpha) } + val released = tryAwaitRelease() + if (!released) { + coroutineScope.launch { animatable.animateTo(1f) } + } + }, + onTap = { + onClick() + coroutineScope.launch { + animatable.animateTo(pressedAlpha) + animatable.animateTo(1f) + } + } + ) + } + ) + } + + override fun MeasureScope.measure(measurable: Measurable, constraints: Constraints): MeasureResult { + val placeable = measurable.measure(constraints) + + return layout(placeable.width, placeable.height) { + placeable.placeWithLayer(0, 0) { + this.alpha = animatable.value + } + } + } + + override fun SemanticsPropertyReceiver.applySemantics() { + role = Role.Button + onClick { + onClick() + true + } + } +} + +/** + * Applies alpha animation feedback on press without consuming click events. + * This allows the Button's onClick to work while providing full-area visual feedback. + */ +@Composable +fun Modifier.alphaFeedback( + pressedAlpha: Float = 0.7f, + enabled: Boolean = true, +): Modifier = if (enabled) { + val animatable = remember { Animatable(1f) } + val scope = rememberCoroutineScope() + + this + .pointerInput(Unit) { + detectTapGestures( + onPress = { + scope.launch { animatable.animateTo(pressedAlpha) } + tryAwaitRelease() + scope.launch { animatable.animateTo(1f) } + }, + onTap = null // Don't consume tap - let Button handle it + ) + } + .graphicsLayer { + alpha = animatable.value + } +} else { + this +} diff --git a/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueueManager.kt b/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueueManager.kt index 680d2f048..94af44aa3 100644 --- a/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueueManager.kt +++ b/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueueManager.kt @@ -49,8 +49,7 @@ class ToastQueueManager(private val scope: CoroutineScope) { } newQueue } - // If no toast is currently displayed, show this one immediately - showNextToastIfAvailable() + dismissCurrentToast() } /** diff --git a/app/src/main/java/to/bitkit/ui/shared/util/Modifiers.kt b/app/src/main/java/to/bitkit/ui/shared/util/Modifiers.kt index 596170d32..1568ad651 100644 --- a/app/src/main/java/to/bitkit/ui/shared/util/Modifiers.kt +++ b/app/src/main/java/to/bitkit/ui/shared/util/Modifiers.kt @@ -1,22 +1,13 @@ package to.bitkit.ui.shared.util -import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.composed import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.draw.shadow @@ -27,7 +18,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Paint import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.drawscope.drawIntoCanvas -import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.node.DrawModifierNode @@ -37,54 +27,6 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import to.bitkit.ui.theme.Colors -/** - * Adjusts the alpha of a composable when it is pressed and makes it clickable. - * When pressed, the alpha is reduced to provide visual feedback. - * If `onClick` is null, the clickable behavior is disabled. - * - * Analogue of `TouchableOpacity` in React Native. - */ -fun Modifier.clickableAlpha( - pressedAlpha: Float = 0.7f, - onClick: (() -> Unit)?, -): Modifier = composed { - val interactionSource = remember { MutableInteractionSource() } - val isPressed by interactionSource.collectIsPressedAsState() - - val wasClicked = remember { mutableStateOf(false) } - - LaunchedEffect(isPressed) { - if (!isPressed) { - wasClicked.value = false - } - } - - val alpha by animateFloatAsState( - targetValue = if (isPressed || wasClicked.value) pressedAlpha else 1f, - finishedListener = { - // Reset the clicked state after animation completes - wasClicked.value = false - } - ) - - this - .graphicsLayer { this.alpha = alpha } - .then( - if (onClick != null) { - Modifier.clickable( - onClick = { - wasClicked.value = true - onClick() - }, - interactionSource = interactionSource, - indication = null, - ) - } else { - Modifier - } - ) -} - fun Modifier.gradientBackground( startColor: Color = Colors.White08, endColor: Color = Color.White.copy(alpha = 0.012f), diff --git a/app/src/main/java/to/bitkit/ui/sheets/BackupSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/BackupSheet.kt index bc9bd5cee..0ccaf924b 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/BackupSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/BackupSheet.kt @@ -9,7 +9,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController diff --git a/app/src/main/java/to/bitkit/ui/sheets/BoostTransactionSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/BoostTransactionSheet.kt index f45ce4317..f9d38456a 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/BoostTransactionSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/BoostTransactionSheet.kt @@ -30,7 +30,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import com.synonym.bitkitcore.Activity import to.bitkit.R import to.bitkit.models.BITCOIN_SYMBOL @@ -48,8 +48,8 @@ import to.bitkit.ui.components.SwipeToConfirm import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.components.rememberMoneyText import to.bitkit.ui.scaffold.SheetTopBar +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.shared.modifiers.sheetHeight -import to.bitkit.ui.shared.util.clickableAlpha import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors diff --git a/app/src/main/java/to/bitkit/ui/sheets/BoostTransactionViewModel.kt b/app/src/main/java/to/bitkit/ui/sheets/BoostTransactionViewModel.kt index 50f226f42..051c1465f 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/BoostTransactionViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/BoostTransactionViewModel.kt @@ -211,6 +211,8 @@ class BoostTransactionViewModel @Inject constructor( Logger.debug("Boost successful. newTxId: $newTxId", context = TAG) updateActivity(newTxId = newTxId, isRBF = isRBF).fold( onSuccess = { + lightningRepo.sync() + activityRepo.syncActivities() _uiState.update { it.copy(boosting = false) } setBoostTransactionEffect(BoostTransactionEffects.OnBoostSuccess) }, diff --git a/app/src/main/java/to/bitkit/ui/sheets/GiftSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/GiftSheet.kt index 380d9d8f4..8d286dfbc 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/GiftSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/GiftSheet.kt @@ -8,7 +8,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController import to.bitkit.R diff --git a/app/src/main/java/to/bitkit/ui/sheets/GiftViewModel.kt b/app/src/main/java/to/bitkit/ui/sheets/GiftViewModel.kt index 342a5a110..d7a1bc8e4 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/GiftViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/GiftViewModel.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import to.bitkit.di.BgDispatcher +import to.bitkit.ext.create import to.bitkit.ext.nowTimestamp import to.bitkit.models.NewTransactionSheetDetails import to.bitkit.models.NewTransactionSheetDirection @@ -98,18 +99,14 @@ class GiftViewModel @Inject constructor( private suspend fun insertGiftActivity(result: GiftClaimResult.SuccessWithoutLiquidity) { val nowTimestamp = nowTimestamp().epochSecond.toULong() - val lightningActivity = LightningActivity( + val lightningActivity = LightningActivity.create( id = result.paymentHashOrTxId, txType = PaymentType.RECEIVED, status = PaymentState.SUCCEEDED, value = result.sats.toULong(), - fee = 0u, invoice = result.invoice, - message = result.code, timestamp = nowTimestamp, - preimage = null, - createdAt = nowTimestamp, - updatedAt = null, + message = result.code, ) activityRepo.insertActivity(Activity.Lightning(lightningActivity)).getOrThrow() diff --git a/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt index 2bfe00004..5c4f49e59 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt @@ -11,7 +11,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController diff --git a/app/src/main/java/to/bitkit/utils/Errors.kt b/app/src/main/java/to/bitkit/utils/Errors.kt index 7f0d63088..9796db4bf 100644 --- a/app/src/main/java/to/bitkit/utils/Errors.kt +++ b/app/src/main/java/to/bitkit/utils/Errors.kt @@ -128,3 +128,7 @@ class LdkError(private val inner: LdkException) : AppError("Unknown LDK error.") } } // endregion + +/** Check if the throwable is a TxSyncTimeout exception. */ +fun Throwable.isTxSyncTimeout(): Boolean = + this is NodeException.TxSyncTimeout || cause is NodeException.TxSyncTimeout diff --git a/app/src/main/java/to/bitkit/utils/Logger.kt b/app/src/main/java/to/bitkit/utils/Logger.kt index ade0299aa..85aa44e1c 100644 --- a/app/src/main/java/to/bitkit/utils/Logger.kt +++ b/app/src/main/java/to/bitkit/utils/Logger.kt @@ -142,7 +142,7 @@ class LoggerImpl( path: String = getCallerPath(), line: Int = getCallerLine(), ) { - val errMsg = e?.let { "[${e::class.simpleName}='${e.message}']" }.orEmpty() + val errMsg = e?.let { errLogOf(it) }.orEmpty() val message = formatLog(LogLevel.WARN, "$msg $errMsg", context, path, line) if (compact) Log.w(tag, message) else Log.w(tag, message, e) saver.save(message) @@ -155,7 +155,7 @@ class LoggerImpl( path: String = getCallerPath(), line: Int = getCallerLine(), ) { - val errMsg = e?.let { "[${e::class.simpleName}='${e.message}']" }.orEmpty() + val errMsg = e?.let { errLogOf(it) }.orEmpty() val message = formatLog(LogLevel.ERROR, "$msg $errMsg", context, path, line) if (compact) Log.e(tag, message) else Log.e(tag, message, e) saver.save(message) @@ -339,3 +339,5 @@ val jsonLogger = Json(json) { inline fun jsonLogOf(value: T): String = with(jsonLogger) { encodeToString(serializersModule.serializer(), value) } + +fun errLogOf(e: Throwable): String = "[${e::class.simpleName}='${e.message}']" diff --git a/app/src/main/java/to/bitkit/utils/Perf.kt b/app/src/main/java/to/bitkit/utils/Perf.kt index 46a13c4d1..c79176d9b 100644 --- a/app/src/main/java/to/bitkit/utils/Perf.kt +++ b/app/src/main/java/to/bitkit/utils/Perf.kt @@ -4,17 +4,16 @@ import java.time.Instant import kotlin.system.measureTimeMillis internal inline fun measured( - functionName: String, + label: String, block: () -> T, ): T { var result: T - val elapsed = measureTimeMillis { + val elapsedMs = measureTimeMillis { result = block() - }.let { it / 1000.0 } + } - val threadName = Thread.currentThread().name - Logger.performance("$functionName took $elapsed seconds on $threadName") + Logger.debug("$label took ${elapsedMs}ms") return result } diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 41028fcd0..232d496dd 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -46,7 +46,6 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import kotlinx.datetime.Clock import org.lightningdevkit.ldknode.Event import org.lightningdevkit.ldknode.PaymentId import org.lightningdevkit.ldknode.SpendableUtxo @@ -69,6 +68,7 @@ import to.bitkit.ext.maxSendableSat import to.bitkit.ext.maxWithdrawableSat import to.bitkit.ext.minSendableSat import to.bitkit.ext.minWithdrawableSat +import to.bitkit.ext.nowMillis import to.bitkit.ext.rawId import to.bitkit.ext.removeSpaces import to.bitkit.ext.setClipboardText @@ -93,6 +93,7 @@ import to.bitkit.repositories.CurrencyRepo import to.bitkit.repositories.HealthRepo import to.bitkit.repositories.LightningRepo import to.bitkit.repositories.PreActivityMetadataRepo +import to.bitkit.repositories.TransferRepo import to.bitkit.repositories.WalletRepo import to.bitkit.services.AppUpdaterService import to.bitkit.ui.Routes @@ -106,14 +107,19 @@ import to.bitkit.utils.Logger import to.bitkit.utils.jsonLogOf import java.math.BigDecimal import javax.inject.Inject +import kotlin.coroutines.cancellation.CancellationException +import kotlin.time.Clock +import kotlin.time.ExperimentalTime +@OptIn(ExperimentalTime::class) @Suppress("LongParameterList") @HiltViewModel class AppViewModel @Inject constructor( connectivityRepo: ConnectivityRepo, healthRepo: HealthRepo, - @param:ApplicationContext private val context: Context, - @param:BgDispatcher private val bgDispatcher: CoroutineDispatcher, + toastManagerProvider: @JvmSuppressWildcards (CoroutineScope) -> ToastQueueManager, + @ApplicationContext private val context: Context, + @BgDispatcher private val bgDispatcher: CoroutineDispatcher, private val keychain: Keychain, private val lightningRepo: LightningRepo, private val walletRepo: WalletRepo, @@ -126,7 +132,7 @@ class AppViewModel @Inject constructor( private val appUpdaterService: AppUpdaterService, private val notifyPaymentReceivedHandler: NotifyPaymentReceivedHandler, private val cacheStore: CacheStore, - private val toastManagerProvider: @JvmSuppressWildcards (CoroutineScope) -> ToastQueueManager, + private val transferRepo: TransferRepo, ) : ViewModel() { val healthState = healthRepo.healthState @@ -241,9 +247,9 @@ class AppViewModel @Inject constructor( runCatching { when (event) { is Event.BalanceChanged -> handleBalanceChanged() - is Event.ChannelClosed -> Unit - is Event.ChannelPending -> Unit - is Event.ChannelReady -> notifyChannelReady(event) + is Event.ChannelClosed -> handleChannelClosed() + is Event.ChannelPending -> handleChannelPending() + is Event.ChannelReady -> handleChannelReady(event) is Event.OnchainTransactionConfirmed -> handleOnchainTransactionConfirmed(event) is Event.OnchainTransactionEvicted -> handleOnchainTransactionEvicted(event) is Event.OnchainTransactionReceived -> handleOnchainTransactionReceived(event) @@ -260,6 +266,7 @@ class AppViewModel @Inject constructor( is Event.SyncProgress -> Unit } }.onFailure { e -> + if (e is CancellationException) throw e Logger.error("LDK event handler error", e, context = TAG) } } @@ -267,12 +274,24 @@ class AppViewModel @Inject constructor( private suspend fun handleBalanceChanged() { walletRepo.syncBalances() + transferRepo.syncTransferStates() } - private suspend fun handleSyncCompleted() { - walletRepo.syncNodeAndWallet() + private suspend fun handleChannelReady(event: Event.ChannelReady) { + transferRepo.syncTransferStates() + walletRepo.syncBalances() + notifyChannelReady(event) } + private suspend fun handleChannelPending() = transferRepo.syncTransferStates() + + private suspend fun handleChannelClosed() { + transferRepo.syncTransferStates() + walletRepo.syncBalances() + } + + private fun handleSyncCompleted() = walletRepo.debounceSyncByEvent() + private suspend fun handleOnchainTransactionConfirmed(event: Event.OnchainTransactionConfirmed) { activityRepo.handleOnchainTransactionConfirmed(event.txid, event.details) } @@ -1706,7 +1725,8 @@ class AppViewModel @Inject constructor( handleScan(data.removeLightningSchemes()) } - // Todo Temporaary fix while these schemes can't be decoded + // TODO Temporary fix while these schemes can't be decoded + @Suppress("SpellCheckingInspection") private fun String.removeLightningSchemes(): String { return this .replace("lnurl:", "") @@ -1769,31 +1789,29 @@ class AppViewModel @Inject constructor( } viewModelScope.launch { - val currentTime = Clock.System.now().toEpochMilliseconds() + val currentTime = nowMillis() when (currentSheet) { - TimedSheetType.BACKUP -> { - settingsStore.update { it.copy(backupWarningIgnoredMillis = currentTime) } + TimedSheetType.HIGH_BALANCE -> settingsStore.update { + it.copy( + balanceWarningTimes = it.balanceWarningTimes + 1, + balanceWarningIgnoredMillis = currentTime, + ) } - TimedSheetType.HIGH_BALANCE -> { - settingsStore.update { - it.copy( - balanceWarningTimes = it.balanceWarningTimes + 1, - balanceWarningIgnoredMillis = currentTime - ) - } + TimedSheetType.NOTIFICATIONS -> settingsStore.update { + it.copy(notificationsIgnoredMillis = currentTime) } - TimedSheetType.APP_UPDATE -> Unit - - TimedSheetType.NOTIFICATIONS -> { - settingsStore.update { it.copy(notificationsIgnoredMillis = currentTime) } + TimedSheetType.BACKUP -> settingsStore.update { + it.copy(backupWarningIgnoredMillis = currentTime) } - TimedSheetType.QUICK_PAY -> { - settingsStore.update { it.copy(quickPayIntroSeen = true) } + TimedSheetType.QUICK_PAY -> settingsStore.update { + it.copy(quickPayIntroSeen = true) } + + TimedSheetType.APP_UPDATE -> Unit } } diff --git a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt index 96a36b22c..1aa39e6a8 100644 --- a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt @@ -24,7 +24,6 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeoutOrNull -import kotlinx.datetime.Clock import org.lightningdevkit.ldknode.ChannelDetails import to.bitkit.R import to.bitkit.data.CacheStore @@ -43,13 +42,16 @@ import to.bitkit.utils.Logger import javax.inject.Inject import kotlin.math.min import kotlin.math.roundToLong +import kotlin.time.Clock import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds +import kotlin.time.ExperimentalTime const val RETRY_INTERVAL_MS = 1 * 60 * 1000L // 1 minutes in ms const val GIVE_UP_MS = 30 * 60 * 1000L // 30 minutes in ms @Suppress("LongParameterList") +@OptIn(ExperimentalTime::class) @HiltViewModel class TransferViewModel @Inject constructor( @ApplicationContext private val context: Context, @@ -208,7 +210,7 @@ class TransferViewModel @Inject constructor( lspOrderId = order.id, ) launch { walletRepo.syncBalances() } - watchOrder(order.id) + launch { watchOrder(order.id) } } .onFailure { error -> ToastEventBus.send(error) @@ -216,47 +218,67 @@ class TransferViewModel @Inject constructor( } } - private fun watchOrder(orderId: String, frequencyMs: Long = 2_500) { - var isSettled = false - var error: Throwable? = null - - viewModelScope.launch { - Logger.debug("Started to watch order '$orderId'", context = TAG) - - while (!isSettled && error == null) { - try { - Logger.debug("Refreshing order '$orderId'") - val order = blocktankRepo.getOrder(orderId, refresh = true).getOrNull() - if (order == null) { - error = Exception("Order not found '$orderId'").also { - Logger.error(it.message, context = TAG) - } - break - } - - val step = updateOrder(order) - settingsStore.update { it.copy(lightningSetupStep = step) } - Logger.debug("LN setup step: $step") + private suspend fun watchOrder(orderId: String): Result { + Logger.debug("Started watching order: '$orderId'", context = TAG) + try { + // Step 0: Starting + settingsStore.update { it.copy(lightningSetupStep = LN_SETUP_STEP_0) } + Logger.debug("LN setup step: $LN_SETUP_STEP_0", context = TAG) + delay(MIN_STEP_DELAY_MS) + + // Poll until payment is confirmed (order state becomes PAID or EXECUTED) + val paidOrder = pollUntil(orderId) { order -> + order.state2 == BtOrderState2.PAID || order.state2 == BtOrderState2.EXECUTED + } ?: return Result.failure(Exception("Order not found or expired")) + + // Step 1: Payment confirmed + settingsStore.update { it.copy(lightningSetupStep = LN_SETUP_STEP_1) } + Logger.debug("LN setup step: $LN_SETUP_STEP_1", context = TAG) + delay(MIN_STEP_DELAY_MS) + + // Try to open channel (idempotent - safe to call multiple times) + blocktankRepo.openChannel(paidOrder.id) + + // Step 2: Channel opening requested + settingsStore.update { it.copy(lightningSetupStep = LN_SETUP_STEP_2) } + Logger.debug("LN setup step: $LN_SETUP_STEP_2", context = TAG) + delay(MIN_STEP_DELAY_MS) + + // Poll until channel is ready (EXECUTED state or channel has state) + pollUntil(orderId) { order -> + order.state2 == BtOrderState2.EXECUTED || order.channel?.state != null + } ?: return Result.failure(Exception("Order not found or expired")) + + // Step 3: Complete + transferRepo.syncTransferStates() + settingsStore.update { it.copy(lightningSetupStep = LN_SETUP_STEP_3) } + Logger.debug("LN setup step: $LN_SETUP_STEP_3", context = TAG) + + Logger.debug("Order settled: '$orderId'", context = TAG) + return Result.success(true) + } catch (e: Throwable) { + Logger.error("Failed to watch order: '$orderId'", e, context = TAG) + return Result.failure(e) + } finally { + Logger.debug("Stopped watching order: '$orderId'", context = TAG) + } + } - if (order.state2 == BtOrderState2.EXPIRED) { - error = Exception("Order expired '$orderId'").also { - Logger.error(it.message, context = TAG) - } - break - } - if (step > 2) { - Logger.debug("Order settled, stopping watch order '$orderId'", context = TAG) - isSettled = true - break - } - } catch (e: Throwable) { - Logger.error("Failed to watch order '$orderId'", e, context = TAG) - error = e - break - } - delay(frequencyMs) + private suspend fun pollUntil(orderId: String, condition: (IBtOrder) -> Boolean): IBtOrder? { + while (true) { + val order = blocktankRepo.getOrder(orderId, refresh = true).getOrNull() + if (order == null) { + Logger.error("Order not found: '$orderId'", context = TAG) + return null } - Logger.debug("Stopped watching order '$orderId'", context = TAG) + if (order.state2 == BtOrderState2.EXPIRED) { + Logger.error("Order expired: '$orderId'", context = TAG) + return null + } + if (condition(order)) { + return order + } + delay(POLL_INTERVAL_MS) } } @@ -306,26 +328,6 @@ class TransferViewModel @Inject constructor( } } - private suspend fun updateOrder(order: IBtOrder): Int { - if (order.channel != null) { - transferRepo.syncTransferStates() - return LN_SETUP_STEP_3 - } - - when (order.state2) { - BtOrderState2.CREATED -> return 0 - - BtOrderState2.PAID -> { - blocktankRepo.openChannel(order.id) - return 1 - } - - BtOrderState2.EXECUTED -> return 2 - - else -> return 0 - } - } - fun onUseDefaultLspBalanceClick() { val defaultOrder = _spendingUiState.value.defaultOrder _spendingUiState.update { it.copy(order = defaultOrder, defaultOrder = null, isAdvanced = false) } @@ -490,6 +492,11 @@ class TransferViewModel @Inject constructor( companion object { private const val TAG = "TransferViewModel" + private const val MIN_STEP_DELAY_MS = 500L + private const val POLL_INTERVAL_MS = 2_500L + const val LN_SETUP_STEP_0 = 0 + const val LN_SETUP_STEP_1 = 1 + const val LN_SETUP_STEP_2 = 2 const val LN_SETUP_STEP_3 = 3 } } diff --git a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt index ff03f0084..af73379ec 100644 --- a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt @@ -7,11 +7,9 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.Job import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map @@ -28,12 +26,14 @@ import to.bitkit.repositories.BackupRepo import to.bitkit.repositories.BlocktankRepo import to.bitkit.repositories.LightningRepo import to.bitkit.repositories.RecoveryModeException +import to.bitkit.repositories.SyncSource import to.bitkit.repositories.WalletRepo import to.bitkit.ui.onboarding.LOADING_MS import to.bitkit.ui.shared.toast.ToastEventBus import to.bitkit.utils.Logger -import to.bitkit.utils.ServiceError +import to.bitkit.utils.isTxSyncTimeout import javax.inject.Inject +import kotlin.coroutines.cancellation.CancellationException import kotlin.time.Duration.Companion.milliseconds @HiltViewModel @@ -64,9 +64,7 @@ class WalletViewModel @Inject constructor( @Deprecated("Prioritize get the wallet and lightning states from LightningRepo or WalletRepo") val uiState = _uiState.asStateFlow() - private val _walletEffect = MutableSharedFlow(extraBufferCapacity = 1) - val walletEffect = _walletEffect.asSharedFlow() - private fun walletEffect(effect: WalletViewModelEffects) = viewModelScope.launch { _walletEffect.emit(effect) } + private var syncJob: Job? = null init { if (walletExists) { @@ -142,7 +140,7 @@ class WalletViewModel @Inject constructor( .onSuccess { walletRepo.setWalletExistsState() walletRepo.syncBalances() - // Skip refreshing during restore, it will be called when it completes + // Skip refresh during restore, it will be called after completion if (restoreState.isIdle()) { walletRepo.refreshBip21() } @@ -172,17 +170,24 @@ class WalletViewModel @Inject constructor( walletRepo.syncNodeAndWallet() .onFailure { error -> Logger.error("Failed to refresh state: ${error.message}", error) - if (error !is TimeoutCancellationException) { - ToastEventBus.send(error) - } + if (error is CancellationException || error.isTxSyncTimeout()) return@onFailure + ToastEventBus.send(error) } } fun onPullToRefresh() { - viewModelScope.launch { + // Cancel any existing sync, manual or event triggered + syncJob?.cancel() + walletRepo.cancelSyncByEvent() + lightningRepo.clearPendingSync() + + syncJob = viewModelScope.launch { _uiState.update { it.copy(isRefreshing = true) } - refreshState().join() - _uiState.update { it.copy(isRefreshing = false) } + try { + walletRepo.syncNodeAndWallet(source = SyncSource.MANUAL) + } finally { + _uiState.update { it.copy(isRefreshing = false) } + } } } @@ -220,21 +225,6 @@ class WalletViewModel @Inject constructor( } } - fun toggleReceiveOnSpending() { - viewModelScope.launch { - walletRepo.toggleReceiveOnSpendingBalance() - .onSuccess { - updateBip21Invoice() - }.onFailure { e -> - if (e is ServiceError.GeoBlocked) { - walletEffect(WalletViewModelEffects.NavigateGeoBlockScreen) - return@launch - } - updateBip21Invoice() - } - } - } - fun refreshReceiveState() = viewModelScope.launch { launch { blocktankRepo.refreshInfo() } lightningRepo.updateGeoBlockState() @@ -290,10 +280,6 @@ class WalletViewModel @Inject constructor( walletRepo.resetPreActivityMetadataTagsForCurrentInvoice() } - fun loadTagsForCurrentInvoice() = viewModelScope.launch { - walletRepo.loadTagsForCurrentInvoice() - } - fun updateBip21Description(newText: String) { if (newText.isEmpty()) { Logger.warn("Empty") @@ -326,10 +312,6 @@ data class MainUiState( val selectedTags: List = listOf(), ) -sealed interface WalletViewModelEffects { - data object NavigateGeoBlockScreen : WalletViewModelEffects -} - sealed interface RestoreState { data object Initial : RestoreState sealed interface InProgress : RestoreState { diff --git a/app/src/main/res/drawable/ic_share.xml b/app/src/main/res/drawable/ic_share.xml index b7aa4ce87..e63379543 100644 --- a/app/src/main/res/drawable/ic_share.xml +++ b/app/src/main/res/drawable/ic_share.xml @@ -1,19 +1,18 @@ + android:width="16dp" + android:height="16dp" + android:viewportWidth="16" + android:viewportHeight="16"> + android:pathData="M10.646,3.146C10.842,2.951 11.158,2.951 11.354,3.146L14.354,6.146C14.549,6.342 14.549,6.658 14.354,6.854L11.354,9.854C11.158,10.049 10.842,10.049 10.646,9.854C10.451,9.658 10.451,9.342 10.646,9.146L13.293,6.5L10.646,3.854C10.451,3.658 10.451,3.342 10.646,3.146Z" + android:fillColor="#FF4400" + android:fillType="evenOdd"/> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3950ca793..41729a2dc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1078,6 +1078,7 @@ The transaction was successfully boosted. Boost Failed Bitkit was unable to boost the transaction. + Unable to increase the fee any further. Otherwise, it will exceed half the current input balance Your transaction may settle faster if you include an additional network fee. Set your custom fee below. Your transaction may settle faster if you include an additional network fee. Here is a recommendation: Use Suggested Fee diff --git a/app/src/test/java/to/bitkit/androidServices/LightningNodeServiceTest.kt b/app/src/test/java/to/bitkit/androidServices/LightningNodeServiceTest.kt index 3f2169582..e79fa9b05 100644 --- a/app/src/test/java/to/bitkit/androidServices/LightningNodeServiceTest.kt +++ b/app/src/test/java/to/bitkit/androidServices/LightningNodeServiceTest.kt @@ -12,7 +12,6 @@ import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltTestApplication import dagger.hilt.android.testing.UninstallModules -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.runBlocking import org.junit.After @@ -41,10 +40,8 @@ import to.bitkit.CurrentActivity import to.bitkit.R import to.bitkit.data.AppCacheData import to.bitkit.data.CacheStore -import to.bitkit.di.BgDispatcher +import to.bitkit.di.DbModule import to.bitkit.di.DispatchersModule -import to.bitkit.di.IoDispatcher -import to.bitkit.di.UiDispatcher import to.bitkit.domain.commands.NotifyPaymentReceived import to.bitkit.domain.commands.NotifyPaymentReceivedHandler import to.bitkit.models.NewTransactionSheetDetails @@ -57,58 +54,35 @@ import to.bitkit.services.NodeEventHandler import to.bitkit.test.BaseUnitTest @HiltAndroidTest -@UninstallModules(DispatchersModule::class) -@Config(application = HiltTestApplication::class) +@UninstallModules(DispatchersModule::class, DbModule::class) +@Config(application = HiltTestApplication::class, sdk = [34]) // Pin Robolectric to an SDK that supports Java 17 @RunWith(RobolectricTestRunner::class) class LightningNodeServiceTest : BaseUnitTest() { - @get:Rule(order = 0) - val mainDispatcherRule = coroutinesTestRule - @get:Rule(order = 1) var hiltRule = HiltAndroidRule(this) @BindValue - @JvmField - val lightningRepo: LightningRepo = mock() - - @BindValue - @JvmField - val walletRepo: WalletRepo = mock() - - @BindValue - @JvmField - val notifyPaymentReceivedHandler: NotifyPaymentReceivedHandler = mock() - - @BindValue - @JvmField - val cacheStore: CacheStore = mock() + val lightningRepo = mock() @BindValue - @UiDispatcher - @JvmField - val uiDispatcher: CoroutineDispatcher = testDispatcher + val walletRepo = mock() @BindValue - @BgDispatcher - @JvmField - val bgDispatcher: CoroutineDispatcher = testDispatcher + val notifyPaymentReceivedHandler = mock() @BindValue - @IoDispatcher - @JvmField - val ioDispatcher: CoroutineDispatcher = testDispatcher + val cacheStore = mock() - private val eventHandlerCaptor: KArgumentCaptor = argumentCaptor() + private val captor: KArgumentCaptor = argumentCaptor() private val cacheDataFlow = MutableSharedFlow(replay = 1) private val context = ApplicationProvider.getApplicationContext() @Before fun setUp() = runBlocking { hiltRule.inject() - whenever( - lightningRepo.start(any(), anyOrNull(), any(), anyOrNull(), anyOrNull(), eventHandlerCaptor.capture()) - ).thenReturn(Result.success(Unit)) + whenever(lightningRepo.start(any(), anyOrNull(), any(), anyOrNull(), anyOrNull(), captor.capture())) + .thenReturn(Result.success(Unit)) whenever(lightningRepo.stop()).thenReturn(Result.success(Unit)) // Set up CacheStore mock @@ -125,9 +99,8 @@ class LightningNodeServiceTest : BaseUnitTest() { title = context.getString(R.string.notification_received_title), body = "Received ₿ 100 ($0.10)", ) - whenever(notifyPaymentReceivedHandler.invoke(any())).thenReturn( - Result.success(NotifyPaymentReceived.Result.ShowNotification(sheet, notification)) - ) + whenever(notifyPaymentReceivedHandler.invoke(any())) + .thenReturn(Result.success(NotifyPaymentReceived.Result.ShowNotification(sheet, notification))) // Grant permissions for notifications val app = context as Application @@ -148,7 +121,7 @@ class LightningNodeServiceTest : BaseUnitTest() { controller.create().startCommand(0, 0) testScheduler.advanceUntilIdle() - val capturedHandler = eventHandlerCaptor.lastValue + val capturedHandler = captor.lastValue assertNotNull("Event handler should be captured", capturedHandler) val event = Event.PaymentReceived( @@ -195,7 +168,7 @@ class LightningNodeServiceTest : BaseUnitTest() { customRecords = emptyList() ) - eventHandlerCaptor.lastValue?.invoke(event) + captor.lastValue?.invoke(event) testScheduler.advanceUntilIdle() val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager @@ -223,7 +196,7 @@ class LightningNodeServiceTest : BaseUnitTest() { customRecords = emptyList() ) - eventHandlerCaptor.lastValue?.invoke(event) + captor.lastValue?.invoke(event) testScheduler.advanceUntilIdle() val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager @@ -235,6 +208,6 @@ class LightningNodeServiceTest : BaseUnitTest() { assertNotNull("Payment notification should be present", paymentNotification) val body = paymentNotification?.extras?.getString(Notification.EXTRA_TEXT) - assertEquals("Received ₿ 100 (\$0.10)", body) + assertEquals($$"Received ₿ 100 ($0.10)", body) } } diff --git a/app/src/test/java/to/bitkit/di/TestModule.kt b/app/src/test/java/to/bitkit/di/TestModule.kt new file mode 100644 index 000000000..4e97cc2a8 --- /dev/null +++ b/app/src/test/java/to/bitkit/di/TestModule.kt @@ -0,0 +1,56 @@ +package to.bitkit.di + +import android.content.Context +import androidx.room.Room +import dagger.Module +import dagger.Provides +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import dagger.hilt.testing.TestInstallIn +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import to.bitkit.data.AppDb +import to.bitkit.data.dao.TransferDao +import javax.inject.Singleton + +@OptIn(ExperimentalCoroutinesApi::class) +@Module +@TestInstallIn( + components = [SingletonComponent::class], + replaces = [DispatchersModule::class, DbModule::class] +) +object TestModule { + + @Provides + @Singleton + @UiDispatcher + fun provideUiDispatcher(): CoroutineDispatcher = UnconfinedTestDispatcher() + + @Provides + @Singleton + @IoDispatcher + fun provideIoDispatcher(): CoroutineDispatcher = UnconfinedTestDispatcher() + + @Provides + @Singleton + @BgDispatcher + fun provideBgDispatcher(): CoroutineDispatcher = UnconfinedTestDispatcher() + + @Provides + @Singleton + fun provideAppDb( + @ApplicationContext context: Context, + ): AppDb { + return Room.inMemoryDatabaseBuilder( + context, + AppDb::class.java + ) + .allowMainThreadQueries() + .build() + } + + @Provides + @Singleton + fun provideTransferDao(db: AppDb): TransferDao = db.transferDao() +} diff --git a/app/src/test/java/to/bitkit/ext/DateTimeExtTest.kt b/app/src/test/java/to/bitkit/ext/DateTimeExtTest.kt index 3fcd5408e..f748a3ee7 100644 --- a/app/src/test/java/to/bitkit/ext/DateTimeExtTest.kt +++ b/app/src/test/java/to/bitkit/ext/DateTimeExtTest.kt @@ -8,7 +8,9 @@ import java.util.concurrent.TimeUnit import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue +import kotlin.time.ExperimentalTime +@OptIn(ExperimentalTime::class) class DateTimeExtTest : BaseUnitTest() { @Test diff --git a/app/src/test/java/to/bitkit/repositories/ActivityDetailViewModelTest.kt b/app/src/test/java/to/bitkit/repositories/ActivityDetailViewModelTest.kt index 5fb886635..f2d301e85 100644 --- a/app/src/test/java/to/bitkit/repositories/ActivityDetailViewModelTest.kt +++ b/app/src/test/java/to/bitkit/repositories/ActivityDetailViewModelTest.kt @@ -10,9 +10,11 @@ import org.junit.Before import org.junit.Test import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock +import org.mockito.kotlin.mockingDetails import org.mockito.kotlin.whenever import to.bitkit.R import to.bitkit.data.SettingsStore +import to.bitkit.ext.create import to.bitkit.test.BaseUnitTest import to.bitkit.viewmodels.ActivityDetailViewModel import kotlin.test.assertEquals @@ -20,6 +22,7 @@ import kotlin.test.assertNull import kotlin.test.assertTrue class ActivityDetailViewModelTest : BaseUnitTest() { + private lateinit var sut: ActivityDetailViewModel private val context = mock() private val activityRepo = mock() @@ -27,14 +30,15 @@ class ActivityDetailViewModelTest : BaseUnitTest() { private val settingsStore = mock() private val lightningRepo = mock() - private lateinit var sut: ActivityDetailViewModel + companion object Fixtures { + const val ACTIVITY_ID = "test-activity-1" + const val ORDER_ID = "test-order-id" + } @Before fun setUp() { - whenever(context.getString(R.string.wallet__activity_error_not_found)) - .thenReturn("Activity not found") - whenever(context.getString(R.string.wallet__activity_error_load_failed)) - .thenReturn("Failed to load activity") + whenever(context.getString(R.string.wallet__activity_error_not_found)).thenReturn("Activity not found") + whenever(context.getString(R.string.wallet__activity_error_load_failed)).thenReturn("Failed to load activity") whenever(blocktankRepo.blocktankState).thenReturn(MutableStateFlow(BlocktankState())) whenever(activityRepo.activitiesChanged).thenReturn(MutableStateFlow(System.currentTimeMillis())) @@ -57,41 +61,27 @@ class ActivityDetailViewModelTest : BaseUnitTest() { @Test fun `findOrderForTransfer finds order by channelId`() = test { - val orderId = "test-order-id" - val mockOrder = mock { - on { id } doReturn orderId - } - - whenever(blocktankRepo.blocktankState).thenReturn( - MutableStateFlow(BlocktankState(orders = listOf(mockOrder))) - ) + val order = mock { on { id } doReturn ORDER_ID } + whenever(blocktankRepo.blocktankState).thenReturn(MutableStateFlow(BlocktankState(orders = listOf(order)))) - val result = sut.findOrderForTransfer(orderId, null) + val result = sut.findOrderForTransfer(ORDER_ID, null) - assertEquals(mockOrder, result) + assertEquals(order, result) } @Test fun `findOrderForTransfer finds order by channelId matching order id`() = test { - val orderId = "order-123" - val mockOrder = mock { - on { id } doReturn orderId - } + val order = mock { on { id } doReturn ORDER_ID } + whenever(blocktankRepo.blocktankState).thenReturn(MutableStateFlow(BlocktankState(orders = listOf(order)))) - whenever(blocktankRepo.blocktankState).thenReturn( - MutableStateFlow(BlocktankState(orders = listOf(mockOrder))) - ) - - val result = sut.findOrderForTransfer(orderId, null) + val result = sut.findOrderForTransfer(ORDER_ID, null) - assertEquals(mockOrder, result) + assertEquals(order, result) } @Test fun `findOrderForTransfer returns null when order not found`() = test { - whenever(blocktankRepo.blocktankState).thenReturn( - MutableStateFlow(BlocktankState(orders = emptyList())) - ) + whenever(blocktankRepo.blocktankState).thenReturn(MutableStateFlow(BlocktankState(orders = emptyList()))) val result = sut.findOrderForTransfer("non-existent-id", null) @@ -100,97 +90,83 @@ class ActivityDetailViewModelTest : BaseUnitTest() { @Test fun `loadActivity starts observation of activity changes`() = test { - val activityId = "test-activity-1" - val initialActivity = createTestActivity(activityId, confirmed = false) - val updatedActivity = createTestActivity(activityId, confirmed = true) + val initialActivity = createTestActivity(ACTIVITY_ID, confirmed = false) + val updatedActivity = createTestActivity(ACTIVITY_ID, confirmed = true) val activitiesChangedFlow = MutableStateFlow(System.currentTimeMillis()) whenever(activityRepo.activitiesChanged).thenReturn(activitiesChangedFlow) - whenever(activityRepo.getActivity(activityId)) - .thenReturn(Result.success(initialActivity)) - whenever(activityRepo.getActivityTags(activityId)) - .thenReturn(Result.success(emptyList())) + whenever(activityRepo.getActivity(ACTIVITY_ID)).thenReturn(Result.success(initialActivity)) + whenever(activityRepo.getActivityTags(ACTIVITY_ID)).thenReturn(Result.success(emptyList())) // Load activity - sut.loadActivity(activityId) + sut.loadActivity(ACTIVITY_ID) // Verify initial state loaded val initialState = sut.uiState.value.activityLoadState assertTrue(initialState is ActivityDetailViewModel.ActivityLoadState.Success) - assertEquals(initialActivity, (initialState as ActivityDetailViewModel.ActivityLoadState.Success).activity) + assertEquals(initialActivity, initialState.activity) // Simulate activity update - whenever(activityRepo.getActivity(activityId)) - .thenReturn(Result.success(updatedActivity)) + whenever(activityRepo.getActivity(ACTIVITY_ID)).thenReturn(Result.success(updatedActivity)) activitiesChangedFlow.value = System.currentTimeMillis() // Verify ViewModel reflects updated activity val updatedState = sut.uiState.value.activityLoadState assertTrue(updatedState is ActivityDetailViewModel.ActivityLoadState.Success) - assertEquals(updatedActivity, (updatedState as ActivityDetailViewModel.ActivityLoadState.Success).activity) + assertEquals(updatedActivity, updatedState.activity) } @Test fun `clearActivityState stops observation`() = test { - val activityId = "test-activity-1" - val activity = createTestActivity(activityId) + val activity = createTestActivity(ACTIVITY_ID) val activitiesChangedFlow = MutableStateFlow(System.currentTimeMillis()) whenever(activityRepo.activitiesChanged).thenReturn(activitiesChangedFlow) - whenever(activityRepo.getActivity(activityId)) - .thenReturn(Result.success(activity)) - whenever(activityRepo.getActivityTags(activityId)) - .thenReturn(Result.success(emptyList())) + whenever(activityRepo.getActivity(ACTIVITY_ID)).thenReturn(Result.success(activity)) + whenever(activityRepo.getActivityTags(ACTIVITY_ID)).thenReturn(Result.success(emptyList())) // Load activity - sut.loadActivity(activityId) + sut.loadActivity(ACTIVITY_ID) // Clear state sut.clearActivityState() // Trigger activity change - val callCountBefore = org.mockito.kotlin.mockingDetails(activityRepo).invocations.size + val callCountBefore = mockingDetails(activityRepo).invocations.size activitiesChangedFlow.value = System.currentTimeMillis() // Verify no reload after clear (getActivity not called again) - val callCountAfter = org.mockito.kotlin.mockingDetails(activityRepo).invocations.size + val callCountAfter = mockingDetails(activityRepo).invocations.size assertEquals(callCountBefore, callCountAfter) } @Test fun `reloadActivity keeps last state on failure`() = test { - val activityId = "test-activity-1" - val activity = createTestActivity(activityId) + val activity = createTestActivity(ACTIVITY_ID) val activitiesChangedFlow = MutableStateFlow(System.currentTimeMillis()) whenever(activityRepo.activitiesChanged).thenReturn(activitiesChangedFlow) - whenever(activityRepo.getActivity(activityId)) - .thenReturn(Result.success(activity)) - whenever(activityRepo.getActivityTags(activityId)) - .thenReturn(Result.success(emptyList())) + whenever(activityRepo.getActivity(ACTIVITY_ID)).thenReturn(Result.success(activity)) + whenever(activityRepo.getActivityTags(ACTIVITY_ID)).thenReturn(Result.success(emptyList())) // Load activity - sut.loadActivity(activityId) + sut.loadActivity(ACTIVITY_ID) // Simulate reload failure - whenever(activityRepo.getActivity(activityId)) - .thenReturn(Result.failure(Exception("Network error"))) + whenever(activityRepo.getActivity(ACTIVITY_ID)).thenReturn(Result.failure(Exception("Network error"))) activitiesChangedFlow.value = System.currentTimeMillis() // Verify last known state is preserved val state = sut.uiState.value.activityLoadState assertTrue(state is ActivityDetailViewModel.ActivityLoadState.Success) - assertEquals(activity, (state as ActivityDetailViewModel.ActivityLoadState.Success).activity) + assertEquals(activity, state.activity) } @Test fun `loadActivity handles error gracefully`() = test { - val activityId = "test-activity-1" - - whenever(activityRepo.getActivity(activityId)) - .thenReturn(Result.failure(Exception("Database error"))) + whenever(activityRepo.getActivity(ACTIVITY_ID)).thenReturn(Result.failure(Exception("Database error"))) - sut.loadActivity(activityId) + sut.loadActivity(ACTIVITY_ID) val state = sut.uiState.value.activityLoadState assertTrue(state is ActivityDetailViewModel.ActivityLoadState.Error) @@ -201,7 +177,7 @@ class ActivityDetailViewModelTest : BaseUnitTest() { confirmed: Boolean = false, ): Activity.Onchain { return Activity.Onchain( - v1 = OnchainActivity( + v1 = OnchainActivity.create( id = id, txType = PaymentType.RECEIVED, txId = "tx-$id", @@ -211,15 +187,7 @@ class ActivityDetailViewModelTest : BaseUnitTest() { address = "bc1...", confirmed = confirmed, timestamp = (System.currentTimeMillis() / 1000).toULong(), - isBoosted = false, - boostTxIds = emptyList(), - isTransfer = false, - doesExist = true, confirmTimestamp = if (confirmed) (System.currentTimeMillis() / 1000).toULong() else null, - channelId = null, - transferTxId = null, - createdAt = null, - updatedAt = null, ) ) } diff --git a/app/src/test/java/to/bitkit/repositories/ActivityRepoTest.kt b/app/src/test/java/to/bitkit/repositories/ActivityRepoTest.kt index 87fb93a98..a6249d3cd 100644 --- a/app/src/test/java/to/bitkit/repositories/ActivityRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/ActivityRepoTest.kt @@ -8,7 +8,6 @@ import com.synonym.bitkitcore.PaymentType import com.synonym.bitkitcore.SortDirection import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf -import kotlinx.datetime.Clock import org.junit.Before import org.junit.Test import org.lightningdevkit.ldknode.PaymentDetails @@ -25,14 +24,18 @@ import org.mockito.kotlin.wheneverBlocking import to.bitkit.data.AppCacheData import to.bitkit.data.CacheStore import to.bitkit.data.dto.PendingBoostActivity +import to.bitkit.ext.create import to.bitkit.services.CoreService import to.bitkit.test.BaseUnitTest import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNull import kotlin.test.assertTrue +import kotlin.time.Clock +import kotlin.time.ExperimentalTime @Suppress("LargeClass") +@OptIn(ExperimentalTime::class) class ActivityRepoTest : BaseUnitTest() { private val coreService = mock() @@ -56,25 +59,15 @@ class ActivityRepoTest : BaseUnitTest() { on { v1 } doReturn testActivityV1 } - private val baseOnchainActivity = OnchainActivity( + private val baseOnchainActivity = OnchainActivity.create( id = "base_activity_id", txType = PaymentType.SENT, txId = "base_tx_id", value = 1000uL, fee = 100uL, - feeRate = 10uL, address = "bc1test", - confirmed = false, timestamp = 1234567890uL, - isBoosted = false, - boostTxIds = emptyList(), - isTransfer = false, - doesExist = true, - confirmTimestamp = null, - channelId = null, - transferTxId = null, - createdAt = null, - updatedAt = null + feeRate = 10uL, ) @Suppress("LongParameterList") @@ -140,7 +133,7 @@ class ActivityRepoTest : BaseUnitTest() { } private fun setupSyncActivitiesMocks( - cacheData: AppCacheData + cacheData: AppCacheData, ) { whenever(cacheStore.data).thenReturn(flowOf(cacheData)) wheneverBlocking { lightningRepo.getPayments() }.thenReturn(Result.success(emptyList())) diff --git a/app/src/test/java/to/bitkit/repositories/CurrencyRepoTest.kt b/app/src/test/java/to/bitkit/repositories/CurrencyRepoTest.kt index 2faadc35b..8cbdcdf19 100644 --- a/app/src/test/java/to/bitkit/repositories/CurrencyRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/CurrencyRepoTest.kt @@ -3,7 +3,6 @@ package to.bitkit.repositories import app.cash.turbine.test import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.take -import kotlinx.datetime.Clock import org.junit.Before import org.junit.Test import org.mockito.kotlin.mock @@ -18,24 +17,26 @@ import to.bitkit.models.FxRate import to.bitkit.models.PrimaryDisplay import to.bitkit.services.CurrencyService import to.bitkit.test.BaseUnitTest -import to.bitkit.ui.shared.toast.ToastEventBus import java.math.BigDecimal import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNull import kotlin.test.assertTrue +import kotlin.time.Clock import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.minutes +import kotlin.time.ExperimentalTime +@OptIn(ExperimentalTime::class) class CurrencyRepoTest : BaseUnitTest() { - private val currencyService: CurrencyService = mock() - private val settingsStore: SettingsStore = mock() - private val cacheStore: CacheStore = mock() - private val toastEventBus: ToastEventBus = mock() - private val clock: Clock = mock() + private val currencyService = mock() + private val settingsStore = mock() + private val cacheStore = mock() + private val clock = mock() private lateinit var sut: CurrencyRepo + @Suppress("SpellCheckingInspection") private val testRates = listOf( FxRate( symbol = "BTCUSD", @@ -149,9 +150,9 @@ class CurrencyRepoTest : BaseUnitTest() { awaitItem() // Wait for state to be initialized assertEquals(testRates[1], rate) - assertEquals(45000.0, rate?.rate) - assertEquals("€", rate?.currencySymbol) - assertEquals("🇪🇺", rate?.currencyFlag) + assertEquals(45_000.0, rate.rate) + assertEquals("€", rate.currencySymbol) + assertEquals("🇪🇺", rate.currencyFlag) cancelAndIgnoreRemainingEvents() } } diff --git a/app/src/test/java/to/bitkit/repositories/HealthRepoTest.kt b/app/src/test/java/to/bitkit/repositories/HealthRepoTest.kt index aa4b777f0..f0ecfef10 100644 --- a/app/src/test/java/to/bitkit/repositories/HealthRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/HealthRepoTest.kt @@ -4,8 +4,6 @@ import app.cash.turbine.test import com.synonym.bitkitcore.IBtOrder import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf -import kotlinx.datetime.Clock -import kotlinx.datetime.Instant import org.junit.Before import org.junit.Test import org.lightningdevkit.ldknode.ChannelDetails @@ -19,13 +17,17 @@ import to.bitkit.models.HealthState import to.bitkit.models.NodeLifecycleState import to.bitkit.test.BaseUnitTest import kotlin.test.assertEquals +import kotlin.time.Clock import kotlin.time.Duration.Companion.minutes +import kotlin.time.ExperimentalTime +import kotlin.time.Instant +@OptIn(ExperimentalTime::class) class HealthRepoTest : BaseUnitTest() { - private val connectivityRepo: ConnectivityRepo = mock() - private val lightningRepo: LightningRepo = mock() - private val blocktankRepo: BlocktankRepo = mock() - private val cacheStore: CacheStore = mock() + private val connectivityRepo = mock() + private val lightningRepo = mock() + private val blocktankRepo = mock() + private val cacheStore = mock() private val clock: Clock = mock() private lateinit var sut: HealthRepo diff --git a/app/src/test/java/to/bitkit/repositories/PreActivityMetadataRepoTest.kt b/app/src/test/java/to/bitkit/repositories/PreActivityMetadataRepoTest.kt index c18750972..139405fab 100644 --- a/app/src/test/java/to/bitkit/repositories/PreActivityMetadataRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/PreActivityMetadataRepoTest.kt @@ -4,8 +4,6 @@ import app.cash.turbine.test import com.synonym.bitkitcore.PreActivityMetadata import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceUntilIdle -import kotlinx.datetime.Clock -import kotlinx.datetime.Instant import org.junit.Before import org.junit.Test import org.mockito.kotlin.any @@ -21,8 +19,11 @@ import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue +import kotlin.time.Clock +import kotlin.time.ExperimentalTime +import kotlin.time.Instant -@OptIn(ExperimentalCoroutinesApi::class) +@OptIn(ExperimentalCoroutinesApi::class, ExperimentalTime::class) class PreActivityMetadataRepoTest : BaseUnitTest() { private val coreService = mock() diff --git a/app/src/test/java/to/bitkit/repositories/TransferRepoTest.kt b/app/src/test/java/to/bitkit/repositories/TransferRepoTest.kt index 08f63e09d..59ac8dae1 100644 --- a/app/src/test/java/to/bitkit/repositories/TransferRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/TransferRepoTest.kt @@ -6,7 +6,6 @@ import com.synonym.bitkitcore.IBtChannel import com.synonym.bitkitcore.IBtOrder import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf -import kotlinx.datetime.Clock import org.junit.Before import org.junit.Test import org.lightningdevkit.ldknode.BalanceDetails @@ -29,7 +28,10 @@ import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue +import kotlin.time.Clock +import kotlin.time.ExperimentalTime +@OptIn(ExperimentalTime::class) class TransferRepoTest : BaseUnitTest() { private lateinit var sut: TransferRepo diff --git a/app/src/test/java/to/bitkit/repositories/WalletRepoTest.kt b/app/src/test/java/to/bitkit/repositories/WalletRepoTest.kt index 97467c587..89017daf6 100644 --- a/app/src/test/java/to/bitkit/repositories/WalletRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/WalletRepoTest.kt @@ -44,6 +44,7 @@ class WalletRepoTest : BaseUnitTest() { private val preActivityMetadataRepo = mock() private val deriveBalanceStateUseCase = mock() private val wipeWalletUseCase = mock() + private val transferRepo = mock() companion object Fixtures { const val ACTIVITY_TAG = "testTag" @@ -107,6 +108,7 @@ class WalletRepoTest : BaseUnitTest() { preActivityMetadataRepo = preActivityMetadataRepo, deriveBalanceStateUseCase = deriveBalanceStateUseCase, wipeWalletUseCase = wipeWalletUseCase, + transferRepo = transferRepo, ) @Test diff --git a/app/src/test/java/to/bitkit/test/BaseUnitTest.kt b/app/src/test/java/to/bitkit/test/BaseUnitTest.kt index 9368759f8..ef07858b0 100644 --- a/app/src/test/java/to/bitkit/test/BaseUnitTest.kt +++ b/app/src/test/java/to/bitkit/test/BaseUnitTest.kt @@ -11,7 +11,7 @@ import org.junit.Rule abstract class BaseUnitTest( testDispatcher: TestDispatcher = UnconfinedTestDispatcher() ) { - @get:Rule + @get:Rule(order = 0) val coroutinesTestRule = MainDispatcherRule(testDispatcher) protected val testDispatcher get() = coroutinesTestRule.testDispatcher diff --git a/app/src/test/java/to/bitkit/ui/WalletViewModelTest.kt b/app/src/test/java/to/bitkit/ui/WalletViewModelTest.kt index dc83fc88a..8afe69d9a 100644 --- a/app/src/test/java/to/bitkit/ui/WalletViewModelTest.kt +++ b/app/src/test/java/to/bitkit/ui/WalletViewModelTest.kt @@ -20,6 +20,7 @@ import to.bitkit.repositories.BackupRepo import to.bitkit.repositories.BlocktankRepo import to.bitkit.repositories.LightningRepo import to.bitkit.repositories.LightningState +import to.bitkit.repositories.SyncSource import to.bitkit.repositories.WalletRepo import to.bitkit.repositories.WalletState import to.bitkit.test.BaseUnitTest @@ -28,23 +29,23 @@ import to.bitkit.viewmodels.WalletViewModel @OptIn(ExperimentalCoroutinesApi::class) class WalletViewModelTest : BaseUnitTest() { - private lateinit var sut: WalletViewModel - private val walletRepo: WalletRepo = mock() - private val lightningRepo: LightningRepo = mock() - private val settingsStore: SettingsStore = mock() - private val backupRepo: BackupRepo = mock() - private val blocktankRepo: BlocktankRepo = mock() - private val mockLightningState = MutableStateFlow(LightningState()) - private val mockWalletState = MutableStateFlow(WalletState()) - private val mockBalanceState = MutableStateFlow(BalanceState()) - private val mockIsRecoveryMode = MutableStateFlow(false) + private val walletRepo = mock() + private val lightningRepo = mock() + private val settingsStore = mock() + private val backupRepo = mock() + private val blocktankRepo = mock() + + private val lightningState = MutableStateFlow(LightningState()) + private val walletState = MutableStateFlow(WalletState()) + private val balanceState = MutableStateFlow(BalanceState()) + private val isRecoveryMode = MutableStateFlow(false) @Before fun setUp() { - whenever(walletRepo.walletState).thenReturn(mockWalletState) - whenever(lightningRepo.lightningState).thenReturn(mockLightningState) + whenever(walletRepo.walletState).thenReturn(walletState) + whenever(lightningRepo.lightningState).thenReturn(lightningState) sut = WalletViewModel( bgDispatcher = testDispatcher, @@ -82,7 +83,7 @@ class WalletViewModelTest : BaseUnitTest() { fun `onPullToRefresh should sync wallet`() = test { sut.onPullToRefresh() - verify(walletRepo).syncNodeAndWallet() + verify(walletRepo).syncNodeAndWallet(SyncSource.MANUAL) } @Test @@ -165,7 +166,7 @@ class WalletViewModelTest : BaseUnitTest() { fun `backup restore should not be triggered when wallet exists while not restoring`() = test { assertEquals(RestoreState.Initial, sut.restoreState) - mockWalletState.value = mockWalletState.value.copy(walletExists = true) + walletState.value = walletState.value.copy(walletExists = true) verify(backupRepo, never()).performFullRestoreFromLatestBackup() } @@ -173,7 +174,7 @@ class WalletViewModelTest : BaseUnitTest() { @Test fun `onBackupRestoreSuccess should reset restoreState`() = test { whenever(backupRepo.performFullRestoreFromLatestBackup()).thenReturn(Result.success(Unit)) - mockWalletState.value = mockWalletState.value.copy(walletExists = true) + walletState.value = walletState.value.copy(walletExists = true) sut.restoreWallet("mnemonic", "passphrase") assertEquals(RestoreState.InProgress.Wallet, sut.restoreState) @@ -187,7 +188,7 @@ class WalletViewModelTest : BaseUnitTest() { val testError = Exception("Test error") whenever(backupRepo.performFullRestoreFromLatestBackup()).thenReturn(Result.failure(testError)) sut.restoreWallet("mnemonic", "passphrase") - mockWalletState.value = mockWalletState.value.copy(walletExists = true) + walletState.value = walletState.value.copy(walletExists = true) assertEquals(RestoreState.Completed, sut.restoreState) sut.proceedWithoutRestore(onDone = {}) @@ -203,7 +204,7 @@ class WalletViewModelTest : BaseUnitTest() { sut.restoreWallet("mnemonic", "passphrase") assertEquals(RestoreState.InProgress.Wallet, sut.restoreState) - mockWalletState.value = mockWalletState.value.copy(walletExists = true) + walletState.value = walletState.value.copy(walletExists = true) assertEquals(RestoreState.Completed, sut.restoreState) sut.onRestoreContinue() @@ -221,10 +222,10 @@ class WalletViewModelTest : BaseUnitTest() { // Set up mocks BEFORE creating SUT whenever(testWalletRepo.walletState).thenReturn(testWalletState) - whenever(testWalletRepo.balanceState).thenReturn(mockBalanceState) + whenever(testWalletRepo.balanceState).thenReturn(balanceState) whenever(testWalletRepo.walletExists()).thenReturn(true) - whenever(testLightningRepo.lightningState).thenReturn(mockLightningState) - whenever(testLightningRepo.isRecoveryMode).thenReturn(mockIsRecoveryMode) + whenever(testLightningRepo.lightningState).thenReturn(lightningState) + whenever(testLightningRepo.isRecoveryMode).thenReturn(isRecoveryMode) whenever(testLightningRepo.start(any(), anyOrNull(), any(), anyOrNull(), anyOrNull(), anyOrNull())) .thenReturn(Result.success(Unit)) @@ -258,11 +259,11 @@ class WalletViewModelTest : BaseUnitTest() { // Set up mocks BEFORE creating SUT whenever(testWalletRepo.walletState).thenReturn(testWalletState) - whenever(testWalletRepo.balanceState).thenReturn(mockBalanceState) + whenever(testWalletRepo.balanceState).thenReturn(balanceState) whenever(testWalletRepo.walletExists()).thenReturn(true) whenever(testWalletRepo.restoreWallet(any(), anyOrNull())).thenReturn(Result.success(Unit)) - whenever(testLightningRepo.lightningState).thenReturn(mockLightningState) - whenever(testLightningRepo.isRecoveryMode).thenReturn(mockIsRecoveryMode) + whenever(testLightningRepo.lightningState).thenReturn(lightningState) + whenever(testLightningRepo.isRecoveryMode).thenReturn(isRecoveryMode) whenever(testLightningRepo.start(any(), anyOrNull(), any(), anyOrNull(), anyOrNull(), anyOrNull())) .thenReturn(Result.success(Unit)) diff --git a/app/src/test/java/to/bitkit/ui/sheets/BoostTransactionViewModelTest.kt b/app/src/test/java/to/bitkit/ui/sheets/BoostTransactionViewModelTest.kt index f25be6e2c..7cecba8f0 100644 --- a/app/src/test/java/to/bitkit/ui/sheets/BoostTransactionViewModelTest.kt +++ b/app/src/test/java/to/bitkit/ui/sheets/BoostTransactionViewModelTest.kt @@ -15,6 +15,7 @@ import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.mockito.kotlin.wheneverBlocking +import to.bitkit.ext.create import to.bitkit.models.TransactionSpeed import to.bitkit.repositories.ActivityRepo import to.bitkit.repositories.LightningRepo @@ -40,25 +41,15 @@ class BoostTransactionViewModelTest : BaseUnitTest() { private val testTotalFee = 1000UL private val testValue = 50000UL - private val mockOnchainActivity = OnchainActivity( + private val mockOnchainActivity = OnchainActivity.create( id = "test_id", txType = PaymentType.SENT, txId = mockTxId, value = testValue, fee = 500UL, - feeRate = 10UL, address = mockAddress, - confirmed = false, timestamp = 1234567890UL, - isBoosted = false, - boostTxIds = emptyList(), - isTransfer = false, - doesExist = true, - confirmTimestamp = null, - channelId = null, - transferTxId = null, - createdAt = null, - updatedAt = null + feeRate = 10UL, ) private val mockActivitySent = Activity.Onchain(v1 = mockOnchainActivity) diff --git a/app/src/test/java/to/bitkit/viewmodels/AmountInputViewModelTest.kt b/app/src/test/java/to/bitkit/viewmodels/AmountInputViewModelTest.kt index 2c8bac799..e36008926 100644 --- a/app/src/test/java/to/bitkit/viewmodels/AmountInputViewModelTest.kt +++ b/app/src/test/java/to/bitkit/viewmodels/AmountInputViewModelTest.kt @@ -2,7 +2,6 @@ package to.bitkit.viewmodels import kotlinx.coroutines.delay import kotlinx.coroutines.flow.flowOf -import kotlinx.datetime.Clock import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals import org.junit.Assert.assertNotNull @@ -29,17 +28,22 @@ import to.bitkit.ui.components.KEY_000 import to.bitkit.ui.components.KEY_DECIMAL import to.bitkit.ui.components.KEY_DELETE import to.bitkit.ui.components.NumberPadType +import kotlin.time.Clock import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.ExperimentalTime @Suppress("LargeClass") +@OptIn(ExperimentalTime::class) class AmountInputViewModelTest : BaseUnitTest() { private lateinit var viewModel: AmountInputViewModel - private val currencyService: CurrencyService = mock() - private val settingsStore: SettingsStore = mock() - private val cacheStore: CacheStore = mock() - private val clock: Clock = mock() private lateinit var currencyRepo: CurrencyRepo + private val currencyService = mock() + private val settingsStore = mock() + private val cacheStore = mock() + private val clock = mock() + + @Suppress("SpellCheckingInspection") private val testRates = listOf( FxRate( symbol = "BTCUSD", @@ -65,7 +69,7 @@ class AmountInputViewModelTest : BaseUnitTest() { settingsStore = settingsStore, cacheStore = cacheStore, enablePolling = false, - clock = clock + clock = clock, ) viewModel = AmountInputViewModel(currencyRepo) diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml index 746b40004..79b0e5414 100644 --- a/config/detekt/detekt.yml +++ b/config/detekt/detekt.yml @@ -2,10 +2,10 @@ build: maxIssues: 0 excludeCorrectable: false weights: - # complexity: 2 - # LongParameterList: 1 - # style: 1 - # comments: 1 + # complexity: 2 + # LongParameterList: 1 + # style: 1 + # comments: 1 config: validation: true @@ -34,11 +34,11 @@ processors: console-reports: active: true exclude: - - 'ProjectStatisticsReport' - - 'ComplexityReport' - - 'NotificationReport' - - 'FindingsReport' - - 'FileBasedFindingsReport' + - 'ProjectStatisticsReport' + - 'ComplexityReport' + - 'NotificationReport' + - 'FindingsReport' + - 'FileBasedFindingsReport' # - 'LiteFindingsReport' output-reports: @@ -67,7 +67,7 @@ comments: endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)' KDocReferencesNonPublicProperty: active: false - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] OutdatedDocumentation: active: false matchTypeParameters: true @@ -75,7 +75,7 @@ comments: allowParamOnConstructorProperties: false UndocumentedPublicClass: active: false - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] searchInNestedClass: true searchInInnerClass: true searchInInnerObject: true @@ -84,11 +84,11 @@ comments: ignoreDefaultCompanionObject: false UndocumentedPublicFunction: active: false - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] searchProtectedFunction: false UndocumentedPublicProperty: active: false - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] searchProtectedProperty: false complexity: @@ -123,7 +123,7 @@ complexity: - 'with' LabeledExpression: active: false - ignoredLabels: [] + ignoredLabels: [ ] LargeClass: active: true threshold: 600 @@ -138,7 +138,7 @@ complexity: constructorThreshold: 7 ignoreDefaultParameters: false ignoreDataClasses: true - ignoreAnnotatedParameter: [] + ignoreAnnotatedParameter: [ ] ignoreAnnotated: - 'Composable' MethodOverloading: @@ -164,14 +164,14 @@ complexity: active: false StringLiteralDuplication: active: false - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] threshold: 3 ignoreAnnotation: true excludeStringsWithLessThan5Characters: true ignoreStringsRegex: '$^' TooManyFunctions: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/ext/**'] + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/ext/**' ] thresholdInFiles: 11 thresholdInClasses: 11 thresholdInInterfaces: 11 @@ -180,7 +180,7 @@ complexity: ignoreDeprecated: true ignorePrivate: true ignoreOverridden: true - ignoreAnnotatedFunctions: ['Preview'] + ignoreAnnotatedFunctions: [ 'Preview' ] coroutines: active: true @@ -249,7 +249,7 @@ exceptions: - 'toString' InstanceOfCheckForException: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] NotImplementedDeclaration: active: false ObjectExtendsThrowable: @@ -275,7 +275,7 @@ exceptions: active: false ThrowingExceptionsWithoutMessageOrCause: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] exceptions: - 'ArrayIndexOutOfBoundsException' - 'Exception' @@ -290,7 +290,7 @@ exceptions: active: true TooGenericExceptionCaught: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] exceptionNames: - 'ArrayIndexOutOfBoundsException' - 'Error' @@ -327,7 +327,7 @@ naming: enumEntryPattern: '[A-Z][_a-zA-Z0-9]*' ForbiddenClassName: active: false - forbiddenName: [] + forbiddenName: [ ] FunctionMaxLength: active: false maximumFunctionNameLength: 30 @@ -336,7 +336,7 @@ naming: minimumFunctionNameLength: 3 FunctionNaming: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] functionPattern: '[a-z][a-zA-Z0-9]*' excludeClassPattern: '$^' ignoreAnnotated: @@ -407,10 +407,10 @@ performance: threshold: 3 ForEachOnRange: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] SpreadOperator: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] UnnecessaryPartOfBinaryExpression: active: false UnnecessaryTemporaryInstantiation: @@ -443,7 +443,7 @@ potential-bugs: - 'java.util.HashMap' ElseCaseInsteadOfExhaustiveWhen: active: false - ignoredSubjectTypes: [] + ignoredSubjectTypes: [ ] EqualsAlwaysReturnsTrueOrFalse: active: true EqualsWithHashCodeExist: @@ -469,7 +469,7 @@ potential-bugs: - 'kotlin.sequences.Sequence' - 'kotlinx.coroutines.flow.*Flow' - 'java.util.stream.*Stream' - ignoreFunctionCall: [] + ignoreFunctionCall: [ ] ImplicitDefaultLocale: active: true ImplicitUnitReturnType: @@ -483,13 +483,13 @@ potential-bugs: active: true LateinitUsage: active: false - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] ignoreOnClassesPattern: '' MapGetWithNotNullAssertionOperator: active: true MissingPackageDeclaration: active: false - excludes: ['**/*.kts'] + excludes: [ '**/*.kts' ] NullCheckOnMutableProperty: active: false NullableToStringCall: @@ -510,7 +510,7 @@ potential-bugs: active: true UnsafeCallOnNullableType: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] UnsafeCast: active: true UnusedUnaryOperator: @@ -601,7 +601,7 @@ style: allowedPatterns: '' ForbiddenImport: active: false - imports: [] + imports: [ ] forbiddenPatterns: '' ForbiddenMethodCall: active: false @@ -612,7 +612,7 @@ style: value: 'kotlin.io.println' ForbiddenSuppress: active: false - rules: [] + rules: [ ] ForbiddenVoid: active: true ignoreOverridden: false @@ -621,20 +621,14 @@ style: active: true ignoreOverridableFunction: true ignoreActualFunction: true - excludedFunctions: [] + excludedFunctions: [ ] LoopWithTooManyJumpStatements: active: true maxJumpCount: 1 MagicNumber: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.kts'] - ignoreNumbers: - - '-1' - - '0' - - '0.25' - - '0.5' - - '1' - - '2' + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.kts' ] + ignoreNumbers: [ '-1', '0', '0.25', '0.5', '1', '2', '3', '4', '5', '10', '25', '50', '100', '250', '500', '1000', '2500', '5000', '10000', '25000', '50000', '100000' ] ignoreHashCodeFunction: true ignorePropertyDeclaration: true ignoreLocalVariableDeclaration: true @@ -710,7 +704,7 @@ style: StringShouldBeRawString: active: false maxEscapedCharacterCount: 2 - ignoredCharacters: [] + ignoredCharacters: [ ] ThrowsCount: active: true max: 2 @@ -1080,42 +1074,89 @@ Compose: active: true ComposableNaming: active: true + # -- You can optionally disable the checks in this rule for regex matches against the composable name (e.g. molecule presenters) + # allowedComposableFunctionNames: .*Presenter,.*MoleculePresenter ComposableParamOrder: active: true + # -- You can optionally have a list of types to be treated as lambdas (e.g. typedefs or fun interfaces not picked up automatically) + # treatAsLambda: MyLambdaType CompositionLocalAllowlist: active: true + # -- You can optionally define a list of CompositionLocals that are allowed here + # allowedCompositionLocals: LocalSomething,LocalSomethingElse CompositionLocalNaming: active: true ContentEmitterReturningValues: active: true - ContentSlotReused: - active: true + # -- You can optionally add your own composables here + # contentEmitters: MyComposable,MyOtherComposable ContentTrailingLambda: active: true + # -- You can optionally have a list of types to be treated as lambdas (e.g. typedefs or fun interfaces not picked up automatically) + # treatAsLambda: MyLambdaType + # -- You can optionally have a list of types to be treated as composable lambdas (e.g. typedefs or fun interfaces not picked up automatically). + # -- The difference with treatAsLambda is that those need `@Composable` MyLambdaType in the definition, while these won't. + # treatAsComposableLambda: MyComposableLambdaType + ContentSlotReused: + active: true + # -- You can optionally have a list of types to be treated as composable lambdas (e.g. typedefs or fun interfaces not picked up automatically). + # -- The difference with treatAsLambda is that those need `@Composable` MyLambdaType in the definition, while these won't. + # treatAsComposableLambda: MyComposableLambdaType DefaultsVisibility: active: true LambdaParameterEventTrailing: active: true + # -- You can optionally add your own composables here + # contentEmitters: MyComposable,MyOtherComposable + # -- You can add composables here that you don't want to count as content emitters (e.g. custom dialogs or modals) + # contentEmittersDenylist: MyNonEmitterComposable LambdaParameterInRestartableEffect: active: true + # -- You can optionally have a list of types to be treated as lambdas (e.g. typedefs or fun interfaces not picked up automatically) + # treatAsLambda: MyLambdaType Material2: - active: false + active: false # Opt-in, disabled by default. Turn on if you want to disallow Material 2 usages. + # -- You can optionally allow parts of it, if you are in the middle of a migration. + # allowedFromM2: icons.Icons,TopAppBar ModifierClickableOrder: active: true + # -- You can optionally add your own Modifier types + # customModifiers: BananaModifier,PotatoModifier ModifierComposed: active: true + # -- You can optionally add your own Modifier types + # customModifiers: BananaModifier,PotatoModifier ModifierMissing: active: true + # -- You can optionally control the visibility of which composables to check for here + # -- Possible values are: `only_public`, `public_and_internal` and `all` (default is `only_public`) + # checkModifiersForVisibility: only_public + # -- You can optionally add your own Modifier types + # customModifiers: BananaModifier,PotatoModifier + # -- You can suppress this check in functions annotated with these annotations + # ignoreAnnotated: ['Potato', 'Banana'] ModifierNaming: active: true + # -- You can optionally add your own Modifier types + # customModifiers: BananaModifier,PotatoModifier ModifierNotUsedAtRoot: active: true + # -- You can optionally add your own composables here + # contentEmitters: MyComposable,MyOtherComposable + # -- You can optionally add your own Modifier types + # customModifiers: BananaModifier,PotatoModifier ModifierReused: active: true + # -- You can optionally add your own Modifier types + # customModifiers: BananaModifier,PotatoModifier ModifierWithoutDefault: active: true MultipleEmitters: active: true + # -- You can optionally add your own composables here that will count as content emitters + # contentEmitters: MyComposable,MyOtherComposable + # -- You can add composables here that you don't want to count as content emitters (e.g. custom dialogs or modals) + # contentEmittersDenylist: MyNonEmitterComposable MutableParams: active: true MutableStateAutoboxing: @@ -1124,19 +1165,34 @@ Compose: active: true ParameterNaming: active: true + # -- You can optionally have a list of types to be treated as lambdas (e.g. typedefs or fun interfaces not picked up automatically) + # treatAsLambda: MyLambdaType + # -- You can optionally add your allowed lambda names (e.g., usages from the past found in the official Compose code) + # allowedLambdaParameterNames: onSizeChanged, onGloballyPositioned PreviewAnnotationNaming: active: true PreviewNaming: - active: false + active: false # Opt-in, disabled by default. + # -- You can optionally configure the naming strategy for previews. + # -- Possible values are: `suffix`, `prefix`, `anywhere`. By default, it will be `suffix`. + # previewNamingStrategy: suffix PreviewPublic: active: true - RememberContentMissing: - active: true RememberMissing: active: true + RememberContentMissing: + active: true UnstableCollections: - active: false + active: false # Opt-in, disabled by default. Turn on if you want to enforce this (e.g. you have strong skipping disabled) ViewModelForwarding: - active: false + active: true + # -- You can optionally use this rule on things other than types ending in "ViewModel" or "Presenter" (which are the defaults). You can add your own via a regex here: + # allowedStateHolderNames: .*ViewModel,.*Presenter + # -- You can optionally add an allowlist for Composable names that won't be affected by this rule + # allowedForwarding: .*Content,.*FancyStuff + # -- You can optionally add an allowlist for ViewModel/StateHolder names that won't be affected by this rule + # allowedForwardingOfTypes: PotatoViewModel,(Apple|Banana)ViewModel,.*FancyViewModel ViewModelInjection: active: true + # -- You can optionally add your own ViewModel factories here + # viewModelFactories: hiltViewModel,potatoViewModel diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 967907178..9342bafe0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,55 +1,31 @@ [versions] -accompanistPermissions = "0.36.0" -activityCompose = "1.10.1" -agp = "8.13.0" -appcompat = "1.7.0" -barcodeScanning = "17.3.0" -biometric = "1.4.0-alpha02" -bouncyCastle = "1.79" -camera = "1.4.2" -composeBom = "2025.09.00" # https://developer.android.com/develop/ui/compose/bom/bom-mapping -constraintlayoutCompose = "1.1.1" -coreKtx = "1.15.0" -coreSplashscreen = "1.0.1" -datastorePrefs = "1.1.4" +agp = "8.12.0" +camera = "1.5.2" detekt = "1.23.8" -espressoCore = "3.6.1" -hilt = "2.55" -hiltAndroidx = "1.2.0" -junit = "4.13.2" -junitExt = "1.2.1" -kotlin = "2.1.10" -kotlinxDatetime = "0.6.2" -ksp = "2.1.10-1.0.31" -ktor = "3.1.1" -lifecycle = "2.8.7" -material = "1.12.0" -mockitoKotlin = "5.4.0" -navCompose = "2.8.9" -robolectric = "4.14.1" -room = "2.6.1" +hilt = "2.57.2" +hiltAndroidx = "1.3.0" +kotlin = "2.2.21" +ktor = "3.3.3" +lifecycle = "2.10.0" +navCompose = "2.9.6" +room = "2.8.4" slf4j = "2.0.17" -testAndroidx = "1.6.1" -turbine = "1.0.0" -workRuntimeKtx = "2.10.0" -zxing = "3.5.2" -lottieVersion = "6.6.4" -composeChartsVersion = "0.1.7" -haze = "1.6.9" +haze = "1.7.1" [libraries] -accompanist-pager-indicators = { module = "com.google.accompanist:accompanist-pager-indicators", version.ref = "accompanistPermissions" } -accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" } -activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } -appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } -barcode-scanning = { module = "com.google.mlkit:barcode-scanning", version.ref = "barcodeScanning" } -biometric = { module = "androidx.biometric:biometric", version.ref = "biometric" } -bitkit-core = { module = "com.synonym:bitkit-core-android", version = "0.1.31" } -bouncycastle-provider-jdk = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bouncyCastle" } +accompanist-pager-indicators = { module = "com.google.accompanist:accompanist-pager-indicators", version = "0.36.0" } +accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version = "0.37.3" } +activity-compose = { module = "androidx.activity:activity-compose", version = "1.12.1" } +appcompat = { module = "androidx.appcompat:appcompat", version = "1.7.1" } +barcode-scanning = { module = "com.google.mlkit:barcode-scanning", version = "17.3.0" } +biometric = { module = "androidx.biometric:biometric", version = "1.4.0-alpha04" } +bitkit-core = { module = "com.synonym:bitkit-core-android", version = "0.1.33" } +bouncycastle-provider-jdk = { module = "org.bouncycastle:bcprov-jdk18on", version = "1.83" } camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camera" } camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camera" } camera-view = { module = "androidx.camera:camera-view", version.ref = "camera" } -compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } +# https://developer.android.com/develop/ui/compose/bom/bom-mapping +compose-bom = { group = "androidx.compose", name = "compose-bom", version = "2025.12.00" } compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } compose-material3 = { module = "androidx.compose.material3:material3" } compose-ui = { group = "androidx.compose.ui", name = "ui" } @@ -58,13 +34,13 @@ compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } -constraintlayout-compose = { module = "androidx.constraintlayout:constraintlayout-compose", version.ref = "constraintlayoutCompose" } -core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" } -core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "coreSplashscreen" } -datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePrefs" } +constraintlayout-compose = { module = "androidx.constraintlayout:constraintlayout-compose", version = "1.1.1" } +core-ktx = { module = "androidx.core:core-ktx", version = "1.17.0" } +core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version = "1.2.0" } +datastore-preferences = { module = "androidx.datastore:datastore-preferences", version = "1.2.0" } detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } -detekt-compose-rules = { module = "io.nlopez.compose.rules:detekt", version = "0.4.23" } -firebase-bom = { module = "com.google.firebase:firebase-bom", version = "34.2.0" } +detekt-compose-rules = { module = "io.nlopez.compose.rules:detekt", version = "0.5.1" } +firebase-bom = { module = "com.google.firebase:firebase-bom", version = "34.6.0" } firebase-messaging = { module = "com.google.firebase:firebase-messaging" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" } @@ -72,25 +48,23 @@ hilt-android-testing = { module = "com.google.dagger:hilt-android-testing", vers hilt-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "hiltAndroidx" } hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltAndroidx" } hilt-work = { module = "androidx.hilt:hilt-work", version.ref = "hiltAndroidx" } -jna = { module = "net.java.dev.jna:jna", version = "5.17.0" } +jna = { module = "net.java.dev.jna:jna", version = "5.18.1" } kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "kotlin" } -kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" } +kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version = "0.7.1" } ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } -#ldk-node-android = { module = "org.lightningdevkit:ldk-node-android", version = "0.7.0" } # upstream -#ldk-node-android = { module = "org.lightningdevkit:ldk-node-android", version = "0.7.0" } # local -ldk-node-android = { module = "com.github.synonymdev:ldk-node", version = "v0.7.0" } # fork +ldk-node-android = { module = "com.github.synonymdev:ldk-node", version = "v0.7.0-rc.1" } # fork | local: remove `v` lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "lifecycle" } lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" } lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" } lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" } lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" } lifecycle-viewmodel-savedstate = { module = "androidx.lifecycle:lifecycle-viewmodel-savedstate", version.ref = "lifecycle" } -material = { module = "com.google.android.material:material", version.ref = "material" } +material = { module = "com.google.android.material:material", version = "1.13.0" } navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navCompose" } navigation-testing = { module = "androidx.navigation:navigation-testing", version.ref = "navCompose" } room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } @@ -99,19 +73,19 @@ room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = " room-testing = { group = "androidx.room", name = "room-testing", version.ref = "room" } slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j" } -test-core = { module = "androidx.test:core", version.ref = "testAndroidx" } +test-core = { module = "androidx.test:core", version = "1.7.0" } test-coroutines = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test" } -test-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" } -test-junit = { group = "junit", name = "junit", version.ref = "junit" } -test-junit-ext = { module = "androidx.test.ext:junit", version.ref = "junitExt" } -test-mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockitoKotlin" } -test-robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } -test-turbine = { group = "app.cash.turbine", name = "turbine", version.ref = "turbine" } -vss = { module = "com.synonym:vss-client-android", version = "0.3.0" } -work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "workRuntimeKtx" } -zxing = { module = "com.google.zxing:core", version.ref = "zxing" } -lottie = { module = "com.airbnb.android:lottie-compose", version.ref = "lottieVersion" } -charts = { module = "io.github.ehsannarmani:compose-charts", version.ref = "composeChartsVersion" } +test-espresso-core = { module = "androidx.test.espresso:espresso-core", version = "3.7.0" } +test-junit = { group = "junit", name = "junit", version = "4.13.2" } +test-junit-ext = { module = "androidx.test.ext:junit", version = "1.3.0" } +test-mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version = "6.1.0" } +test-robolectric = { module = "org.robolectric:robolectric", version = "4.16" } +test-turbine = { group = "app.cash.turbine", name = "turbine", version = "1.2.1" } +vss-client = { module = "com.synonym:vss-client-android", version = "0.4.0" } +work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version = "2.11.0" } +zxing = { module = "com.google.zxing:core", version = "3.5.4" } +lottie = { module = "com.airbnb.android:lottie-compose", version = "6.7.1" } +charts = { module = "io.github.ehsannarmani:compose-charts", version = "0.2.0" } haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" } haze-materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = "haze" } @@ -119,9 +93,9 @@ haze-materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = android-application = { id = "com.android.application", version.ref = "agp" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } -google-services = { id = "com.google.gms.google-services", version = "4.4.3" } +google-services = { id = "com.google.gms.google-services", version = "4.4.4" } hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } -ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +ksp = { id = "com.google.devtools.ksp", version = "2.3.2" } room = { id = "androidx.room", version.ref = "room" }