feat(android): migrate auth, telemetry, feature flags to ByteLyst Kotlin Platform SDK
This commit is contained in:
parent
b7688b55d1
commit
6b82ca1b33
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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(":"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user