|
| 1 | +# MyFAQ.app |
| 2 | + |
| 3 | +Native iOS and Android client for [phpMyFAQ](https://www.phpmyfaq.de), |
| 4 | +built with Kotlin Multiplatform, SwiftUI, and Jetpack Compose. |
| 5 | + |
| 6 | +**Status: Phase 0 (foundations)**. The apps display a placeholder |
| 7 | +screen that proves the shared module, API client, encrypted database, |
| 8 | +and secure storage are wired end-to-end. No user-visible features yet |
| 9 | +-- those land in Phase 1. |
| 10 | + |
| 11 | +- Website: [myfaq.app](https://myfaq.app) |
| 12 | +- phpMyFAQ minimum version: **4.2.0** |
| 13 | +- Business model: freemium (read + offline free forever; writes behind |
| 14 | + Pro unlock in Phase 3) |
| 15 | + |
| 16 | +--- |
| 17 | + |
| 18 | +## Prerequisites |
| 19 | + |
| 20 | +| Tool | Minimum | Install | |
| 21 | +|-------------------|-----------|----------------------------------------------| |
| 22 | +| JDK | 17+ | `brew install openjdk` or [Temurin](https://adoptium.net) | |
| 23 | +| Android SDK | API 35 | [Android Studio](https://developer.android.com/studio) or `sdkmanager` | |
| 24 | +| Xcode | 15.0 | Mac App Store (macOS only) | |
| 25 | +| XcodeGen | latest | `brew install xcodegen` | |
| 26 | +| Git | 2.x | pre-installed on macOS | |
| 27 | + |
| 28 | +Run the bootstrap checker: |
| 29 | + |
| 30 | +```bash |
| 31 | +mobile/scripts/bootstrap.sh |
| 32 | +``` |
| 33 | + |
| 34 | +### Android SDK quick setup (without Android Studio) |
| 35 | + |
| 36 | +```bash |
| 37 | +brew install --cask android-commandlinetools |
| 38 | +sdkmanager "platforms;android-35" "build-tools;35.0.0" |
| 39 | +echo "sdk.dir=$HOME/Library/Android/sdk" > mobile/local.properties |
| 40 | +``` |
| 41 | + |
| 42 | +### Xcode first-launch setup |
| 43 | + |
| 44 | +If you see `xcodebuild` errors about `IDESimulatorFoundation`: |
| 45 | + |
| 46 | +```bash |
| 47 | +sudo xcodebuild -runFirstLaunch |
| 48 | +sudo xcodebuild -license accept |
| 49 | +``` |
| 50 | + |
| 51 | +--- |
| 52 | + |
| 53 | +## Repository layout |
| 54 | + |
| 55 | +``` |
| 56 | +phpMyFAQ/MyFAQ |
| 57 | +├── plans/ Planning docs (source of truth) |
| 58 | +│ ├── mobile-app-plan.md Architecture & feature plan |
| 59 | +│ └── phase-0-foundations.md Phase 0 detailed execution plan |
| 60 | +├── docs/mobile/ Technical docs |
| 61 | +│ ├── architecture.md Module diagram & boundaries |
| 62 | +│ ├── build.md Build reference |
| 63 | +│ └── phase-0-handoff.md Pinned versions & decisions |
| 64 | +├── .github/workflows/ CI/CD |
| 65 | +│ ├── mobile-ci.yml Lint + test + debug builds |
| 66 | +│ ├── mobile-openapi-sync.yml API client regeneration |
| 67 | +│ └── mobile-release.yml Signed release (Phase 1) |
| 68 | +└── mobile/ Source tree (Gradle root) |
| 69 | + ├── shared/ Kotlin Multiplatform module |
| 70 | + │ ├── src/commonMain/ Business logic, API, DB, DI |
| 71 | + │ ├── src/androidMain/ Android platform actuals |
| 72 | + │ ├── src/iosMain/ iOS platform actuals |
| 73 | + │ └── src/commonTest/ Shared tests |
| 74 | + ├── androidApp/ Jetpack Compose host |
| 75 | + ├── iosApp/ SwiftUI host (XcodeGen) |
| 76 | + ├── spec/openapi/ Pinned phpMyFAQ API spec |
| 77 | + └── scripts/ Dev tooling |
| 78 | +``` |
| 79 | + |
| 80 | +--- |
| 81 | + |
| 82 | +## Building & running |
| 83 | + |
| 84 | +### Android |
| 85 | + |
| 86 | +```bash |
| 87 | +cd mobile |
| 88 | +./gradlew :androidApp:assembleDebug |
| 89 | +``` |
| 90 | + |
| 91 | +The debug APK lands at |
| 92 | +`mobile/androidApp/build/outputs/apk/debug/androidApp-debug.apk`. |
| 93 | + |
| 94 | +Install on a connected device or emulator: |
| 95 | + |
| 96 | +```bash |
| 97 | +adb install androidApp/build/outputs/apk/debug/androidApp-debug.apk |
| 98 | +``` |
| 99 | + |
| 100 | +Or open `mobile/` as a project in Android Studio and run from the IDE. |
| 101 | + |
| 102 | +### iOS |
| 103 | + |
| 104 | +```bash |
| 105 | +# 1. Build the shared KMP framework |
| 106 | +cd mobile |
| 107 | +./gradlew :shared:assembleSharedDebugXCFramework |
| 108 | + |
| 109 | +# 2. Generate the Xcode project (one-time, re-run after project.yml changes) |
| 110 | +cd iosApp |
| 111 | +xcodegen generate |
| 112 | + |
| 113 | +# 3a. Open in Xcode and run (Cmd+R) |
| 114 | +open iosApp.xcodeproj |
| 115 | + |
| 116 | +# 3b. Or build from CLI |
| 117 | +xcodebuild \ |
| 118 | + -project iosApp.xcodeproj \ |
| 119 | + -scheme iosApp \ |
| 120 | + -configuration Debug \ |
| 121 | + -destination 'platform=iOS Simulator,name=iPhone 16' \ |
| 122 | + CODE_SIGNING_ALLOWED=NO \ |
| 123 | + build |
| 124 | +``` |
| 125 | + |
| 126 | +To run on a simulator from CLI after building: |
| 127 | + |
| 128 | +```bash |
| 129 | +xcrun simctl boot "iPhone 16" 2>/dev/null |
| 130 | +xcrun simctl install booted \ |
| 131 | + build/Build/Products/Debug-iphonesimulator/iosApp.app |
| 132 | +xcrun simctl launch booted app.myfaq.ios |
| 133 | +``` |
| 134 | + |
| 135 | +> **Note:** After changing shared Kotlin code, rebuild the |
| 136 | +> XCFramework (step 1) before rebuilding in Xcode. The pre-build |
| 137 | +> script in `project.yml` does this automatically but adds build |
| 138 | +> time. For faster iteration, disable it in Xcode's Build Phases |
| 139 | +> and run the Gradle task manually. |
| 140 | +
|
| 141 | +--- |
| 142 | + |
| 143 | +## Running tests |
| 144 | + |
| 145 | +```bash |
| 146 | +cd mobile |
| 147 | + |
| 148 | +# Shared module tests (JVM — fast, no device/simulator needed) |
| 149 | +./gradlew :shared:testDebugUnitTest |
| 150 | + |
| 151 | +# Shared module iOS tests (requires macOS + Xcode) |
| 152 | +./gradlew :shared:iosSimulatorArm64Test |
| 153 | + |
| 154 | +# All shared tests |
| 155 | +./gradlew :shared:allTests |
| 156 | +``` |
| 157 | + |
| 158 | +### What the tests cover (Phase 0) |
| 159 | + |
| 160 | +| Test file | What it validates | |
| 161 | +|-----------|-------------------| |
| 162 | +| `MyFaqApiTest` | `/meta` deserialization, unknown-key tolerance, MetaLoader rendering | |
| 163 | +| `EntitlementsTest` | `Entitlements.isPro()` always returns `false` | |
| 164 | +| `SecureStoreContract` | Round-trip put/get/remove/clear (abstract; run by platform tests) | |
| 165 | + |
| 166 | +--- |
| 167 | + |
| 168 | +## Tech stack |
| 169 | + |
| 170 | +| Layer | Technology | |
| 171 | +|-------|-----------| |
| 172 | +| Shared logic | Kotlin Multiplatform | |
| 173 | +| Android UI | Jetpack Compose, Material 3 | |
| 174 | +| iOS UI | SwiftUI | |
| 175 | +| HTTP | Ktor (MockEngine in Phase 0; OkHttp/Darwin in Phase 1) | |
| 176 | +| Serialization | kotlinx.serialization | |
| 177 | +| Database | SQLDelight + SQLCipher (Android) / Data Protection (iOS) | |
| 178 | +| Secure storage | EncryptedSharedPreferences (Android) / Keychain (iOS) | |
| 179 | +| DI | Koin | |
| 180 | +| API codegen | openapi-generator (Kotlin multiplatform target) | |
| 181 | + |
| 182 | +### Pinned versions |
| 183 | + |
| 184 | +See [`mobile/gradle/libs.versions.toml`](mobile/gradle/libs.versions.toml) |
| 185 | +for the single source of truth. Key versions: |
| 186 | + |
| 187 | +- Kotlin **2.1.20**, Gradle **9.4.1**, AGP **8.10.0** |
| 188 | +- Ktor **3.0.3**, SQLDelight **2.0.2**, Koin **4.0.0** |
| 189 | +- Android min SDK **26**, iOS **16.0** |
| 190 | + |
| 191 | +--- |
| 192 | + |
| 193 | +## Regenerating the API client |
| 194 | + |
| 195 | +The Kotlin API client is generated from the pinned phpMyFAQ OpenAPI |
| 196 | +spec at `mobile/spec/openapi/v3.2.yaml`. To update: |
| 197 | + |
| 198 | +```bash |
| 199 | +# Download the spec from a phpMyFAQ release |
| 200 | +curl -fsSL \ |
| 201 | + https://raw.githubusercontent.com/thorsten/phpMyFAQ/4.2.0/docs/openapi.yaml \ |
| 202 | + -o mobile/spec/openapi/v3.2.yaml |
| 203 | + |
| 204 | +# Regenerate (requires openapi-generator-cli or Docker) |
| 205 | +mobile/scripts/generate-api-client.sh |
| 206 | +``` |
| 207 | + |
| 208 | +Or trigger the `mobile-openapi-sync` GitHub Actions workflow. |
| 209 | + |
| 210 | +--- |
| 211 | + |
| 212 | +## Project structure (shared module) |
| 213 | + |
| 214 | +``` |
| 215 | +shared/src/commonMain/kotlin/app/myfaq/shared/ |
| 216 | +├── api/ |
| 217 | +│ ├── MyFaqApi.kt Interface + impl (Phase 0: /meta only) |
| 218 | +│ ├── MetaLoader.kt Callback facade for platform UI |
| 219 | +│ ├── HttpClientFactory.kt MockEngine wiring (Phase 0) |
| 220 | +│ └── dto/Meta.kt /meta response DTO |
| 221 | +├── data/ |
| 222 | +│ └── DatabaseFactory.kt Encrypted DB creation + passphrase mgmt |
| 223 | +├── di/ |
| 224 | +│ └── SharedModule.kt Koin modules + initKoin() |
| 225 | +├── domain/ |
| 226 | +│ └── Instance.kt Instance + AuthMode domain models |
| 227 | +├── entitlements/ |
| 228 | +│ └── Entitlements.kt Pro gate facade + expect object |
| 229 | +└── platform/ |
| 230 | + ├── SecureStore.kt expect class (put/get/remove/clear) |
| 231 | + └── DatabaseDriverFactory.kt expect class (create with passphrase) |
| 232 | +``` |
| 233 | + |
| 234 | +--- |
| 235 | + |
| 236 | +## Phase roadmap |
| 237 | + |
| 238 | +| Phase | What ships | Status | |
| 239 | +|-------|-----------|--------| |
| 240 | +| **0** | KMP scaffold, CI, encrypted DB, secure storage, Entitlements stub | **current** | |
| 241 | +| **1** | Workspaces, categories, FAQ list/detail, server search, paywall shell | planned | |
| 242 | +| **2** | Offline: SQLite + FTS5, background sync, attachments (public v1.0.0) | planned | |
| 243 | +| **3** | Pro: StoreKit 2 + Play Billing, login/OAuth2, ask/comment/rate (v2.0.0) | planned | |
| 244 | +| **4** | Accessibility, localization, telemetry scrub | planned | |
| 245 | +| **5** | Widgets, watch app, iPad split-view, push notifications | planned | |
| 246 | + |
| 247 | +See [`plans/mobile-app-plan.md`](plans/mobile-app-plan.md) for the |
| 248 | +full architecture plan and |
| 249 | +[`plans/phase-0-foundations.md`](plans/phase-0-foundations.md) for |
| 250 | +Phase 0 details. |
| 251 | + |
| 252 | +--- |
| 253 | + |
| 254 | +## Contributing |
| 255 | + |
| 256 | +1. Check out the repo and run `mobile/scripts/bootstrap.sh` |
| 257 | +2. Make changes under `mobile/` |
| 258 | +3. Run `cd mobile && ./gradlew :shared:allTests` before pushing |
| 259 | +4. CI runs on every push to `main` and on PRs (path-filtered to |
| 260 | + `mobile/**`) |
| 261 | + |
| 262 | +### Code style |
| 263 | + |
| 264 | +- Kotlin: enforced by ktlint + detekt (run via `./gradlew ktlintCheck detekt`) |
| 265 | +- Swift: swiftlint (configured in CI) |
| 266 | +- Commit messages: [Conventional Commits](https://www.conventionalcommits.org/) |
| 267 | + |
| 268 | +--- |
| 269 | + |
| 270 | +## License |
| 271 | + |
| 272 | +[Mozilla Public License 2.0](LICENSE) |
0 commit comments