Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 2 additions & 23 deletions .github/workflows/develop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,34 +36,13 @@ jobs:
- name: Gradle cache
uses: gradle/actions/setup-gradle@v3

- name: AVD cache
uses: actions/cache@v4
id: avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
key: avd-30

- name: Create AVD and generate snapshot for caching
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: ReactiveCircus/android-emulator-runner@v2.33.0
with:
api-level: 30
target: google_atd
arch: x86
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: false
script: echo "Generated AVD snapshot for caching."

- name: Run Android tests
uses: ReactiveCircus/android-emulator-runner@v2.33.0
uses: ReactiveCircus/android-emulator-runner@v2.35.0
with:
api-level: 30
target: google_atd
arch: x86
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: ./gradlew :input-engine:connectedAndroidTest
185 changes: 185 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

Input Engine is a Kotlin Multiplatform UI Library that provides customizable input screens for money amounts, percentages, quantities, and PIN entry. It supports both Android and iOS platforms using shared Compose Multiplatform UI code.

## Build Commands

### Building and Testing

```bash
# Build the entire project
./gradlew build

# Build only the input-engine library
./gradlew :input-engine:build

# Run unit tests (Android)
./gradlew testDebug

# Run Android instrumented tests (requires emulator or device)
./gradlew :input-engine:connectedAndroidTest

# Build sample app
./gradlew :sample:assemble
```

### Code Quality

```bash
# Check code formatting (ktlint)
./gradlew spotlessCheck

# Auto-fix formatting issues
./gradlew spotlessApply

# Run Android lint
./gradlew lint
./gradlew lintFix # Apply safe suggestions
```

### iOS Development

```bash
# Build XCFramework for iOS integration
./gradlew :input-engine:assembleXCFramework
```

Note: iOS builds require Xcode and macOS. The XCFramework will be generated in `input-engine/build/`.

### Publishing

```bash
# Publish to Maven Central (requires credentials)
./gradlew publishAndReleaseToMavenCentral --no-configuration-cache
```

## Architecture

### Multiplatform Structure

The codebase follows standard Kotlin Multiplatform conventions with these source sets:

- **`commonMain/`** - Shared business logic, UI components (Compose Multiplatform), and contracts
- **`androidMain/`** - Android-specific implementations (Activities, Activity Result Contracts)
- **`iosMain/`** - iOS-specific implementations (Presenters, UIViewController bridges)
- **`commonTest/`, `androidUnitTest/`, `iosTest/`** - Platform-specific tests

### Key Package Structure

- **`contract/`** - Platform-agnostic input contracts with `expect`/`actual` implementations
- Common interfaces define the contract (e.g., `AmountInputContract`)
- Android uses Activity Result API
- iOS uses Presenter pattern with `ComposeUIViewController`
- Legacy Android contracts exist in `androidMain/contract/legacy/` for backward compatibility

- **`data/`** - Data models and I/O types
- `MoneyIO`, `PercentIO`, `QuantityIO` - Serializable data transfer objects
- `MoneyParam`, `PercentageParam`, `QuantityParam` - Configuration parameters (Enable/Disable sealed classes)
- `CurrencyIO` - Currency representation

- **`domain/`** - Domain logic and utilities
- `NumpadKey`, `Digit` - Input handling abstractions
- `StringParam` - Sealed class for optional string parameters
- `helper/` - Business logic helpers

- **`formatting/`** - Platform-specific number formatters
- Common interfaces with `expect`/`actual` implementations
- Uses native platform formatting (Android: `NumberFormat`, iOS: `NSNumberFormatter`)

- **`ui/`** - Compose Multiplatform UI layer
- `screens/` - Full-screen composables (`AmountInputScreen`, `QuantityInputScreen`, etc.)
- `components/` - Reusable UI components (`NumberButton`, `Toolbar`, etc.)
- ViewModels in `commonMain` using `androidx.lifecycle`
- Android Activities bridge to ViewModels
- iOS Presenters use `ComposeUIViewController` to display screens

- **`theme/`** - Compose theming and styling

### Contract Pattern

The library uses a contract-based API where each input type has:

1. **Request** - Input parameters (e.g., `AmountInputRequest`)
2. **Result** - Sealed class with `Success` or `Canceled` (e.g., `AmountInputResult`)
3. **Contract** - Platform-specific launcher interface

Example flow:
- Android: Uses `rememberLauncherForActivityResult` to launch Activities
- iOS: Uses `AmountInputPresenter` to present `ComposeUIViewController`
- Both platforms share the same Compose UI screens and ViewModels

### ViewModel Factory Pattern

ViewModels are created using factory pattern with `CreationExtras`:
- Request data and formatters are passed via `CreationExtras`
- iOS uses `MutableCreationExtras` in Presenters
- Android Activities retrieve extras from Intent

## Configuration

### Build Configuration

- **Application ID**: `de.tillhub.inputengine`
- **Min SDK**: 24 (Android 7.0)
- **Compile SDK**: 35
- **Java Version**: 17
- **iOS Framework Name**: `input-engineKit`
- **Maven Coordinates**: `io.github.tillhub:input-engine`

### Gradle Plugins

- Kotlin Multiplatform
- Android Library
- Compose Multiplatform & Compose Compiler
- Kotlinx Serialization
- Mokkery (for mocking in tests)
- Spotless (code formatting with ktlint)
- Maven Publishing

## Testing

### Test Exclusions

UI tests are excluded from unit test runs (configured in `input-engine/build.gradle.kts`):
- `**/inputengine/ui/components/**`
- `**/inputengine/ui/screens/**`

These are run separately as instrumented tests on Android or as iOS UI tests.

### Running Specific Tests

```bash
# Run tests for a specific formatter
./gradlew testDebug --tests "*MoneyFormatterTest"

# Run all formatting tests
./gradlew testDebug --tests "*.formatting.*"
```

## CI/CD Workflows

- **PR Checks** (`pr-checks.yml`): Runs `spotlessCheck` and `testDebug` on pull requests
- **Develop Branch** (`develop.yml`): Runs Android instrumented tests on emulator
- **Master Branch** (`publish.yml`): Publishes to Maven Central (macOS runner required for iOS compilation)

## Important Notes

### Serialization

All request/result classes use `kotlinx.serialization` with `@Serializable` annotations. Android passes serialized JSON through Intent extras; iOS passes objects directly through ViewModels.

### Expect/Actual Pattern

Platform-specific implementations are marked with `expect` (common) and `actual` (platform-specific):
- Contracts (`rememberAmountInputLauncher`, etc.)
- Formatters (`MoneyFormatterImpl`, `DecimalFormatter`, etc.)

When modifying these, ensure both Android and iOS implementations are updated.

### Legacy Support

Android maintains legacy Activity Result Contracts in `androidMain/contract/legacy/` for apps that haven't migrated to the Compose-based API. Do not remove these without coordination.
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[versions]
input-engine = "2.1.1"
input-engine = "2.1.2"
activityKtx = "1.10.1"
agp = "8.11.1"
androidx-activity = "1.10.1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@ package de.tillhub.inputengine.ui.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import de.tillhub.inputengine.domain.StringParam
import de.tillhub.inputengine.resources.Res
import de.tillhub.inputengine.resources.max_value
Expand All @@ -33,7 +31,7 @@ internal fun AmountInputPreview(
verticalArrangement = Arrangement.Center,
) {
Text(
modifier = Modifier.padding(16.dp).semantics { contentDescription = "Current amount" },
modifier = Modifier.semantics { contentDescription = "Current amount" },
style = MaterialTheme.typography.displayLarge,
maxLines = 1,
text = amount.text,
Expand All @@ -47,7 +45,7 @@ internal fun AmountInputPreview(

if (amountMin is StringParam.Enable) {
Text(
modifier = Modifier.padding(bottom = 8.dp).semantics { contentDescription = "Min allowed amount" },
modifier = Modifier.semantics { contentDescription = "Min allowed amount" },
style = MaterialTheme.typography.labelSmall,
maxLines = 1,
text = stringResource(Res.string.min_value, amountMin.value),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@ package de.tillhub.inputengine.ui.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import de.tillhub.inputengine.domain.StringParam
import de.tillhub.inputengine.resources.Res
import de.tillhub.inputengine.resources.max_value
Expand All @@ -31,7 +29,7 @@ fun PercentageInputPreview(
verticalArrangement = Arrangement.Center,
) {
Text(
modifier = Modifier.padding(16.dp).semantics { contentDescription = "Current percentage" },
modifier = Modifier.semantics { contentDescription = "Current percentage" },
style = MaterialTheme.typography.displayLarge,
maxLines = 1,
text = percentText,
Expand All @@ -40,7 +38,7 @@ fun PercentageInputPreview(

if (percentageMin is StringParam.Enable) {
Text(
modifier = Modifier.padding(bottom = 8.dp).semantics { contentDescription = "Min allowed percentage" },
modifier = Modifier.semantics { contentDescription = "Min allowed percentage" },
style = MaterialTheme.typography.labelSmall,
maxLines = 1,
text = stringResource(Res.string.min_value, percentageMin.value),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ internal fun QuantityInputPreview(
}
Text(
text = quantity.text,
modifier = Modifier.padding(16.dp).semantics { contentDescription = "Current quantity" },
modifier = Modifier.padding(horizontal = 16.dp).semantics { contentDescription = "Current quantity" },
style = MaterialTheme.typography.displayLarge,
color =
if (quantity.isHint) {
Expand Down Expand Up @@ -92,7 +92,7 @@ internal fun QuantityInputPreview(

if (minQuantity is StringParam.Enable) {
Text(
modifier = Modifier.padding(bottom = 8.dp).semantics { contentDescription = "Min allowed quantity" },
modifier = Modifier.semantics { contentDescription = "Min allowed quantity" },
text = stringResource(Res.string.min_value, minQuantity.value),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSecondary,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ internal fun AmountInputScreen(
amountMax = amountMax,
)
NumberKeyboard(
modifier = Modifier.padding(vertical = 24.dp),
modifier = Modifier.padding(vertical = 8.dp),
onClick = viewModel::input,
showNegative = viewModel.amountInputMode == AmountInputMode.BOTH,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ internal fun PercentageInputScreen(
percentageMax = viewModel.maxStringParam,
)
NumberKeyboard(
modifier = Modifier.padding(vertical = 24.dp),
modifier = Modifier.padding(vertical = 8.dp),
onClick = viewModel::input,
showDecimalSeparator = viewModel.allowDecimal,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@ package de.tillhub.inputengine.ui.screens
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
Expand Down Expand Up @@ -112,10 +110,9 @@ internal fun PinInputScreen(
onOverride = { onResult(PinInputResult.Success(viewModel.responseExtras)) },
)
NumberKeyboard(
modifier = Modifier.padding(vertical = 24.dp),
modifier = Modifier.padding(top = 24.dp),
onClick = viewModel::input,
)
Spacer(modifier = Modifier.height(64.dp))
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ internal fun QuantityInputScreen(
)

NumberKeyboard(
modifier = Modifier.padding(vertical = 24.dp),
modifier = Modifier.padding(vertical = 8.dp),
showDecimalSeparator = viewModel.allowDecimal,
showNegative = viewModel.allowNegative,
onClick = viewModel::processKey,
Expand Down