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)
implementation(libs.androidx.glance.material3) implementation(libs.androidx.glance.material3)
// ByteLyst Platform SDK (via Gradle includeBuild in settings.gradle.kts)
implementation("com.bytelyst.platform:kotlin-platform-sdk")
// Kotlin // Kotlin
implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.coroutines.android) 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.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.chronomind.app.auth.AuthService import com.bytelyst.platform.BLAuthClient
import com.chronomind.app.auth.AuthState
import com.chronomind.app.auth.LoginScreen import com.chronomind.app.auth.LoginScreen
import com.chronomind.app.ui.navigation.ChronoMindNavHost import com.chronomind.app.ui.navigation.ChronoMindNavHost
import com.chronomind.app.ui.theme.CMColors import com.chronomind.app.ui.theme.CMColors
@ -20,17 +19,16 @@ import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@Inject lateinit var authService: AuthService @Inject lateinit var authClient: BLAuthClient
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
authService.checkExistingSession()
setContent { setContent {
ChronoMindTheme { ChronoMindTheme {
val authState by authService.state.collectAsState() val authState by authClient.state.collectAsState()
if (authState is AuthState.LoggedIn) { if (authState is BLAuthClient.AuthState.LoggedIn) {
Surface( Surface(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
color = CMColors.bg color = CMColors.bg
@ -38,7 +36,7 @@ class MainActivity : ComponentActivity() {
ChronoMindNavHost() ChronoMindNavHost()
} }
} else { } 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.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.bytelyst.platform.BLAuthClient
import com.chronomind.app.ui.theme.CMColors import com.chronomind.app.ui.theme.CMColors
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Composable @Composable
fun LoginScreen(authService: AuthService) { fun LoginScreen(authClient: BLAuthClient) {
val authState by authService.state.collectAsState() val authState by authClient.state.collectAsState()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var isRegister by remember { mutableStateOf(false) } var isRegister by remember { mutableStateOf(false) }
var name by remember { mutableStateOf("") } var name by remember { mutableStateOf("") }
@ -56,7 +57,7 @@ fun LoginScreen(authService: AuthService) {
&& password.any { it.isLowerCase() } && password.any { it.isLowerCase() }
&& password.any { it.isDigit() } && password.any { it.isDigit() }
val isValidName = !isRegister || name.trim().isNotEmpty() 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( val fieldColors = OutlinedTextFieldDefaults.colors(
focusedTextColor = CMColors.text, 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)) Spacer(modifier = Modifier.height(8.dp))
Text( Text(
text = (authState as AuthState.Error).message, text = (authState as BLAuthClient.AuthState.Error).message,
color = CMColors.error, color = CMColors.error,
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
) )
@ -164,8 +165,8 @@ fun LoginScreen(authService: AuthService) {
Button( Button(
onClick = { onClick = {
scope.launch { scope.launch {
if (isRegister) authService.register(name, email, password) if (isRegister) authClient.register(name, email, password)
else authService.login(email, password) else authClient.login(email, password)
} }
}, },
modifier = Modifier modifier = Modifier
@ -175,7 +176,7 @@ fun LoginScreen(authService: AuthService) {
enabled = canSubmit, enabled = canSubmit,
colors = ButtonDefaults.buttonColors(containerColor = CMColors.accent), colors = ButtonDefaults.buttonColors(containerColor = CMColors.accent),
) { ) {
if (authState is AuthState.Loading) { if (authState is BLAuthClient.AuthState.Loading) {
CircularProgressIndicator( CircularProgressIndicator(
modifier = Modifier.size(20.dp), modifier = Modifier.size(20.dp),
strokeWidth = 2.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.data.TimerDatabase
import com.chronomind.app.notifications.TimerNotificationManager import com.chronomind.app.notifications.TimerNotificationManager
import com.chronomind.app.sync.SyncRepository import com.chronomind.app.sync.SyncRepository
import com.chronomind.app.telemetry.FeatureFlagService
import com.chronomind.app.telemetry.TelemetryService
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn 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.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.chronomind.app.auth.AuthService import com.bytelyst.platform.BLAuthClient
import com.chronomind.app.auth.AuthState
import com.chronomind.app.engine.CascadePreset import com.chronomind.app.engine.CascadePreset
import com.chronomind.app.engine.UrgencyLevel import com.chronomind.app.engine.UrgencyLevel
import com.chronomind.app.engine.getUrgencyConfig import com.chronomind.app.engine.getUrgencyConfig
@ -22,12 +21,12 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class SettingsViewModel @Inject constructor( class SettingsViewModel @Inject constructor(
val authService: AuthService, val authClient: BLAuthClient,
) : ViewModel() ) : ViewModel()
@Composable @Composable
fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) { 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 defaultUrgency by remember { mutableStateOf(UrgencyLevel.STANDARD) }
var defaultCascade by remember { mutableStateOf(CascadePreset.STANDARD) } var defaultCascade by remember { mutableStateOf(CascadePreset.STANDARD) }
var hapticEnabled by remember { mutableStateOf(true) } var hapticEnabled by remember { mutableStateOf(true) }
@ -50,8 +49,8 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
// Account // Account
SettingsSection("Account") { SettingsSection("Account") {
if (authState is AuthState.LoggedIn) { if (authState is BLAuthClient.AuthState.LoggedIn) {
val user = (authState as AuthState.LoggedIn).user val user = (authState as BLAuthClient.AuthState.LoggedIn).user
SettingsRow("Email") { SettingsRow("Email") {
Text(user.email, color = CMColors.textMuted, fontSize = 14.sp) 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), .padding(horizontal = 16.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.End, horizontalArrangement = Arrangement.End,
) { ) {
TextButton(onClick = { viewModel.authService.logout() }) { TextButton(onClick = { viewModel.authClient.logout() }) {
Text("Sign Out", color = CMColors.error) 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.data.toTimer
import com.chronomind.app.engine.* import com.chronomind.app.engine.*
import com.chronomind.app.sync.SyncRepository import com.chronomind.app.sync.SyncRepository
import com.chronomind.app.telemetry.TelemetryService import com.bytelyst.platform.BLTelemetryClient
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -23,7 +23,7 @@ import javax.inject.Inject
class TimerViewModel @Inject constructor( class TimerViewModel @Inject constructor(
private val timerDao: TimerDao, private val timerDao: TimerDao,
private val syncRepository: SyncRepository, private val syncRepository: SyncRepository,
private val telemetryService: TelemetryService, private val telemetryClient: BLTelemetryClient,
) : ViewModel() { ) : ViewModel() {
private val _timers = MutableStateFlow<List<CMTimer>>(emptyList()) private val _timers = MutableStateFlow<List<CMTimer>>(emptyList())
@ -70,7 +70,7 @@ class TimerViewModel @Inject constructor(
_timers.value = _timers.value + timer _timers.value = _timers.value + timer
persist(timer) persist(timer)
if (syncRepository.syncEnabled) syncRepository.enqueueCreate(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) { fun addAlarm(label: String, targetTime: Date, urgency: UrgencyLevel = UrgencyLevel.STANDARD) {
@ -78,7 +78,7 @@ class TimerViewModel @Inject constructor(
_timers.value = _timers.value + timer _timers.value = _timers.value + timer
persist(timer) persist(timer)
if (syncRepository.syncEnabled) syncRepository.enqueueCreate(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()) { fun addPomodoro(label: String = "Focus Session", config: PomodoroConfig = PomodoroConfig()) {
@ -86,7 +86,7 @@ class TimerViewModel @Inject constructor(
_timers.value = _timers.value + timer _timers.value = _timers.value + timer
persist(timer) persist(timer)
if (syncRepository.syncEnabled) syncRepository.enqueueCreate(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) { fun pause(id: String) {
@ -118,7 +118,7 @@ class TimerViewModel @Inject constructor(
_timers.value = _timers.value.filter { it.id != id } _timers.value = _timers.value.filter { it.id != id }
viewModelScope.launch { timerDao.deleteById(id) } viewModelScope.launch { timerDao.deleteById(id) }
if (syncRepository.syncEnabled) syncRepository.enqueueDelete(id) if (syncRepository.syncEnabled) syncRepository.enqueueDelete(id)
telemetryService.trackTimer("timer_deleted") telemetryClient.trackEvent("info", "timers", "timer_deleted")
} }
// MARK: - Tick // MARK: - Tick

View File

@ -22,3 +22,10 @@ dependencyResolution {
rootProject.name = "ChronoMind" rootProject.name = "ChronoMind"
include(":app") include(":app")
include(":wear") 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(":"))
}
}