Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
118ea11
Add adaptive width breakpoint helper for home navigation drawer
LZRS Jul 15, 2026
8a31da1
Add HomeScreen with adaptive navigation drawer around GroupListScreen
LZRS Jul 15, 2026
24848d1
Make HomeScreen the app's start destination
LZRS Jul 15, 2026
f59e680
Split home drawer into Registers and bottom Sync now sections
LZRS Jul 15, 2026
14907d9
Configure remote FHIR server on all platform entry points
LZRS Jul 22, 2026
a8dbee6
Add AppFhirSyncTask and SyncNowUseCase for one-time FHIR sync
LZRS Jul 22, 2026
13ce364
Add HomeViewModel driving sync state
LZRS Jul 22, 2026
5b0a2c5
Wire Sync now to HomeViewModel and real one-time FHIR sync
LZRS Jul 22, 2026
089d757
Wire HomeScreen and DI to HomeViewModel
LZRS Jul 22, 2026
476cb45
Add auth dependencies and generateAuthConfig codegen task
LZRS Jul 28, 2026
753da4d
Add auth data models (Session, AuthState, LoginOutcome, ...)
LZRS Jul 28, 2026
62e0e6b
Add OAuthConfig with OIDC discovery URL
LZRS Jul 28, 2026
5ea673e
Add PKCE helpers with per-platform secure random bytes
LZRS Jul 28, 2026
809cd71
Add OidcAuthApi (discovery, token exchange, refresh, userinfo, logout)
LZRS Jul 28, 2026
9dc0207
Add KSafe-backed SessionRepository with per-platform storage
LZRS Jul 28, 2026
ebc3cd3
Add AuthorizationLauncher with JVM desktop loopback-redirect actual
LZRS Jul 28, 2026
60f39f7
Add Android AuthorizationLauncher (Custom Tabs + deep link redirect)
LZRS Jul 28, 2026
456cb53
Add iOS AuthorizationLauncher (ASWebAuthenticationSession)
LZRS Jul 28, 2026
34f172e
Add Web AuthorizationLauncher (full-page redirect)
LZRS Jul 28, 2026
f95ef91
Add AuthService (PKCE flow, session refresh/revalidation) and FhirBea…
LZRS Jul 28, 2026
74de755
Add AuthViewModel adapting AuthService to Compose state
LZRS Jul 28, 2026
2dad93d
Wire auth classes through Koin (authModule, AuthViewModel)
LZRS Jul 28, 2026
86206f5
Authenticate FHIR sync with the session's Bearer token on all platforms
LZRS Jul 28, 2026
8ab3727
Add adaptive LoginScreen
LZRS Jul 28, 2026
a0f990d
Gate App() NavHost behind AuthViewModel's auth state
LZRS Jul 28, 2026
9d8645c
Add Sign out entry to the home navigation drawer
LZRS Jul 28, 2026
06ed1b0
Apply formatting pass and bump ohs-fhir-engine to 2.0.0-alpha02
LZRS Jul 28, 2026
8bfa93b
Fix Android Internet permission
LZRS Jul 28, 2026
968934a
Add FhirRepository.hasAnyData(), backed by FhirEngine.count()
LZRS Jul 28, 2026
c9b1002
Clear local FHIR data on logout (fhirEngine.clearDatabase())
LZRS Jul 28, 2026
7f41eaf
Add InitialSyncViewModel gating a session on local data presence
LZRS Jul 28, 2026
2712a01
Add InitialSyncScreen (blocking progress + retry/continue UI)
LZRS Jul 28, 2026
ac7e464
Gate Authenticated sessions behind InitialSyncViewModel before showin…
LZRS Jul 28, 2026
42d25bf
Fix AuthModuleTest for AuthViewModel's new FhirEngine dependency
LZRS Jul 28, 2026
50d16e6
Remove redundant TopAppBar from the Households screen
LZRS Jul 28, 2026
1efaa7c
Replace RunSyncNowUseCase with per-platform sync implementations
LZRS Jul 29, 2026
da4a6c7
Add Sync.cancelOneTimeSync to the JVM/web foreground scheduler
LZRS Jul 29, 2026
2d06686
Add Sync.periodicSync with a per-platform connectivity gate
LZRS Jul 29, 2026
25d2761
Add SyncNowUseCase.cancel() and implement it on every platform
LZRS Jul 29, 2026
c1bbadb
Add PeriodicSyncUseCase with a WorkManager-backed Android implementation
LZRS Jul 29, 2026
3d13245
Add ForegroundPeriodicSyncUseCase for JVM and web
LZRS Jul 29, 2026
6c7a7c2
Add IosPeriodicSyncUseCase backed by BGTaskScheduler
LZRS Jul 29, 2026
fdd6d7c
Start periodic sync once InitialSyncViewModel passes its gate
LZRS Jul 29, 2026
95afd7b
Turn the drawer's Sync now item into a start/cancel toggle
LZRS Jul 29, 2026
3576cf9
Apply spotless formatting
LZRS Jul 29, 2026
f6503cf
Fix sqlite-wasm worker request validation
ellykits Jul 29, 2026
cc42874
Document identity-provider configuration in the README
ellykits Jul 29, 2026
9d7ecd4
Move the OAuthConfig default note into its class KDoc
ellykits Jul 29, 2026
244cef4
Unify desktop storage under ~/.player-reference
ellykits Jul 29, 2026
4166fe5
Replace the sync use cases with a single SyncManager
ellykits Jul 29, 2026
2029bd4
Refresh the session before headless background sync
ellykits Jul 29, 2026
9518316
Cancel sync and keep local data on logout, cover redirect bootstrap
ellykits Jul 29, 2026
b20e341
Harden iOS secure random and Keychain accessibility
ellykits Jul 29, 2026
084ee71
Extract user-facing strings to composeResources
ellykits Jul 29, 2026
4b1a680
Gate initial sync on a persisted success flag
ellykits Jul 29, 2026
1f991bb
Rebrand the app to Player Reference with the new logo
ellykits Jul 29, 2026
b43750a
Refresh the registers after a sync completes
ellykits Jul 29, 2026
296ff11
Seed the theme palette from the app logo
ellykits Jul 29, 2026
34acf0d
Unescape apostrophes in string resources
ellykits Jul 29, 2026
45b2685
Center the questionnaire form on wide screens
ellykits Jul 29, 2026
9dce755
Show households alongside their detail on wide screens
ellykits Jul 29, 2026
f151e6e
Add member detail pane, a nav rail, and shell polish
ellykits Jul 29, 2026
42c1df5
Flatten cards, brand the drawer, and drop marketing copy
ellykits Jul 29, 2026
f874e9d
Render list rows as flat rows with dividers
ellykits Jul 29, 2026
c9055ed
Replace list cards with flat selectable rows
ellykits Jul 29, 2026
976f6a2
Redesign login as a flat brand-led screen
ellykits Jul 29, 2026
52a718a
Rework the questionnaire top bar
ellykits Jul 29, 2026
6c1e455
Flatten the patient and household detail screens
ellykits Jul 29, 2026
3a9e069
Drop the empty band under the register title
ellykits Jul 29, 2026
e502f04
Recenter the launcher icon in its safe zone
ellykits Jul 29, 2026
eb4142e
Render the patient header without a card
ellykits Jul 29, 2026
08808c4
Order households by last updated, newest first
ellykits Jul 29, 2026
b2764ef
Merge pull request #1 from ellykits/fix-sync-login
LZRS Jul 29, 2026
b454326
Suppress RestrictedApi for Result.Success check in sync worker
LZRS Aug 3, 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
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,51 @@ Code generation is part of compilation. The `ig-codegen` Gradle plugin runs its

For iOS, open [`iosApp/`](./iosApp) in Xcode and run, or use the run-configuration widget in a Kotlin Multiplatform IDE.

## Identity provider (OIDC) configuration

Sign-in is provider-agnostic. The client speaks standard OpenID Connect and resolves every endpoint — authorization, token, userinfo, and end-session — at runtime from the provider's discovery document at `{issuer}/.well-known/openid-configuration`. Nothing is hardcoded per provider, so switching from one OAuth2/OIDC provider to another is a configuration change, not a code change: point `OAUTH_ISSUER` at the new provider (and update the client id, scopes, and redirect registration to match). See [`OidcAuthApi`](./ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/auth/OidcAuthApi.kt) and [`OAuthConfig`](./ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/auth/OAuthConfig.kt).

The flow is Authorization Code with PKCE and a `state` check. The provider must therefore expose the client as a **public client** (no client secret) that permits PKCE, and the app's redirect URIs must be registered as allowed redirects. Any standards-compliant OIDC provider — Keycloak, Okta, Zitadel, and others — works under these constraints.

### Configure a provider

1. Register a public (PKCE) client at your provider and note its client id.
2. Register the per-platform redirect URIs below as allowed redirect URIs.
3. Copy `local.properties.sample` to `local.properties` and fill in the keys below. The file is git-ignored; CI may override any key with an environment variable of the same name.

Configuration is read at build time by the `generateAuthConfig` task and baked into `GeneratedAuthConfig`, which backs `OAuthConfig.Default`.

| Key | Meaning | Example |
| --- | --- | --- |
| `OAUTH_ISSUER` | OIDC issuer; discovery is performed against `{issuer}/.well-known/openid-configuration` | `https://keycloak.example.org/realms/ohs-player` |
| `OAUTH_CLIENT_ID` | Public client id (PKCE, no secret) | `ohs-player-reference-app` |
| `OAUTH_SCOPES` | Space-separated scopes; `offline_access` yields a refresh token | `openid profile email offline_access` |
| `OAUTH_REDIRECT_SCHEME` | Custom URI scheme for the mobile deep-link redirect | `dev.ohs.player.reference.app` |
| `OAUTH_REDIRECT_HOST` | Host component of the mobile redirect | `auth` |
| `OAUTH_DESKTOP_REDIRECT_PORT` | Localhost loopback port for the desktop (JVM) redirect | `8765` |
| `OAUTH_WEB_REDIRECT_URL` | Full-page redirect URL for the web (JS/Wasm) build | `http://localhost:8080/callback` |
| `FHIR_BASE_URL` | Base URL of the FHIR server; requests carry the session Bearer token | `https://hapi.fhir.org/baseR4` |

### Issuer examples

The issuer is the only value that identifies the provider. Note that for Keycloak the realm is part of the issuer.

| Provider | `OAUTH_ISSUER` |
| --- | --- |
| Keycloak | `https://host/realms/<realm>` |
| Okta | `https://<org>.okta.com` (or a custom authorization server, `https://<org>.okta.com/oauth2/<server-id>`) |
| Zitadel | `https://<instance>.zitadel.cloud` |

### Redirect URIs to register

Each platform completes the authorization redirect differently, so register all of the ones you build for:

| Platform | Redirect URI | Derived from |
| --- | --- | --- |
| Android / iOS | `{OAUTH_REDIRECT_SCHEME}://{OAUTH_REDIRECT_HOST}` | e.g. `dev.ohs.player.reference.app://auth` |
| Desktop (JVM) | `http://127.0.0.1:{OAUTH_DESKTOP_REDIRECT_PORT}/callback` (also register the `http://localhost:...` form) | loopback port |
| Web (JS/Wasm) | `OAUTH_WEB_REDIRECT_URL` | e.g. `http://localhost:8080/callback` |

## From FHIR data to view state

A screen never consumes a raw FHIR resource. It consumes a typed *view-state* — a flat, serializable data class containing exactly the fields the screen needs. View-state is produced by a configuration-driven pipeline:
Expand Down
3 changes: 3 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ android.useAndroidX=true

#MPP
kotlin.mpp.enableCInteropCommonization=true

# Enabled parallel sync for Gradle 9.4+
org.gradle.tooling.parallel=true
29 changes: 28 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,19 @@ androidx-core = "1.18.0"
androidx-espresso = "3.7.0"
androidx-lifecycle = "2.10.0"
androidx-testExt = "1.3.0"
androidx-work = "2.8.1"
composeHotReload = "1.1.1"
composeMultiplatform = "1.11.0"
composeMaterialIcons = "1.7.3"
fhirDataCapture = "2.0.0-alpha02"
junit = "4.13.2"
kermit = "2.1.0"
koinBom = "4.1.1"
ktor = "3.2.3"
ksafe = "2.1.3"
kotlincryptoHash = "0.8.0"
androidxBrowser = "1.8.0"
kotlinxBrowser = "0.3"
kotlin = "2.3.21"
kotlinx-coroutines = "1.11.0"
kotlinpoet = "2.3.0"
Expand All @@ -24,9 +30,10 @@ ktfmt = "0.54"
ktlint = "1.5.0"
navigation-compose = "2.9.2"
material3 = "1.10.0-alpha05"
composeAdaptive = "1.2.0"
kotlinxSerializationJson = "1.11.0"
spotless = "8.6.0"
ohsFhirEngine = "2.0.0-alpha01"
ohsFhirEngine = "2.0.0-alpha02"
ohsFhirModel = "1.0.0-beta05"
ohsFhirPath = "1.0.0-beta03"
kotlinxDatetime = "0.8.0"
Expand All @@ -36,6 +43,7 @@ ionspin-bignum = "0.3.10"
fhir-data-capture = { module = "dev.ohs.fhir:fhir-data-capture", version.ref = "fhirDataCapture" }
kermit = { module = "co.touchlab:kermit", version.ref = "kermit" }
koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koinBom" }
koin-test = { module = "io.insert-koin:koin-test" }
koin-core = { module = "io.insert-koin:koin-core" }
koin-compose = { module = "io.insert-koin:koin-compose" }
koin-composeViewmodel = { module = "io.insert-koin:koin-compose-viewmodel" }
Expand All @@ -54,6 +62,10 @@ compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.re
compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "composeMultiplatform" }
compose-material = { module = "org.jetbrains.compose.material:material", version.ref = "composeMultiplatform" }
compose-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "material3" }
compose-adaptive = { module = "org.jetbrains.compose.material3.adaptive:adaptive", version.ref = "composeAdaptive" }
compose-adaptive-layout = { module = "org.jetbrains.compose.material3.adaptive:adaptive-layout", version.ref = "composeAdaptive" }
compose-adaptive-navigation = { module = "org.jetbrains.compose.material3.adaptive:adaptive-navigation", version.ref = "composeAdaptive" }
compose-material3-adaptive-navigation-suite = { module = "org.jetbrains.compose.material3:material3-adaptive-navigation-suite", version.ref = "material3" }
compose-materialIconsCore = { module = "org.jetbrains.compose.material:material-icons-core", version.ref = "composeMaterialIcons" }
compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "composeMultiplatform" }
compose-components-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "composeMultiplatform" }
Expand All @@ -70,6 +82,21 @@ ohs-fhir-model = { module = "dev.ohs.fhir:fhir-model", version.ref = "ohsFhirMod
ohs-fhir-path = { module = "dev.ohs.fhir:fhir-path", version.ref = "ohsFhirPath" }
ionspin-bignum = { module = "com.ionspin.kotlin:bignum", version.ref = "ionspin-bignum" }
spotless = { module = "com.diffplug.spotless:spotless-plugin-gradle", version.ref = "spotless" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-auth = { module = "io.ktor:ktor-client-auth", version.ref = "ktor" }
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
ktor-client-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor" }
ktor-client-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktor" }
ksafe = { module = "eu.anifantakis:ksafe", version.ref = "ksafe" }
androidx-browser = { module = "androidx.browser:browser", version.ref = "androidxBrowser" }
androidx-work-runtime = { module = "androidx.work:work-runtime-ktx", version.ref = "androidx-work" }
kotlincrypto-hash-bom = { module = "org.kotlincrypto.hash:bom", version.ref = "kotlincryptoHash" }
kotlincrypto-hash-sha2 = { module = "org.kotlincrypto.hash:sha2" }
kotlinx-browser = { module = "org.jetbrains.kotlinx:kotlinx-browser", version.ref = "kotlinxBrowser" }

[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions iosApp/iosApp/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,17 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDisplayName</key>
<string>Player Reference</string>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>dev.ohs.player.reference.app.sync.periodic</string>
</array>
<key>UIBackgroundModes</key>
<array>
<string>processing</string>
</array>
</dict>
</plist>
33 changes: 33 additions & 0 deletions local.properties.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# ---------------------------------------------------------------------------
# local.properties.sample — copy to `local.properties` and fill in your values.
# `local.properties` is git-ignored. These are read at build time by the
# :ohs-player-reference-app `generateAuthConfig` task. CI may override any of
# these with environment variables of the same name.
#
# PKCE uses a PUBLIC client, so there is NO client secret here.
# ---------------------------------------------------------------------------

# --- OAuth / OIDC provider ---------------------------------------------------
# Provider-agnostic: the app resolves all endpoints via OIDC discovery at
# {OAUTH_ISSUER}/.well-known/openid-configuration.
# Keycloak: https://host/realms/<realm> (the realm IS part of the issuer)
OAUTH_ISSUER=https://keycloak.example.org/realms/ohs-player
# Public client id (PKCE / public client — no secret).
OAUTH_CLIENT_ID=ohs-player-reference-app
# OAuth scopes (space separated). offline_access yields a refresh token.
OAUTH_SCOPES=openid profile email offline_access

# --- Redirect URIs (must be registered as Valid Redirect URIs at the provider) ---
# Android + iOS use a custom URI scheme deep link: {SCHEME}://{HOST}
OAUTH_REDIRECT_SCHEME=dev.ohs.player.reference.app
OAUTH_REDIRECT_HOST=auth
# Desktop (JVM) uses a localhost loopback redirect on this port.
# Register as: http://127.0.0.1:8765/callback (and http://localhost:8765/callback)
OAUTH_DESKTOP_REDIRECT_PORT=8765
# Web (JS/Wasm) uses a full-page redirect back to the app origin.
OAUTH_WEB_REDIRECT_URL=http://localhost:8080/callback

# --- FHIR server --------------------------------------------------------------
# Base URL of the remote FHIR server. Requests are authenticated with the
# signed-in session's Bearer token (see FhirBearerAuthenticator).
FHIR_BASE_URL=https://hapi.fhir.org/baseR4
Loading
Loading