Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
fb100ee
Set root = true in .editorconfig
dbAbstract Apr 20, 2026
f975f92
Implement `signUpWithEmailAndPassword` in `AuthRepository` and its im…
dbAbstract Apr 20, 2026
cfc11a8
Add feature:onboarding:domain module and CheckAppStateUseCase
dbAbstract Apr 20, 2026
37b67c9
Add onboarding:data module and configure DI for user and onboarding f…
dbAbstract Apr 20, 2026
70cfa6d
Update Instant import from kotlinx.datetime to kotlin.time in User AP…
dbAbstract Apr 20, 2026
5bd207b
Add libs:presentation module and KmpObservable Swift wrapper
dbAbstract Apr 20, 2026
91d82b8
Export user and presentation modules in libs:di and add UserDependenc…
dbAbstract Apr 20, 2026
c5388bc
Add Claude skill for creating and working with UseCase classes
dbAbstract Apr 20, 2026
4001438
Add repository-layer-design Claude skill
dbAbstract Apr 20, 2026
e4e6b47
Add presentation layer design skill for Claude tool documentation
dbAbstract Apr 20, 2026
a75ae88
Add feature:onboarding:presentation module and implement presenters
dbAbstract Apr 20, 2026
8f66895
Update presentation layer design guidelines for component structure a…
dbAbstract Apr 20, 2026
aaf5298
Refactor onboarding presentation layer to use ViewModels and MVI-styl…
dbAbstract Apr 21, 2026
160a557
Add AndroidX Navigation3 and Lifecycle Navigation3 dependencies
dbAbstract Apr 21, 2026
9e19004
Reset
dbAbstract Apr 21, 2026
42d06a0
Add libs:navigation module and define AppRoute interface
dbAbstract Apr 21, 2026
7719195
Add Android-specific navigation-runtime dependency to libs:navigation…
dbAbstract Apr 21, 2026
e0b3610
Add libs:design module and implement YaseyoTheme with light and dark …
dbAbstract Apr 21, 2026
c55c112
Add AppState model to onboarding API module
dbAbstract Apr 21, 2026
d2eb6ec
Add ResolveAppStateUseCase to onboarding:api
dbAbstract Apr 21, 2026
65c30bd
Add OnboardingRoutes to onboarding:api module
dbAbstract Apr 21, 2026
ca54408
Add AppViewModel to onboarding:api module
dbAbstract Apr 21, 2026
d7a9004
Add auth and user library dependencies to feature:onboarding:api module
dbAbstract Apr 21, 2026
f913d37
Move AppViewModel to androidMain as MainViewModel and reorganize comp…
dbAbstract Apr 21, 2026
f498e2d
Add Android SplashScreen and App Startup for Koin module initialization
dbAbstract Apr 21, 2026
afbf4f2
Add onboardingModule and migrate ResolveAppStateUseCase to Koin depen…
dbAbstract Apr 21, 2026
b90d631
Add Navigator and integrate it with Koin and MainActivity
dbAbstract Apr 21, 2026
4b33ffc
Integrate Navigation3 with Koin and refactor start destination logic
dbAbstract Apr 21, 2026
9089fcb
Prevent redundant navigation by checking `showApp` state in `MainActi…
dbAbstract Apr 22, 2026
35d600d
Update `OnboardingFeatureNavigation` visibility to internal
dbAbstract Apr 22, 2026
ea2ed55
Implement parent proxy module pattern and update onboarding feature s…
dbAbstract Apr 22, 2026
72eebb7
Refactor onboarding module structure and implement module boundary en…
dbAbstract Apr 22, 2026
96e1463
Update module structure rules and Koin DI patterns in Claude skill gu…
dbAbstract Apr 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 128 additions & 17 deletions .claude/skills/create-new-feature/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,131 @@ name: create-new-feature
description: This skill is to instruct you how to go about implementing a new feature
---

# How to teach me or explain to me

## Instructions
- The structure of my project is a multi module KMP project
- No single `:shared` module, instead I have multiple shared KMP modules
- Among the shared KMP modules, there are 2 variants. `:lib` and `:feature`.
- `:lib` modules (eg. `:lib:auth`) is for core functionality or I/O of a certain data.
- `:lib` modules will always consist of two child modules. `:api` and `:impl`
- `:feature` modules (eg. `:feature:history`) would be for consumer facing functionalities
- `:feature` modules will always consist of three child modules. `:ui`, `:data` and `:domain`
- `:feature:someFeature:ui` will, however, live in the native codebases since that's where it's implemented
- The feature's data and ui child modules, will depend on domain
- When developing, ensure that you get my approval before each step. I need to be architecturally involved to ensure best quality
- Once a feature/functionality or tech stack is implemented, I want you to update the README for the
project to keep it up to date with our architecture and tech stack choice.

## Examples
# How to create a new feature

## Module structure rules

Multi-module KMP project. No single `:shared` module.

### `:feature` modules (e.g. `:feature:onboarding`)
User-facing features. Structure:

```
:feature:<name> ← parent, thin proxy. No source. Just api(:api).
:api ← REQUIRED. KMP. Public surface: routes, use case interfaces, domain models, api Koin module.
:impl ← OPTIONAL. Only create when there's shared commonMain implementation to hide behind :api.
:android ← pure Android library. Nav entries, ViewModels, Compose, FeatureNavigation impl, Android Koin module.
```

Rules:
- `:api` is mandatory.
- `:impl` is case-by-case — add only when there's actual shared Kotlin implementation that warrants hiding. Don't create empty shells.
- `:android` depends on `:impl` when one exists, otherwise depends on `:api` directly.
- Parent has **no source**. It only exists to re-export `:api` via `api(projects.feature.<name>.api)`, so consumers depending on the parent get `:api` transitively without needing to know about the internal split.
- iOS presentation lives in the Xcode/SwiftUI project — **not** as a Gradle module. iOS consumes the umbrella `:libs:di` framework for use cases.

### `:lib` modules (e.g. `:lib:auth`)
Core functionality or I/O. Structure is `:api` + `:impl` + parent aggregator (see existing `libs/auth` and `libs/user` for the pattern). Parent `api`-depends on `:api`, `implementation`-depends on `:impl`.

## Always get architectural approval before each step. After any new feature lands, update the project README.

---

## Step-by-step: adding a new `:feature:<name>`

### 1. Create `:feature:<name>:api`
File: `feature/<name>/api/build.gradle.kts` — applies `yaseyo.kmp.library`, namespace `dev.yaseyo.<name>.api`, commonMain deps for what the public API needs.

Place in `commonMain`:
- `dev.yaseyo.<name>.api.navigation.<Name>Routes` — `sealed interface <Name>Routes : AppRoute`
- `dev.yaseyo.<name>.api.model.*` — domain models
- `dev.yaseyo.<name>.api.*` — use case classes/interfaces
- `dev.yaseyo.<name>.api.di.<Name>Module.kt` — `val <name>Module = module { ... }` with bindings that are platform-agnostic

### 2. Create `:feature:<name>` (parent proxy)
File: `feature/<name>/build.gradle.kts`:
```kotlin
plugins {
id("yaseyo.kmp.library")
}

kotlin {
androidLibrary { namespace = "dev.yaseyo.<name>" }

sourceSets {
commonMain.dependencies {
api(projects.feature.<name>.api)
}
}
}
```
No `src/` directory. Nothing else.

### 3. Create `:feature:<name>:android`
File: `feature/<name>/android/build.gradle.kts` — applies `yaseyo.android.library` (and `libs.plugins.composeCompiler` if the module uses Compose). Namespace `dev.yaseyo.<name>.android`. Depends on `projects.feature.<name>.api` (or `projects.feature.<name>.impl` if one exists).

Place in `src/main/kotlin/dev/yaseyo/<name>/android/`:
- `<Name>FeatureNavigation.kt` — `internal class <Name>FeatureNavigation : FeatureNavigation { ... }` with nav entries
- `<Name>FeatureAndroidModule.kt` — `val <name>FeatureAndroidModule = module { factory<FeatureNavigation> { <Name>FeatureNavigation() } }`

Do **not** wrap in `activityRetainedScope`. Register in the root scope (see `wiring-up-koin` skill).

### 4. Register modules in `settings.gradle.kts`
```kotlin
include(":feature:<name>")
include(":feature:<name>:api")
include(":feature:<name>:android")
// include(":feature:<name>:impl") only if :impl was created
```

### 5. Wire up in `:libs:di`
`:libs:di` is the DI composition root — the ONE module allowed to depend on `:android` from outside its feature tree.

In `libs/di/build.gradle.kts`:
```kotlin
commonMain.dependencies {
api(projects.feature.<name>) // transitive :api exposure
}
androidMain.dependencies {
implementation(projects.feature.<name>.android)
}
```

In `libs/di/src/commonMain/.../AppModules.kt`:
```kotlin
import dev.yaseyo.<name>.api.di.<name>Module
// add to appModules list
```

In `libs/di/src/androidMain/.../KoinInitializer.kt`:
```kotlin
import dev.yaseyo.<name>.android.<name>FeatureAndroidModule
// add to startKoin { modules(...) } call
```

### 6. iOS framework export (if the feature is consumed from Swift)
In `libs/di/build.gradle.kts` iOS target block:
```kotlin
export(projects.feature.<name>)
```
Exports the parent, which transitively exposes `:api` types to Swift.

---

## How `App.kt` collects all Android nav entries (do not change this)

`App.kt` uses `getKoin().getAll<FeatureNavigation>()` to collect every `FeatureNavigation` binding, then wires them all into the Nav3 `entryProvider` via the `registerAll()` extension. Each `:android` module only needs to register its own `factory<FeatureNavigation>` — `App.kt` picks it up automatically.

---

## Checklist

- [ ] `:api` created (routes, use cases, common Koin module)
- [ ] Parent proxy created (no source, just `api(:api)`)
- [ ] `:android` created (nav impl, android Koin module) — depends on `:api` or `:impl`
- [ ] `:impl` added only if there's actual shared implementation to justify it
- [ ] `settings.gradle.kts` updated
- [ ] `:libs:di` wired up: commonMain imports `:api`'s module, androidMain imports `:android`'s module
- [ ] iOS export added if Swift consumption expected
- [ ] User approved architecture at each step
- [ ] README updated
16 changes: 16 additions & 0 deletions .claude/skills/creating-use-case/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
name: working-with-use-cases
description: This skill is to instruct you how to go creating or working with UseCase classes
---

# How to teach me or explain to me

## Instructions
- Should only be made when 2 or more repositories/data sources are needed
- Should have 1 function, `execute` in them
- Usages of the UseCase should have useCase as a suffix
- Ensure unit tests are updated or implemented when working with UseCases
- Should be platform-agnostic
- Should have KDocs written for them to better explain

## Examples
16 changes: 16 additions & 0 deletions .claude/skills/presentation-layer-design/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
name: presentation-layer-design
description: This skill is to instruct you how to go about designing and implementing the presentation layer
---

# How to teach me or explain to me

## Instructions
- The project may have instances of shared Presenters or opt to go for platform specific presenters. This is decided on case by case basis
- The presentation layer should always consist of the Presenter, UiState, EventHandler interface (which is implemented by the ViewModel/Presenter) and UiAction for side effects triggered by Presenter. This has one exception, and that is for cases where the logic does not have enough complexity to warrant this. Eg. just one side effect is not enough to create a UiAction or just one state is not enough to create a UiState
- Presentation components should be in different files
- Where possible, data sources should be owned by the repository or UseCase layer, not sourced from the Presenter
- For init logic, always go for Flow<T>.onStart for KMP Presenters
- Inject Dispatchers from the Coroutines module to ensure testability

## Examples
15 changes: 15 additions & 0 deletions .claude/skills/repository-layer-design/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
name: repository-layer-design
description: This skill is to instruct you how to create or work with Repository layer classes
---

# How to teach me or explain to me

## Instructions
- Should be framework and platform-agnostic
- Implementations should have `Impl` as a suffix
- Ensure getters are made for iOS in iOS main as these are Koin components
- Ensure test coverage is sufficient
- KDoc the contract for better understanding, the KDoc should not explicitly point to implementation details, but rather just simply explain the purpose of the various APIs

## Examples
79 changes: 61 additions & 18 deletions .claude/skills/wiring-up-koin/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,64 @@ name: wiring-up-koin
description: This skill is to instruct you how to go about setting up the wiring of Koin
---

# How to teach me or explain to me

## Instructions

- Among the shared KMP modules, there are 2 variants. `:lib` and `:feature`.
- `:lib` modules (eg. `:lib:auth`) is for core functionality or I/O of a certain data.
- `:lib` modules will always consist of two child modules. `:api` and `:impl`
- `:feature` modules (eg. `:feature:history`) would be for consumer facing functionalities
- `:feature` modules will always consist of three child modules. `:ui`, `:data` and `:domain`
- `:feature:someFeature:ui` will, however, live in the native codebases since that's where it's
implemented
- The feature's data and ui child modules, will depend on domain
- Each parent module will have an aggregator DI Koin module, every child module will have its own
public Koin module
- which then gets included in the parent module's Koin module.
- The parent's Koin module is then added to the :lib:di module's Koin module.

## Examples
# How to wire up Koin

## Composition-root model

`:libs:di` is the single composition root for Dependency Injection. It:
- Assembles all Koin modules in `AppModules.kt` (commonMain) and `KoinInitializer.kt` (androidMain)
- Is the **only** module outside of a feature tree allowed to reference `:android` / `:impl` directly (enforced by the `yaseyo.kmp.library` / `yaseyo.android.library` convention plugin linter)
- No aggregator/umbrella Koin module per feature. `:libs:di` references the common and platform-specific Koin modules directly.

## Module ownership per module type

### `:feature:<name>:api` (KMP, commonMain)
```kotlin
val <name>Module = module {
factoryOf(::SomeUseCase)
// platform-agnostic bindings only
}
```

### `:feature:<name>:impl` (KMP, commonMain) — optional
If an `:impl` exists, it hosts bindings for implementations that should not leak via `:api`. Define a separate `val <name>ImplModule = module { ... }` if needed.

### `:feature:<name>:android` (pure Android library)
```kotlin
val <name>FeatureAndroidModule = module {
factory<FeatureNavigation> { <Name>FeatureNavigation() }
// Android-specific bindings: ViewModels, platform services, etc.
}
```

### `:libs:di`
`AppModules.kt` (commonMain):
```kotlin
internal val appModules = listOf(
// ...
<name>Module, // from :api
)
```

`KoinInitializer.kt` (androidMain):
```kotlin
startKoin {
androidContext(context)
modules(appModules + navigationAndroidModule + <name>FeatureAndroidModule)
}
```

## Scope rules

- Register definitions in the **root scope** (`module { factory<T> { ... } }`) unless there is a specific reason to scope them.
- Do **not** wrap definitions in `activityRetainedScope { }` unless the definition must be tied to an activity's retained lifecycle. Scoped definitions are invisible to root-scope lookups like `getKoin().getAll<T>()`.

## Multi-binding with `getAll`

When multiple `:android` modules contribute implementations of the same interface (e.g. `FeatureNavigation`), each registers its own `factory<TheInterface>`. Call `getKoin().getAll<TheInterface>()` at the call site to collect all of them. This is how `App.kt` aggregates nav entries across features.

## What NOT to do

- **Don't create a per-feature aggregator Koin module** in the parent. The parent has no source. Aggregation happens at the `:libs:di` composition root.
- **Don't have the parent depend on `:android`** — it would create a project cycle (`:android` depends on `:api`/`:impl`, which live under parent).
- **Don't include `:api`'s Koin module from inside `:android`'s module**. `:libs:di` registers both separately. Using `includes(...)` double-registers.
65 changes: 58 additions & 7 deletions .claude/skills/working-with-gradle/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,64 @@ name: working-with-gradle
description: This skill is to instruct you how to go about configuring a new gradle module or altering the gradle configuration of this project by adding/removing/updating dependencies.
---

# How to teach me or explain to me
# Working with Gradle

## Instructions
## General rules

- When adding a new KMP module, use the convention plugin/s defined in the build-logic module
- When adding a plugin or configuring a block in build.gradle.kts, consider adding it into either an
existing convention plugin or suggesting the creation of a new one.
- When adding a plugin, don't forget to add the plugin to the project level build.gradle.kts too!
- When adding a new module, pick a convention plugin from `build-logic` — do not hand-roll `android { }` / `kotlin { }` config.
- When adding a new plugin to the catalog, also add it to the project-level `build.gradle.kts` if it needs to be on the classpath.
- When you find yourself repeating config across modules, promote it into an existing convention plugin (or suggest creating a new one) rather than duplicating.

## Examples
## Convention plugins

Two convention plugins cover all library modules:

### `yaseyo.kmp.library`
KMP modules. Applies:
- `org.jetbrains.kotlin.multiplatform`
- `com.android.kotlin.multiplatform.library`
- `com.android.lint`
- `org.jetbrains.kotlin.plugin.serialization`
- iOS targets (`iosArm64`, `iosSimulatorArm64`)

Exposes a `kmpLibrary { }` DSL for iOS framework production (`iosFrameworkName`, `enableSkie`). Framework production is restricted to `:libs:di` only — attempting to set `iosFrameworkName` in any other module fails the build. Reason: iOS can load at most one K/N framework per process.

### `yaseyo.android.library`
Pure Android libraries (typically `:feature:*:android`). Applies:
- `com.android.library` (includes lint)
- `org.jetbrains.kotlin.android`

Sets min/compile SDK from the version catalog, Java 11 source/target compatibility, and Kotlin JVM 11 target to match the rest of the project.

## Module structure patterns

### `:feature:<name>`
See `create-new-feature` skill. Structure:
- Parent (`yaseyo.kmp.library`) — no source, just `api(:api)` in commonMain
- `:api` (`yaseyo.kmp.library`) — required, commonMain public surface
- `:impl` (`yaseyo.kmp.library`) — optional
- `:android` (`yaseyo.android.library`) — pure Android, depends on `:impl` if exists else `:api`

### `:lib:<name>`
Parent (`yaseyo.kmp.library`) with commonMain aggregating `api(:api)` + `implementation(:impl)`. See `libs/auth` and `libs/user` for the pattern.

## Internal module boundary linter

`ModuleBoundaryEnforcement.kt` (invoked automatically by both convention plugins) enforces that `:impl` and `:android` modules are internal:

- **Allowed** to depend on `:feature:onboarding:impl` / `:feature:onboarding:android`:
- Any module within the same feature tree (e.g. `:feature:onboarding` or `:feature:onboarding:android`)
- `:libs:di` (DI composition root, whitelisted)
- **Not allowed**: any other module. The build fails with a "DESIGN BREACH" message showing the exact line to change.

To extend the rule (e.g. add another internal suffix, or whitelist another module), edit:
`build-logic/convention/src/main/kotlin/dev/yaseyo/ModuleBoundaryEnforcement.kt`

## iOS framework export (for Swift consumption)

In `libs/di/build.gradle.kts`, inside the iOS target block:
```kotlin
export(projects.feature.<name>) // always the parent, never :api/:impl/:android
```

And `api(projects.feature.<name>)` in commonMain dependencies so that the exported types are visible at compile time.
2 changes: 2 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
root = true

[*.kt]
ktlint_function_naming_ignore_when_annotated_with = Composable
ktlint_standard_multiline-expression-wrapping = disabled
4 changes: 4 additions & 0 deletions build-logic/convention/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,9 @@ gradlePlugin {
id = "yaseyo.kmp.library"
implementationClass = "dev.yaseyo.KmpLibraryConventionPlugin"
}
register("androidLibrary") {
id = "yaseyo.android.library"
implementationClass = "dev.yaseyo.AndroidLibraryConventionPlugin"
}
}
}
Loading
Loading