feat(android): migrate auth, telemetry, feature flags to ByteLyst Kotlin Platform SDK

This commit is contained in:
saravanakumardb1 2026-03-01 18:16:13 -08:00
parent b7688b55d1
commit 6b82ca1b33
9 changed files with 140 additions and 47 deletions

View File

@ -76,6 +76,9 @@ dependencies {
implementation(libs.androidx.glance)
implementation(libs.androidx.glance.material3)
// ByteLyst Platform SDK (via Gradle includeBuild in settings.gradle.kts)
implementation("com.bytelyst.platform:kotlin-platform-sdk")
// Kotlin
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.coroutines.android)

View File

@ -9,8 +9,7 @@ import androidx.compose.material3.Surface
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import com.chronomind.app.auth.AuthService
import com.chronomind.app.auth.AuthState
import com.bytelyst.platform.BLAuthClient
import com.chronomind.app.auth.LoginScreen
import com.chronomind.app.ui.navigation.ChronoMindNavHost
import com.chronomind.app.ui.theme.CMColors
@ -20,17 +19,16 @@ import javax.inject.Inject
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@Inject lateinit var authService: AuthService
@Inject lateinit var authClient: BLAuthClient
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
authService.checkExistingSession()
setContent {
ChronoMindTheme {
val authState by authService.state.collectAsState()
if (authState is AuthState.LoggedIn) {
val authState by authClient.state.collectAsState()
if (authState is BLAuthClient.AuthState.LoggedIn) {
Surface(
modifier = Modifier.fillMaxSize(),
color = CMColors.bg
@ -38,7 +36,7 @@ class MainActivity : ComponentActivity() {
ChronoMindNavHost()
}
} else {
LoginScreen(authService = authService)
LoginScreen(authClient = authClient)
}
}
}

View File

@ -37,12 +37,13 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.bytelyst.platform.BLAuthClient
import com.chronomind.app.ui.theme.CMColors
import kotlinx.coroutines.launch
@Composable
fun LoginScreen(authService: AuthService) {
val authState by authService.state.collectAsState()
fun LoginScreen(authClient: BLAuthClient) {
val authState by authClient.state.collectAsState()
val scope = rememberCoroutineScope()
var isRegister by remember { mutableStateOf(false) }
var name by remember { mutableStateOf("") }
@ -56,7 +57,7 @@ fun LoginScreen(authService: AuthService) {
&& password.any { it.isLowerCase() }
&& password.any { it.isDigit() }
val isValidName = !isRegister || name.trim().isNotEmpty()
val canSubmit = isValidEmail && isValidPassword && isValidName && authState !is AuthState.Loading
val canSubmit = isValidEmail && isValidPassword && isValidName && authState !is BLAuthClient.AuthState.Loading
val fieldColors = OutlinedTextFieldDefaults.colors(
focusedTextColor = CMColors.text,
@ -150,10 +151,10 @@ fun LoginScreen(authService: AuthService) {
)
}
if (authState is AuthState.Error) {
if (authState is BLAuthClient.AuthState.Error) {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = (authState as AuthState.Error).message,
text = (authState as BLAuthClient.AuthState.Error).message,
color = CMColors.error,
style = MaterialTheme.typography.bodySmall,
)
@ -164,8 +165,8 @@ fun LoginScreen(authService: AuthService) {
Button(
onClick = {
scope.launch {
if (isRegister) authService.register(name, email, password)
else authService.login(email, password)
if (isRegister) authClient.register(name, email, password)
else authClient.login(email, password)
}
},
modifier = Modifier
@ -175,7 +176,7 @@ fun LoginScreen(authService: AuthService) {
enabled = canSubmit,
colors = ButtonDefaults.buttonColors(containerColor = CMColors.accent),
) {
if (authState is AuthState.Loading) {
if (authState is BLAuthClient.AuthState.Loading) {
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
strokeWidth = 2.dp,

View File

@ -6,8 +6,6 @@ import com.chronomind.app.data.TimerDao
import com.chronomind.app.data.TimerDatabase
import com.chronomind.app.notifications.TimerNotificationManager
import com.chronomind.app.sync.SyncRepository
import com.chronomind.app.telemetry.FeatureFlagService
import com.chronomind.app.telemetry.TelemetryService
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -54,21 +52,4 @@ object AppModule {
}
}
@Provides
@Singleton
fun provideTelemetryService(
@ApplicationContext context: Context
): TelemetryService {
return TelemetryService(context).also {
it.start()
}
}
@Provides
@Singleton
fun provideFeatureFlagService(
@ApplicationContext context: Context
): FeatureFlagService {
return FeatureFlagService(context)
}
}

View File

@ -0,0 +1,25 @@
package com.chronomind.app.platform
import android.content.Context
import com.bytelyst.platform.BLPlatformConfig
/**
* ChronoMind platform configuration.
*
* Creates a [BLPlatformConfig] with ChronoMind-specific values.
* All SDK components receive this config via Hilt DI.
*/
object Config {
fun create(context: Context): BLPlatformConfig {
val baseUrl = context.applicationInfo.metaData?.getString("PLATFORM_SERVICE_URL")
?: "https://api.chronomind.app"
return BLPlatformConfig(
productId = "chronomind",
baseUrl = "$baseUrl/api",
platform = "android",
channel = "native",
applicationId = "com.chronomind.app",
)
}
}

View File

@ -0,0 +1,79 @@
package com.chronomind.app.platform
import android.content.Context
import com.bytelyst.platform.BLAuthClient
import com.bytelyst.platform.BLFeatureFlagClient
import com.bytelyst.platform.BLKillSwitchClient
import com.bytelyst.platform.BLPlatformConfig
import com.bytelyst.platform.BLSecureStore
import com.bytelyst.platform.BLTelemetryClient
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
/**
* Hilt module providing ByteLyst Platform SDK components.
*
* Replaces the hand-rolled AuthService, TelemetryService, and FeatureFlagService
* with SDK-backed implementations.
*/
@Module
@InstallIn(SingletonComponent::class)
object PlatformModule {
@Provides
@Singleton
fun provideConfig(@ApplicationContext context: Context): BLPlatformConfig {
return Config.create(context)
}
@Provides
@Singleton
fun provideSecureStore(
@ApplicationContext context: Context,
config: BLPlatformConfig,
): BLSecureStore {
return BLSecureStore(context, config.applicationId)
}
@Provides
@Singleton
fun provideAuthClient(
config: BLPlatformConfig,
secureStore: BLSecureStore,
): BLAuthClient {
return BLAuthClient(config, secureStore).also {
it.checkExistingSession()
}
}
@Provides
@Singleton
fun provideTelemetryClient(
config: BLPlatformConfig,
@ApplicationContext context: Context,
): BLTelemetryClient {
return BLTelemetryClient(config, context).also {
it.start()
}
}
@Provides
@Singleton
fun provideFeatureFlagClient(
config: BLPlatformConfig,
): BLFeatureFlagClient {
return BLFeatureFlagClient(config)
}
@Provides
@Singleton
fun provideKillSwitchClient(
config: BLPlatformConfig,
): BLKillSwitchClient {
return BLKillSwitchClient(config)
}
}

View File

@ -11,8 +11,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.ViewModel
import com.chronomind.app.auth.AuthService
import com.chronomind.app.auth.AuthState
import com.bytelyst.platform.BLAuthClient
import com.chronomind.app.engine.CascadePreset
import com.chronomind.app.engine.UrgencyLevel
import com.chronomind.app.engine.getUrgencyConfig
@ -22,12 +21,12 @@ import javax.inject.Inject
@HiltViewModel
class SettingsViewModel @Inject constructor(
val authService: AuthService,
val authClient: BLAuthClient,
) : ViewModel()
@Composable
fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
val authState by viewModel.authService.state.collectAsState()
val authState by viewModel.authClient.state.collectAsState()
var defaultUrgency by remember { mutableStateOf(UrgencyLevel.STANDARD) }
var defaultCascade by remember { mutableStateOf(CascadePreset.STANDARD) }
var hapticEnabled by remember { mutableStateOf(true) }
@ -50,8 +49,8 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
// Account
SettingsSection("Account") {
if (authState is AuthState.LoggedIn) {
val user = (authState as AuthState.LoggedIn).user
if (authState is BLAuthClient.AuthState.LoggedIn) {
val user = (authState as BLAuthClient.AuthState.LoggedIn).user
SettingsRow("Email") {
Text(user.email, color = CMColors.textMuted, fontSize = 14.sp)
}
@ -64,7 +63,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
.padding(horizontal = 16.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.End,
) {
TextButton(onClick = { viewModel.authService.logout() }) {
TextButton(onClick = { viewModel.authClient.logout() }) {
Text("Sign Out", color = CMColors.error)
}
}

View File

@ -7,7 +7,7 @@ import com.chronomind.app.data.toEntity
import com.chronomind.app.data.toTimer
import com.chronomind.app.engine.*
import com.chronomind.app.sync.SyncRepository
import com.chronomind.app.telemetry.TelemetryService
import com.bytelyst.platform.BLTelemetryClient
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
@ -23,7 +23,7 @@ import javax.inject.Inject
class TimerViewModel @Inject constructor(
private val timerDao: TimerDao,
private val syncRepository: SyncRepository,
private val telemetryService: TelemetryService,
private val telemetryClient: BLTelemetryClient,
) : ViewModel() {
private val _timers = MutableStateFlow<List<CMTimer>>(emptyList())
@ -70,7 +70,7 @@ class TimerViewModel @Inject constructor(
_timers.value = _timers.value + timer
persist(timer)
if (syncRepository.syncEnabled) syncRepository.enqueueCreate(timer)
telemetryService.trackTimer("timer_created", tags = mapOf("type" to "countdown"))
telemetryClient.trackEvent("info", "timers", "timer_created", tags = mapOf("type" to "countdown"))
}
fun addAlarm(label: String, targetTime: Date, urgency: UrgencyLevel = UrgencyLevel.STANDARD) {
@ -78,7 +78,7 @@ class TimerViewModel @Inject constructor(
_timers.value = _timers.value + timer
persist(timer)
if (syncRepository.syncEnabled) syncRepository.enqueueCreate(timer)
telemetryService.trackTimer("timer_created", tags = mapOf("type" to "alarm", "urgency" to urgency.name))
telemetryClient.trackEvent("info", "timers", "timer_created", tags = mapOf("type" to "alarm", "urgency" to urgency.name))
}
fun addPomodoro(label: String = "Focus Session", config: PomodoroConfig = PomodoroConfig()) {
@ -86,7 +86,7 @@ class TimerViewModel @Inject constructor(
_timers.value = _timers.value + timer
persist(timer)
if (syncRepository.syncEnabled) syncRepository.enqueueCreate(timer)
telemetryService.trackTimer("timer_created", tags = mapOf("type" to "pomodoro"))
telemetryClient.trackEvent("info", "timers", "timer_created", tags = mapOf("type" to "pomodoro"))
}
fun pause(id: String) {
@ -118,7 +118,7 @@ class TimerViewModel @Inject constructor(
_timers.value = _timers.value.filter { it.id != id }
viewModelScope.launch { timerDao.deleteById(id) }
if (syncRepository.syncEnabled) syncRepository.enqueueDelete(id)
telemetryService.trackTimer("timer_deleted")
telemetryClient.trackEvent("info", "timers", "timer_deleted")
}
// MARK: - Tick

View File

@ -22,3 +22,10 @@ dependencyResolution {
rootProject.name = "ChronoMind"
include(":app")
include(":wear")
// ByteLyst Platform SDK — local composite build from sibling repo
includeBuild("../../learning_ai_common_plat/packages/kotlin-platform-sdk") {
dependencySubstitution {
substitute(module("com.bytelyst.platform:kotlin-platform-sdk")).using(project(":"))
}
}