Initial commit: BountyBoard Android client

Native Kotlin + Jetpack Compose (Material 3) client for the BountyBoard
platform. Cookie-session + CSRF auth with a configurable base URL, the full
role-based feature set (admin / consultant / developer), local + OIDC SSO
sign-in, and background push notifications (WorkManager poll) for alerts and
messages. Bundles the app's editorial "ledger" design system and the original
Fraunces / Schibsted Grotesk / Spline Sans Mono typefaces.

Includes a prebuilt debug APK (BountyBoard-debug.apk) for side-loading.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vojtěch Maršál
2026-06-13 14:31:31 +02:00
commit d0b1529c3f
88 changed files with 10197 additions and 0 deletions
@@ -0,0 +1,60 @@
package com.anypreta.bountyboard
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.core.content.ContextCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.anypreta.bountyboard.data.push.PushNotifier
import com.anypreta.bountyboard.di.LocalAppContainer
import com.anypreta.bountyboard.ui.nav.AppRoot
import com.anypreta.bountyboard.ui.theme.BBTheme
import com.anypreta.bountyboard.ui.theme.BountyBoardTheme
class MainActivity : ComponentActivity() {
private val requestNotifPermission =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { /* no-op */ }
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
super.onCreate(savedInstanceState)
enableEdgeToEdge()
val container = (application as BountyBoardApp).container
// Deep link from a tapped system notification.
intent?.getStringExtra(PushNotifier.EXTRA_ROUTE)?.let { container.setDeepLink(it) }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) !=
PackageManager.PERMISSION_GRANTED
) {
requestNotifPermission.launch(Manifest.permission.POST_NOTIFICATIONS)
}
setContent {
val themeId by container.settings.theme.collectAsStateWithLifecycle(initialValue = "light")
CompositionLocalProvider(LocalAppContainer provides container) {
BountyBoardTheme(theme = BBTheme.from(themeId)) {
AppRoot()
}
}
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
val container = (application as BountyBoardApp).container
intent.getStringExtra(PushNotifier.EXTRA_ROUTE)?.let { container.setDeepLink(it) }
}
}