Skip to content

Commit ed0f594

Browse files
committed
feat: implement biometric authentication and reorganize core modules
- Implement biometric authentication for Android and iOS using a platform-agnostic `BiometricInteractor`. - Add `BiometricEnrollViewModel` and `BiometricDisableViewModel` to handle secure credential enrollment and management. - Introduce `BiometricEnrollDialog` and `BiometricDisableConfirmationDialog` UI components. - Update `SignInViewModel` and `SettingsViewModel` to support biometric unlock and configuration toggles. - Implement secure storage using Android Keystore (AES-GCM) with DataStore on Android, and Keychain with biometric access control on iOS. - Relocate the `:ui:shared` module to `:core:ui` and reorganize testing modules into a nested `:core:test` structure (`:common`, `:ui`, `:jvm`). - Introduce `AutofillInteractor` to decouple presentation logic from the Compose-specific `AutofillManager`. - Add `TooltipIconButton` common composable and apply it to action buttons in `NoteDetail`, `FileListScreen`, `MainScreen`, and `SignInScreen`. - Refine the `DeleteDialog` UI to use Material 3 error color schemes and updated button styles. - Update string resources to include biometric labels and refine empty state messaging. - Add UI test cases for biometric sign-in and settings flows. - Bump project version to `8.5.5` and update dependencies including Gradle (9.5.0), AGP (9.2.0), and Kotlin (2.3.21). - Update project documentation, architecture guides, and GitHub Actions workflows to reflect the new module structure.
1 parent d4cb7f5 commit ed0f594

231 files changed

Lines changed: 3004 additions & 476 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/kmp.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ jobs:
6262
strategy:
6363
fail-fast: false
6464
matrix:
65-
api-level: [ 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36 ]
65+
api-level: [ 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37 ]
6666
target: [default]
6767
steps:
6868
- name: Check out

‎.github/workflows/web.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ jobs:
4848
uses: actions/configure-pages@v6
4949
- name: Upload artifact
5050
if: ${{ steps.build.outcome != 'failure' || steps.dist.outcome != 'failure' }}
51-
uses: actions/upload-pages-artifact@v4
51+
uses: actions/upload-pages-artifact@v5
5252
with:
5353
path: ./app/web/build/dist/wasmJs/productionExecutable
5454

‎.gitignore‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,12 @@ app/android/note_room_key_store.jks
6565

6666
### iOS
6767
# Generated files
68-
/iosApp/iosApp.xcworkspace/xcuserdata/
69-
/iosApp/fastlane/report.xml
68+
/app/iosApp/iosApp.xcworkspace/xcuserdata/
69+
/app/iosApp/fastlane/report.xml
7070

7171
# Built application files
72-
/iosApp/iosApp.app.dSYM.zip
73-
/iosApp/iosApp.ipa
72+
/app/iosApp/iosApp.app.dSYM.zip
73+
/app/iosApp/iosApp.ipa
7474

7575
# Code signing files
7676
/app/iosApp/fastlane/28F5CB4337.json

‎AGENTS.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
## Quick Reference
99

1010
### Project Structure & Module Organization
11-
- Core: `core/domain`, `core/presentation`, `core/data/db-sqldelight` (default), `core/data/db-room` (optional), `core/test`.
11+
- Core: `core/domain`, `core/presentation`, `core/data/db-sqldelight` (default), `core/data/db-room` (optional), `core/test/common`.
1212
- Features: `feature/backup/{domain,ui}`, `feature/console/{domain,presentation,ui}`, `feature/file-explorer/data`.
13-
- UI: `ui/shared` (common Compose code and resources), `ui/test` (multiplatform Compose UI tests), `ui/test-jvm` (JVM-specific UI test utilities).
13+
- UI: `core/ui` (common Compose code and resources), `core/test/ui` (multiplatform Compose UI tests), `core/test/jvm` (JVM-specific UI test utilities).
1414
- Apps: `app/android`, `app/desktop`, `app/web`, `app/ios-kit` (CocoaPods framework), `app/iosApp` (Xcode project).
1515
- Tooling: `build-logic` (Gradle conventions), `thirdparty` (vendored modules), `gradle/libs.versions.toml` (versions).
1616
- Switch DB module via `gradle.properties` key `CORE_DATA_DB_MODULE`.
@@ -40,7 +40,7 @@ After making changes, AI agents must perform the following checks sequentially.
4040
- Compose: prefer state hoisting; keep @Composable functions small and previewable.
4141

4242
## Testing Guidelines
43-
- Unit tests: Kotlin Multiplatform `kotlin("test")`/JUnit. Run all with `./gradlew test` or per module (e.g., `:ui:shared:jvmTest`).
43+
- Unit tests: Kotlin Multiplatform `kotlin("test")`/JUnit. Run all with `./gradlew test` or per module (e.g., `:core:ui:jvmTest`).
4444
- Android UI tests: Espresso/Compose in `app/android/src/androidTest`. Run with `connectedCheck`.
4545
- Desktop UI tests: in `app/desktop/src/jvmTest` using `uiTestJUnit4`.
4646
- iOS UI tests: Multiplatform Compose UI tests in `app/ios-kit/src/commonTest`. Run with `:app:ios-kit:iosSimulatorArm64Test`.

‎CHANGELOG.md‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [8.5.5] - 2026-05-01
9+
10+
### Features
11+
- Biometric authentication (Android & iOS)
12+
- Implement tooltips and refine delete dialog UI
13+
14+
### Refactoring
15+
- Decouple presentation from Compose and implement `AutofillInteractor` for cross-platform autofill support
16+
- Reorganize project structure by moving UI and test modules to the `core` namespace
17+
- Nested test module structure for better organization
18+
819
## [8.5.4] - 2026-04-18
920

1021
### Features
@@ -140,7 +151,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
140151

141152
### Refactoring
142153
- Improve UI test stability and maintainability
143-
- Create multiplatform `ui:test` module and move UI tests
154+
- Create multiplatform `core:test:ui` module and move UI tests
144155
- Simplify navigation methods and improve null handling in RouterImpl
145156
- Improve UI test retry logic and stability
146157

‎CONTRIBUTING.md‎

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ NoteDelight is a **Kotlin Multiplatform** note-taking application with database
2727

2828
### Supported Platforms
2929

30-
- ✅ Android (minSdk 24)
30+
- ✅ Android (minSdk 23)
3131
- ✅ iOS (14.0+)
3232
- ✅ Desktop (Windows, macOS, Linux)
3333
- ✅ Web (WebAssembly, experimental)
@@ -89,6 +89,60 @@ kotlin.code.style=official
8989
- One blank line between functions
9090
- Two blank lines between top-level declarations
9191
- No blank lines at start/end of blocks
92+
- Inside an `expect`/`interface` body with several method signatures, separate them with blank lines so the
93+
declaration list reads as members rather than a wall of text:
94+
```kotlin
95+
expect class BiometricInteractor {
96+
97+
suspend fun canAuthenticate(): Boolean
98+
99+
fun hasStoredPassword(): Boolean
100+
// ...
101+
}
102+
```
103+
104+
#### Call Sites & Lambdas
105+
- **Use named arguments** when calling a function with three or more parameters, or whenever the call site
106+
would otherwise need a same-typed positional argument list. This is especially important for
107+
cross-platform interactors and view-model actions:
108+
```kotlin
109+
biometricInteractor.encryptAndStorePassword(
110+
password = password,
111+
title = title,
112+
subtitle = subtitle,
113+
negativeButton = negativeButton,
114+
)
115+
```
116+
- **Annotate non-trivial local types** so the reader does not have to follow inference through several
117+
generics or platform calls (`val res: DecryptedPasswordResult = ...`, `val plain: ByteArray = ...`).
118+
- **Order `when` branches by the success path first**, error/`else` branches afterwards — this matches the
119+
way ViewModels read top-to-bottom in the project. Prefer `when (result) { is Success -> ...; else -> ... }`
120+
over an inverted `if (!success) ... else ...` ladder.
121+
- **Collapse trivial `viewModelScope.launch` blocks to a single line** when their body is one statement
122+
(e.g. `private fun cancel() = viewModelScope.launch { router.popBackStack() }`).
123+
- **Compose state edits**: prefer a single `mutableStateFlow.update { it.copy(...) }` that sets every field
124+
affected by an event over multiple chained `update` calls. It keeps the resulting state atomic and
125+
makes the visible transition obvious.
126+
127+
#### Composables, strings, and event arguments
128+
- **Do not pipe localized strings through actions or screen wrappers as empty placeholders.** If a screen
129+
needs a `stringResource` to dispatch an action, read it directly at the call site:
130+
```kotlin
131+
// ❌ Avoid
132+
onClick = { onAction(SignInAction.OnBiometricClick("", "", "")) }
133+
// ...wrapper that overwrites the empty strings before forwarding to the ViewModel.
134+
135+
// ✅ Prefer
136+
val title = stringResource(Res.string.biometric_prompt_title)
137+
val subtitle = stringResource(Res.string.biometric_prompt_subtitle)
138+
val negative = stringResource(Res.string.biometric_prompt_negative_button)
139+
onClick = { onAction(SignInAction.OnBiometricClick(title, subtitle, negative)) }
140+
```
141+
When the strings are needed inside a stateless `…Body` composable that also has its own preview, expose
142+
them as defaulted parameters (`title: String = stringResource(Res.string.…)`) instead of forwarding the
143+
action with empty strings and then re-resolving them in the stateful wrapper.
144+
- **Prefer method references for forwarding callbacks** (`onAction = signInViewModel::onAction`) when the
145+
wrapper performs no transformation.
92146

93147
### Code Organization
94148

@@ -455,7 +509,7 @@ Closes #123
455509
```
456510

457511
```
458-
ui/shared: Fix dark theme colors in note editor
512+
core/ui: Fix dark theme colors in note editor
459513
460514
- Update Material 3 color scheme
461515
- Fix text visibility issues

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ Supported platforms:
4141
| database | ✅ | ✅ | ✅ | ✅ |
4242
| encryption | ✅ | ✅ | ✅ | ✅ |
4343
| backup | ✅ | ✅ | ✅ | ✅ |
44+
| biometric | ✅ | ✅ | | |
4445

4546
Interested in contributing new features or fixes? Check out [CONTRIBUTING.md](/CONTRIBUTING.md).
4647

‎app/android/README.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ android {
229229
}
230230

231231
dependencies {
232-
androidTestImplementation(project(":ui:test-jvm"))
232+
androidTestImplementation(project(":core:test:jvm"))
233233
androidTestUtil(libs.androidx.test.orchestrator)
234234
}
235235
```
@@ -317,7 +317,7 @@ fastlane production # Deploy to production
317317
- `core:domain` - Domain layer
318318
- `core:data` - Data layer (SQLDelight or Room)
319319
- `core:presentation` - ViewModels
320-
- `ui:shared` - Shared UI
320+
- `core:ui` - Shared UI
321321

322322
### Android
323323
- `androidx.activity.compose` - Activity with Compose
@@ -334,7 +334,7 @@ fastlane production # Deploy to production
334334
- `leakcanary.android` - Memory leak detection
335335

336336
### Testing
337-
- `ui:test-jvm` - UI test framework
337+
- `core:test:jvm` - UI test framework
338338
- `androidx.test.ext.junit` - AndroidX Test
339339
- `androidx.test.runner` - Test runner
340340
- `androidx.test.orchestrator` - Test orchestrator
@@ -466,8 +466,8 @@ val permissionLauncher = rememberLauncherForActivityResult(
466466

467467
## Related Modules
468468

469-
- **Depends on**: `ui:shared`, `core:presentation`, `core:data`, `core:domain`
470-
- **Test dependencies**: `ui:test-jvm`, `core:test`
469+
- **Depends on**: `core:ui`, `core:presentation`, `core:data`, `core:domain`
470+
- **Test dependencies**: `core:test:jvm`, `core:test:common`
471471
- **Alternative apps**: `app:desktop`, `app:web`, `app:iosApp`
472472

473473
## Resources

‎app/android/build.gradle.kts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ android {
2525
applicationId = "com.softartdev.noteroom"
2626
minSdk = libs.versions.minSdk.get().toInt()
2727
targetSdk = libs.versions.targetSdk.get().toInt()
28-
versionCode = 854
29-
versionName = "8.5.4"
28+
versionCode = 855
29+
versionName = "8.5.5"
3030
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
3131
testInstrumentationRunnerArguments["clearPackageData"] = "true"
3232
vectorDrawables.useSupportLibrary = true
@@ -73,7 +73,7 @@ dependencies {
7373
implementation(projects.core.domain)
7474
implementation(project(project.property("CORE_DATA_DB_MODULE").toString()))
7575
implementation(projects.core.presentation)
76-
implementation(projects.ui.shared)
76+
implementation(projects.core.ui)
7777
implementation(projects.feature.backup.domain)
7878
implementation(projects.feature.backup.ui)
7979
implementation(kotlin("reflect", libs.versions.kotlin.get()))
@@ -101,8 +101,8 @@ dependencies {
101101
coreLibraryDesugaring(libs.desugar)
102102
testImplementation(libs.junit)
103103
testImplementation(libs.bundles.mockito)
104-
androidTestImplementation(projects.ui.test)
105-
androidTestImplementation(projects.ui.testJvm) {
104+
androidTestImplementation(projects.core.test.ui)
105+
androidTestImplementation(projects.core.test.jvm) {
106106
exclude(group = "org.jetbrains.runtime", module = "jbr-api")
107107
}
108108
androidTestImplementation(libs.commonsware.saferoom)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
@file:OptIn(ExperimentalTestApi::class)
2+
3+
package com.softartdev.notedelight.ui
4+
5+
import androidx.compose.ui.test.ComposeUiTest
6+
import androidx.compose.ui.test.ExperimentalTestApi
7+
import androidx.test.espresso.Espresso
8+
import androidx.test.ext.junit.runners.AndroidJUnit4
9+
import androidx.test.filters.FlakyTest
10+
import com.softartdev.notedelight.MainActivity
11+
import com.softartdev.notedelight.di.biometricTestModule
12+
import com.softartdev.notedelight.interactor.TestBiometricInteractor
13+
import com.softartdev.notedelight.reflect
14+
import com.softartdev.notedelight.ui.cases.BiometricSettingsTestCase
15+
import leakcanary.DetectLeaksAfterTestSuccess
16+
import leakcanary.TestDescriptionHolder
17+
import org.junit.After
18+
import org.junit.Rule
19+
import org.junit.Test
20+
import org.junit.rules.RuleChain
21+
import org.junit.runner.RunWith
22+
import org.koin.core.context.loadKoinModules
23+
import org.koin.mp.KoinPlatformTools
24+
25+
@FlakyTest
26+
@RunWith(AndroidJUnit4::class)
27+
class BiometricSettingsTest {
28+
29+
private val testBiometricInteractor: TestBiometricInteractor
30+
get() = KoinPlatformTools.defaultContext().get().get(TestBiometricInteractor::class)
31+
32+
private val composeTestRule = customAndroidComposeRule<MainActivity>(
33+
beforeActivityLaunched = {
34+
loadKoinModules(biometricTestModule)
35+
testBiometricInteractor.reset(canAuthenticateResult = true)
36+
}
37+
)
38+
39+
@get:Rule
40+
val rules: RuleChain = RuleChain.outerRule(TestDescriptionHolder)
41+
.around(DetectLeaksAfterTestSuccess())
42+
.around(composeTestRule)
43+
44+
private val composeUiTest: ComposeUiTest = reflect(composeTestRule)
45+
46+
@After
47+
fun tearDown() = testBiometricInteractor.reset()
48+
49+
@Test
50+
fun biometricSettingsTest() = BiometricSettingsTestCase(
51+
composeUiTest = composeUiTest,
52+
closeSoftKeyboard = Espresso::closeSoftKeyboard,
53+
).invoke()
54+
}

0 commit comments

Comments
 (0)